mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-02-07 01:28:32 -06:00
Merge pull request #786 from bluewave-labs/feat/button-loading
Feat/button loading, resolves #727
This commit is contained in:
@@ -1,25 +1,13 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Divider,
|
||||
IconButton,
|
||||
Modal,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Button, ButtonGroup, Modal, Stack, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import EditSvg from "../../../assets/icons/edit.svg?react";
|
||||
import Field from "../../Inputs/Field";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import { networkService } from "../../../main";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import BasicTable from "../../BasicTable";
|
||||
import Remove from "../../../assets/icons/trash-bin.svg?react";
|
||||
import Select from "../../Inputs/Select";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
|
||||
@@ -54,6 +42,7 @@ const TeamPanel = () => {
|
||||
const [members, setMembers] = useState([]);
|
||||
const [filter, setFilter] = useState("all");
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSendingInvite, setIsSendingInvite] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTeam = async () => {
|
||||
@@ -173,6 +162,7 @@ const TeamPanel = () => {
|
||||
};
|
||||
|
||||
const handleInviteMember = async () => {
|
||||
setIsSendingInvite(true);
|
||||
if (!toInvite.role.includes("user") || !toInvite.role.includes("admin"))
|
||||
setToInvite((prev) => ({ ...prev, role: ["user"] }));
|
||||
|
||||
@@ -185,24 +175,28 @@ const TeamPanel = () => {
|
||||
|
||||
if (error) {
|
||||
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
|
||||
} else
|
||||
try {
|
||||
await networkService.requestInvitationToken(
|
||||
authToken,
|
||||
toInvite.email,
|
||||
toInvite.role
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
closeInviteModal();
|
||||
createToast({
|
||||
body: "Member invited. They will receive an email with details on how to create their account.",
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message || "Unknown error.",
|
||||
});
|
||||
}
|
||||
try {
|
||||
await networkService.requestInvitationToken(
|
||||
authToken,
|
||||
toInvite.email,
|
||||
toInvite.role
|
||||
);
|
||||
closeInviteModal();
|
||||
createToast({
|
||||
body: "Member invited. They will receive an email with details on how to create their account.",
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message || "Unknown error.",
|
||||
});
|
||||
} finally {
|
||||
setIsSendingInvite(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeInviteModal = () => {
|
||||
setIsOpen(false);
|
||||
setToInvite({ email: "", role: ["0"] });
|
||||
@@ -307,13 +301,14 @@ const TeamPanel = () => {
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Button
|
||||
<LoadingButton
|
||||
loading={isSendingInvite}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
Invite a team member
|
||||
</Button>
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
<BasicTable
|
||||
data={tableData}
|
||||
@@ -389,14 +384,19 @@ const TeamPanel = () => {
|
||||
mt={theme.spacing(8)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button variant="text" color="info" onClick={closeInviteModal}>
|
||||
<LoadingButton
|
||||
loading={isSendingInvite}
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={closeInviteModal}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleInviteMember}
|
||||
loading={false}
|
||||
loading={isSendingInvite}
|
||||
disabled={Object.keys(errors).length !== 0}
|
||||
>
|
||||
Send invite
|
||||
|
||||
@@ -28,6 +28,26 @@ export const createPageSpeed = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const getPagespeedMonitorById = createAsyncThunk(
|
||||
"monitors/getMonitorById",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitorId } = data;
|
||||
const res = await networkService.getMonitorByid(authToken, monitorId);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getPageSpeedByTeamId = createAsyncThunk(
|
||||
"pageSpeedMonitors/getPageSpeedByTeamId",
|
||||
async (token, thunkApi) => {
|
||||
@@ -109,6 +129,25 @@ export const deletePageSpeed = createAsyncThunk(
|
||||
}
|
||||
}
|
||||
);
|
||||
export const pausePageSpeed = createAsyncThunk(
|
||||
"pageSpeedMonitors/pausePageSpeed",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitorId } = data;
|
||||
const res = await networkService.pauseMonitorById(authToken, monitorId);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const pageSpeedMonitorSlice = createSlice({
|
||||
name: "pageSpeedMonitor",
|
||||
@@ -124,7 +163,7 @@ const pageSpeedMonitorSlice = createSlice({
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// Monitors by userId
|
||||
// Monitors by teamId
|
||||
// *****************************************************
|
||||
|
||||
.addCase(getPageSpeedByTeamId.pending, (state) => {
|
||||
@@ -143,6 +182,23 @@ const pageSpeedMonitorSlice = createSlice({
|
||||
: "Getting page speed monitors failed";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
.addCase(getPagespeedMonitorById.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getPagespeedMonitorById.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(getPagespeedMonitorById.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to get pagespeed monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
@@ -163,7 +219,7 @@ const pageSpeedMonitorSlice = createSlice({
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// Update Monitor
|
||||
// *****************************************************
|
||||
.addCase(updatePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
@@ -198,6 +254,24 @@ const pageSpeedMonitorSlice = createSlice({
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete page speed monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Pause Monitor
|
||||
// *****************************************************
|
||||
.addCase(pausePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(pausePageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(pausePageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to pause page speed monitor";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -236,7 +236,7 @@ const uptimeMonitorsSlice = createSlice({
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to pause uptime monitor";
|
||||
: "Failed to get uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// update Monitor
|
||||
|
||||
@@ -275,14 +275,15 @@ const Configure = () => {
|
||||
</>
|
||||
)}
|
||||
</LoadingButton>
|
||||
<Button
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="error"
|
||||
sx={{ px: theme.spacing(8) }}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<ConfigBox>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, Modal, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Modal, Stack, Typography } from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import {
|
||||
deletePageSpeed,
|
||||
getPagespeedMonitorById,
|
||||
getPageSpeedByTeamId,
|
||||
updatePageSpeed,
|
||||
pausePageSpeed,
|
||||
} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
@@ -17,63 +19,18 @@ import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)} maxWidth="1000px">
|
||||
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={24}
|
||||
sx={{ mb: theme.spacing(4) }}
|
||||
/>
|
||||
<Skeleton variant="rounded" width="50%" height={18} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(6)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Skeleton variant="rounded" width={100} height={34} />
|
||||
<Skeleton variant="rounded" width={100} height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={500} />
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PageSpeedConfigure = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const { monitors } = useSelector((state) => state.pageSpeedMonitors);
|
||||
const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
|
||||
const { monitorId } = useParams();
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
@@ -89,15 +46,25 @@ const PageSpeedConfigure = () => {
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const data = monitors.find((monitor) => monitor._id === monitorId);
|
||||
if (!data) {
|
||||
logger.error("Error fetching pagespeed monitor of id: " + monitorId);
|
||||
navigate("/not-found", { replace: true });
|
||||
}
|
||||
setMonitor({
|
||||
...data,
|
||||
});
|
||||
}, [monitorId, monitors, navigate]);
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
const action = await dispatch(
|
||||
getPagespeedMonitorById({ authToken, monitorId })
|
||||
);
|
||||
|
||||
if (getPagespeedMonitorById.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
} else if (getPagespeedMonitorById.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error fetching monitor of id: " + monitorId);
|
||||
navigate("/not-found", { replace: true });
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [dispatch, authToken, monitorId, navigate]);
|
||||
|
||||
const handleChange = (event, id) => {
|
||||
let { value } = event.target;
|
||||
@@ -119,6 +86,21 @@ const PageSpeedConfigure = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
const action = await dispatch(pausePageSpeed({ authToken, monitorId }));
|
||||
if (pausePageSpeed.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
} else if (pausePageSpeed.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error pausing monitor: " + monitorId);
|
||||
createToast({ body: "Failed to pause monitor" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(
|
||||
@@ -143,11 +125,9 @@ const PageSpeedConfigure = () => {
|
||||
}
|
||||
};
|
||||
|
||||
let loading = Object.keys(monitor).length === 0;
|
||||
|
||||
return (
|
||||
<Stack className="configure-pagespeed" gap={theme.spacing(12)}>
|
||||
{loading ? (
|
||||
{Object.keys(monitor).length === 0 ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
@@ -195,7 +175,9 @@ const PageSpeedConfigure = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box alignSelf="flex-end" ml="auto">
|
||||
<Button
|
||||
<LoadingButton
|
||||
onClick={handlePause}
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
@@ -210,10 +192,20 @@ const PageSpeedConfigure = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PauseCircleOutlineIcon />
|
||||
Pause
|
||||
</Button>
|
||||
<Button
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseCircleOutlineIcon />
|
||||
Pause
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayCircleOutlineRoundedIcon />
|
||||
Resume
|
||||
</>
|
||||
)}
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsOpen(true)}
|
||||
@@ -222,7 +214,7 @@ const PageSpeedConfigure = () => {
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack
|
||||
@@ -322,7 +314,8 @@ const PageSpeedConfigure = () => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end" mt="auto">
|
||||
<Button
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
@@ -330,7 +323,7 @@ const PageSpeedConfigure = () => {
|
||||
sx={{ px: theme.spacing(12) }}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
|
||||
51
Client/src/Pages/PageSpeed/Configure/skeleton.jsx
Normal file
51
Client/src/Pages/PageSpeed/Configure/skeleton.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Box, Skeleton, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)} maxWidth="1000px">
|
||||
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={24}
|
||||
sx={{ mb: theme.spacing(4) }}
|
||||
/>
|
||||
<Skeleton variant="rounded" width="50%" height={18} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(6)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Skeleton variant="rounded" width={100} height={34} />
|
||||
<Skeleton variant="rounded" width={100} height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={500} />
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
@@ -12,11 +14,11 @@ import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMon
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import "./index.css";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
const CreatePageSpeed = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
|
||||
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
@@ -226,7 +228,8 @@ const CreatePageSpeed = () => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end" mt="auto">
|
||||
<Button
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
@@ -234,7 +237,7 @@ const CreatePageSpeed = () => {
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(12) }}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -16,6 +16,9 @@ const Settings = ({ isAdmin }) => {
|
||||
const { isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// TODO Handle saving
|
||||
|
||||
const handleClearStats = async () => {
|
||||
try {
|
||||
const action = await dispatch(
|
||||
@@ -156,13 +159,14 @@ const Settings = ({ isAdmin }) => {
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button
|
||||
<LoadingButton
|
||||
loading={false}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user