sorting
This commit is contained in:
1 parent
eea6b12be6
commit
459d78cbcb
11 files changed
+287
-15
No files matched your search
Generated
+17
@@ -46,6 +46,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.22",
|
||||
"globals": "^16.4.0",
|
||||
"i18n-unused": "0.19.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"vite": "^7.1.7"
|
||||
@@ -4522,6 +4523,22 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-ms": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.22",
|
||||
"globals": "^16.4.0",
|
||||
"i18n-unused": "0.19.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"vite": "^7.1.7"
|
||||
|
||||
@@ -27,13 +27,26 @@ const PageSpeedMonitorsPage = () => {
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const [selectedMonitor, setSelectedMonitor] = useState<IMonitor | null>(null);
|
||||
const isDialogOpen = Boolean(selectedMonitor);
|
||||
const [sortField, setSortField] = useState<string>("name");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const { deleteFn } = useDelete();
|
||||
const requestParams = [
|
||||
"embedChecks=true",
|
||||
"type=infrastructure",
|
||||
`sortField=${sortField}`,
|
||||
`sortOrder=${sortOrder}`,
|
||||
`page=${page}`,
|
||||
`rowsPerPage=${rowsPerPage}`,
|
||||
];
|
||||
|
||||
const monitorRequestUrl = `/monitors?${requestParams.join("&")}`;
|
||||
|
||||
const { response, loading, isValidating, error, refetch } = useGet<
|
||||
ApiResponse<IMonitorWithStats>
|
||||
>(
|
||||
`/monitors?embedChecks=true&type=infrastructure&page=${page}&rowsPerPage=${rowsPerPage}`,
|
||||
monitorRequestUrl,
|
||||
{},
|
||||
{
|
||||
refreshInterval: GLOBAL_REFRESH,
|
||||
@@ -82,6 +95,10 @@ const PageSpeedMonitorsPage = () => {
|
||||
monitors={monitors}
|
||||
refetch={refetch}
|
||||
setSelectedMonitor={setSelectedMonitor}
|
||||
sortField={sortField}
|
||||
setSortField={setSortField}
|
||||
sortOrder={sortOrder}
|
||||
setSortOrder={setSortOrder}
|
||||
count={count}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Table, Pagination, StatusLabel } from "@/components/design-elements";
|
||||
import type { Header } from "@/components/design-elements/Table";
|
||||
import { ActionsMenu } from "@/components/actions-menu";
|
||||
import { Gauge } from "@/components/design-elements";
|
||||
import { ArrowUp, ArrowDown } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
@@ -18,6 +19,10 @@ export const InfraMonitorsTable = ({
|
||||
monitors,
|
||||
refetch,
|
||||
setSelectedMonitor,
|
||||
sortField,
|
||||
setSortField,
|
||||
sortOrder,
|
||||
setSortOrder,
|
||||
count,
|
||||
page,
|
||||
setPage,
|
||||
@@ -27,6 +32,10 @@ export const InfraMonitorsTable = ({
|
||||
monitors: IMonitor[];
|
||||
refetch: Function;
|
||||
setSelectedMonitor: Function;
|
||||
sortField: string;
|
||||
setSortField: (field: string) => void;
|
||||
sortOrder: "asc" | "desc";
|
||||
setSortOrder: (order: "asc" | "desc") => void;
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
@@ -58,6 +67,19 @@ export const InfraMonitorsTable = ({
|
||||
setRowsPerPage(value);
|
||||
};
|
||||
|
||||
const handleSort = (e: any, field: string) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (sortField === field) {
|
||||
const newOrder = sortOrder === "asc" ? "desc" : "asc";
|
||||
setSortOrder(newOrder);
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortOrder("desc");
|
||||
}
|
||||
refetch();
|
||||
};
|
||||
|
||||
const getActions = (monitor: IMonitor): ActionMenuItem[] => {
|
||||
return [
|
||||
{
|
||||
@@ -117,17 +139,50 @@ export const InfraMonitorsTable = ({
|
||||
};
|
||||
|
||||
const getHeaders = () => {
|
||||
const renderSortIcon = (isActive: boolean) => (
|
||||
<Box width={16} display="inline-flex" justifyContent="center">
|
||||
{isActive ? (
|
||||
sortOrder === "asc" ? (
|
||||
<ArrowUp size={16} />
|
||||
) : (
|
||||
<ArrowDown size={16} />
|
||||
)
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
const headers: Header<IMonitor>[] = [
|
||||
{
|
||||
id: "name",
|
||||
content: t("host"),
|
||||
content: (
|
||||
<Typography
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={(e) => handleSort(e, "name")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("host")}
|
||||
{renderSortIcon(sortField === "name")}
|
||||
</Typography>
|
||||
),
|
||||
render: (row) => {
|
||||
return row.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("status"),
|
||||
content: (
|
||||
<Typography
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={(e) => handleSort(e, "status")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("status")}
|
||||
{renderSortIcon(sortField === "status")}
|
||||
</Typography>
|
||||
),
|
||||
render: (row) => {
|
||||
return <StatusLabel status={row.status} />;
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Table, Pagination, StatusLabel } from "@/components/design-elements";
|
||||
import { HistogramPageSpeed } from "@/components/monitors";
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
import type { Header } from "@/components/design-elements/Table";
|
||||
import { ActionsMenu } from "@/components/actions-menu";
|
||||
|
||||
@@ -19,6 +20,10 @@ export const PageSpeedMonitorTable = ({
|
||||
monitors,
|
||||
refetch,
|
||||
setSelectedMonitor,
|
||||
sortField,
|
||||
setSortField,
|
||||
sortOrder,
|
||||
setSortOrder,
|
||||
count,
|
||||
page,
|
||||
setPage,
|
||||
@@ -28,6 +33,10 @@ export const PageSpeedMonitorTable = ({
|
||||
monitors: IMonitor[];
|
||||
refetch: Function;
|
||||
setSelectedMonitor: Function;
|
||||
sortField: string;
|
||||
setSortField: (field: string) => void;
|
||||
sortOrder: "asc" | "desc";
|
||||
setSortOrder: (order: "asc" | "desc") => void;
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
@@ -59,6 +68,19 @@ export const PageSpeedMonitorTable = ({
|
||||
setRowsPerPage(value);
|
||||
};
|
||||
|
||||
const handleSort = (e: any, field: string) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (sortField === field) {
|
||||
const newOrder = sortOrder === "asc" ? "desc" : "asc";
|
||||
setSortOrder(newOrder);
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortOrder("desc");
|
||||
}
|
||||
refetch();
|
||||
};
|
||||
|
||||
const getActions = (monitor: IMonitor): ActionMenuItem[] => {
|
||||
return [
|
||||
{
|
||||
@@ -118,17 +140,52 @@ export const PageSpeedMonitorTable = ({
|
||||
};
|
||||
|
||||
const getHeaders = () => {
|
||||
const renderSortIcon = (isActive: boolean) => (
|
||||
<Box width={16} display="inline-flex" justifyContent="center">
|
||||
{isActive ? (
|
||||
sortOrder === "asc" ? (
|
||||
<ArrowUp size={16} />
|
||||
) : (
|
||||
<ArrowDown size={16} />
|
||||
)
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const headers: Header<IMonitor>[] = [
|
||||
{
|
||||
id: "name",
|
||||
content: t("host"),
|
||||
content: (
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
onClick={(e) => handleSort(e, "name")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("host")}
|
||||
{renderSortIcon(sortField === "name")}
|
||||
</Stack>
|
||||
),
|
||||
render: (row) => {
|
||||
return row.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("status"),
|
||||
content: (
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
direction={"row"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
onClick={(e) => handleSort(e, "status")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("status")}
|
||||
{renderSortIcon(sortField === "status")}
|
||||
</Stack>
|
||||
),
|
||||
render: (row) => {
|
||||
return <StatusLabel status={row.status} />;
|
||||
},
|
||||
|
||||
@@ -27,13 +27,26 @@ const PageSpeedMonitorsPage = () => {
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const [selectedMonitor, setSelectedMonitor] = useState<IMonitor | null>(null);
|
||||
const isDialogOpen = Boolean(selectedMonitor);
|
||||
const [sortField, setSortField] = useState<string>("name");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const { deleteFn } = useDelete();
|
||||
const requestParams = [
|
||||
"embedChecks=true",
|
||||
"type=pagespeed",
|
||||
`sortField=${sortField}`,
|
||||
`sortOrder=${sortOrder}`,
|
||||
`page=${page}`,
|
||||
`rowsPerPage=${rowsPerPage}`,
|
||||
];
|
||||
|
||||
const monitorRequestUrl = `/monitors?${requestParams.join("&")}`;
|
||||
|
||||
const { response, loading, isValidating, error, refetch } = useGet<
|
||||
ApiResponse<IMonitorWithStats>
|
||||
>(
|
||||
`/monitors?embedChecks=true&type=pagespeed&page=${page}&rowsPerPage=${rowsPerPage}`,
|
||||
monitorRequestUrl,
|
||||
{},
|
||||
{
|
||||
refreshInterval: GLOBAL_REFRESH,
|
||||
@@ -82,6 +95,10 @@ const PageSpeedMonitorsPage = () => {
|
||||
monitors={monitors}
|
||||
refetch={refetch}
|
||||
setSelectedMonitor={setSelectedMonitor}
|
||||
sortField={sortField}
|
||||
setSortField={setSortField}
|
||||
sortOrder={sortOrder}
|
||||
setSortOrder={setSortOrder}
|
||||
count={count}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Table, Pagination, StatusLabel } from "@/components/design-elements";
|
||||
import { HistogramResponseTime } from "@/components/monitors";
|
||||
import type { Header } from "@/components/design-elements/Table";
|
||||
import { ActionsMenu } from "@/components/actions-menu";
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
@@ -18,6 +19,10 @@ export const MonitorTable = ({
|
||||
monitors,
|
||||
refetch,
|
||||
setSelectedMonitor,
|
||||
sortField,
|
||||
setSortField,
|
||||
sortOrder,
|
||||
setSortOrder,
|
||||
count,
|
||||
page,
|
||||
setPage,
|
||||
@@ -27,6 +32,10 @@ export const MonitorTable = ({
|
||||
monitors: IMonitor[];
|
||||
refetch: Function;
|
||||
setSelectedMonitor: Function;
|
||||
sortField: string;
|
||||
setSortField: (field: string) => void;
|
||||
sortOrder: "asc" | "desc";
|
||||
setSortOrder: (order: "asc" | "desc") => void;
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
@@ -57,6 +66,19 @@ export const MonitorTable = ({
|
||||
setRowsPerPage(value);
|
||||
};
|
||||
|
||||
const handleSort = (e: any, field: string) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (sortField === field) {
|
||||
const newOrder = sortOrder === "asc" ? "desc" : "asc";
|
||||
setSortOrder(newOrder);
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortOrder("desc");
|
||||
}
|
||||
refetch();
|
||||
};
|
||||
|
||||
const getActions = (monitor: IMonitor): ActionMenuItem[] => {
|
||||
return [
|
||||
{
|
||||
@@ -116,17 +138,50 @@ export const MonitorTable = ({
|
||||
};
|
||||
|
||||
const getHeaders = () => {
|
||||
const renderSortIcon = (isActive: boolean) => (
|
||||
<ArrowUp
|
||||
size={16}
|
||||
style={{
|
||||
visibility: isActive ? "visible" : "hidden",
|
||||
transform: sortOrder === "asc" ? "none" : "rotate(180deg)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const headers: Header<IMonitor>[] = [
|
||||
{
|
||||
id: "name",
|
||||
content: t("host"),
|
||||
content: (
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
onClick={(e) => handleSort(e, "name")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("name")}
|
||||
{renderSortIcon(sortField === "name")}
|
||||
</Stack>
|
||||
),
|
||||
|
||||
render: (row) => {
|
||||
return row.name;
|
||||
return row?.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("status"),
|
||||
content: (
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
direction={"row"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
onClick={(e) => handleSort(e, "status")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("status")}
|
||||
{renderSortIcon(sortField === "status")}
|
||||
</Stack>
|
||||
),
|
||||
render: (row) => {
|
||||
return <StatusLabel status={row.status} />;
|
||||
},
|
||||
@@ -144,7 +199,19 @@ export const MonitorTable = ({
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
content: t("type"),
|
||||
content: (
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
direction={"row"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
onClick={(e) => handleSort(e, "type")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("type")}
|
||||
{renderSortIcon(sortField === "type")}
|
||||
</Stack>
|
||||
),
|
||||
render: (row) => {
|
||||
return row.type;
|
||||
},
|
||||
|
||||
@@ -34,6 +34,8 @@ const UptimeMonitors = () => {
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const [selectedMonitor, setSelectedMonitor] = useState<IMonitor | null>(null);
|
||||
const isDialogOpen = Boolean(selectedMonitor);
|
||||
const [sortField, setSortField] = useState<string>("name");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [selectedTypes, setSelectedTypes] = useState<UptimeMonitorType[]>([
|
||||
@@ -66,6 +68,8 @@ const UptimeMonitors = () => {
|
||||
"embedChecks=true",
|
||||
typeQuery,
|
||||
statusQuery,
|
||||
`sortField=${sortField}`,
|
||||
`sortOrder=${sortOrder}`,
|
||||
`page=${page}`,
|
||||
`rowsPerPage=${rowsPerPage}`,
|
||||
].filter(Boolean);
|
||||
@@ -140,6 +144,10 @@ const UptimeMonitors = () => {
|
||||
monitors={monitors}
|
||||
refetch={refetch}
|
||||
setSelectedMonitor={setSelectedMonitor}
|
||||
sortField={sortField}
|
||||
setSortField={setSortField}
|
||||
sortOrder={sortOrder}
|
||||
setSortOrder={setSortOrder}
|
||||
count={count}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
|
||||
@@ -75,6 +75,8 @@ class MonitorController {
|
||||
|
||||
let result;
|
||||
if (req.validatedQuery.embedChecks === true) {
|
||||
const sortField = req.validatedQuery.sortField || "name";
|
||||
const sortOrder = req.validatedQuery.sortOrder || "desc";
|
||||
const page = req.validatedQuery.page || 0;
|
||||
const rowsPerPage = req.validatedQuery.rowsPerPage || 10;
|
||||
const type = req.validatedQuery.type;
|
||||
@@ -82,6 +84,8 @@ class MonitorController {
|
||||
|
||||
result = await this.monitorService.getAllEmbedChecks(
|
||||
teamId,
|
||||
sortField,
|
||||
sortOrder,
|
||||
page,
|
||||
rowsPerPage,
|
||||
type,
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface IMonitorService {
|
||||
getAll: (teamId: string) => Promise<IMonitor[]>;
|
||||
getAllEmbedChecks: (
|
||||
teamId: string,
|
||||
sortField: string,
|
||||
sortOrder: string,
|
||||
page: number,
|
||||
limit: number,
|
||||
type: MonitorType[],
|
||||
@@ -87,6 +89,8 @@ class MonitorService implements IMonitorService {
|
||||
|
||||
getAllEmbedChecks = async (
|
||||
teamId: string,
|
||||
sortField: string,
|
||||
sortOrder: string,
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
type: MonitorType[] = [],
|
||||
@@ -134,7 +138,7 @@ class MonitorService implements IMonitorService {
|
||||
|
||||
const monitors = await Monitor.find(matchConditions)
|
||||
.lean()
|
||||
.sort({ name: 1 })
|
||||
.sort({ [sortField]: sortOrder === "asc" ? 1 : -1 })
|
||||
.skip(skip)
|
||||
.limit(rowsPerPage);
|
||||
|
||||
@@ -157,7 +161,11 @@ class MonitorService implements IMonitorService {
|
||||
latestChecks: { $push: "$$ROOT" },
|
||||
},
|
||||
},
|
||||
{ $project: { latestChecks: { $slice: [{ $ifNull: ["$latestChecks", []] }, 25] } } },
|
||||
{
|
||||
$project: {
|
||||
latestChecks: { $slice: [{ $ifNull: ["$latestChecks", []] }, 25] },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const checksMap = new Map(
|
||||
@@ -336,7 +344,14 @@ class MonitorService implements IMonitorService {
|
||||
temperature: {
|
||||
$map: {
|
||||
input: {
|
||||
$range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$tempsArrays", 0] }, []] } }],
|
||||
$range: [
|
||||
0,
|
||||
{
|
||||
$size: {
|
||||
$ifNull: [{ $arrayElemAt: ["$tempsArrays", 0] }, []],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
as: "idx",
|
||||
in: {
|
||||
@@ -362,7 +377,14 @@ class MonitorService implements IMonitorService {
|
||||
disk: {
|
||||
$map: {
|
||||
input: {
|
||||
$range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$disksArray", 0] }, []] } }],
|
||||
$range: [
|
||||
0,
|
||||
{
|
||||
$size: {
|
||||
$ifNull: [{ $arrayElemAt: ["$disksArray", 0] }, []],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
as: "idx",
|
||||
in: {
|
||||
@@ -406,7 +428,12 @@ class MonitorService implements IMonitorService {
|
||||
net: {
|
||||
$map: {
|
||||
input: {
|
||||
$range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$netsArray", 0] }, []] } }],
|
||||
$range: [
|
||||
0,
|
||||
{
|
||||
$size: { $ifNull: [{ $arrayElemAt: ["$netsArray", 0] }, []] },
|
||||
},
|
||||
],
|
||||
},
|
||||
as: "idx",
|
||||
in: {
|
||||
|
||||
@@ -135,6 +135,8 @@ export const monitorAllEmbedChecksQuerySchema = z.object({
|
||||
if (!val) return [];
|
||||
return Array.isArray(val) ? val : [val];
|
||||
}),
|
||||
sortField: z.string().optional(),
|
||||
sortOrder: z.string().optional(),
|
||||
page: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
Reference in new issue
Block a user