status page pagination

This commit is contained in:
Alex Holliday committed 2025-12-11 18:41:38 +00:00
1 parent b6a3d1e34d
commit fe03451393
8 files changed
+222 -85

No files matched your search

@@ -0,0 +1,128 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { Table, Pagination } from "@/components/design-elements";
import { ActionsMenu } from "@/components/actions-menu";
import type { Header } from "@/components/design-elements/Table";
import { useNavigate } from "react-router";
import type { IStatusPage } from "@/types/status-page";
import type { ActionMenuItem } from "@/components/actions-menu";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
export const StatusPageTable = ({
statusPages,
setSelectedStatusPage,
page,
setPage,
rowsPerPage,
setRowsPerPage,
count,
}: {
statusPages: IStatusPage[];
setSelectedStatusPage: (statusPage: IStatusPage) => void;
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
count: number;
}) => {
const theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
const handlePageChange = (
_e: React.MouseEvent<HTMLButtonElement> | null,
newPage: number
) => {
setPage(newPage);
};
const handleRowsPerPageChange = (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => {
const value = Number(e.target.value);
setPage(0);
setRowsPerPage(value);
};
const getActions = (statusPage: IStatusPage): ActionMenuItem[] => {
return [
{
id: 1,
label: "Configure",
action: () => {
navigate(`/status-pages/${statusPage._id}/configure`);
},
closeMenu: true,
},
{
id: 7,
label: <Typography color={theme.palette.error.main}>Remove</Typography>,
action: async () => {
setSelectedStatusPage(statusPage);
},
closeMenu: true,
},
];
};
const getHeaders = () => {
const headers: Header<IStatusPage>[] = [
{
id: "name",
content: "Name",
render: (row) => {
return <Typography>{row?.name}</Typography>;
},
},
{
id: "published",
content: "Published",
render: (row) => {
const published = row.isPublished ? "Yes" : "No";
return <Typography>{published}</Typography>;
},
},
{
id: "url",
content: "Public URL",
render: (row) => {
return (
<Typography textTransform={"capitalize"}>{row?.url}</Typography>
);
},
},
{
id: "actions",
content: t("actions"),
render: (row) => {
return <ActionsMenu items={getActions(row)} />;
},
},
];
return headers;
};
const headers = getHeaders();
return (
<Box>
<Table
headers={headers}
data={statusPages}
onRowClick={(row) => {
navigate(`/status-pages/${row._id}`);
}}
/>
<Pagination
component="div"
count={count}
page={page}
rowsPerPage={rowsPerPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handleRowsPerPageChange}
/>
</Box>
);
};
+15 -78
View File
@@ -1,96 +1,31 @@
import { HeaderCreate } from "@/components/common";
import Typography from "@mui/material/Typography";
import { BasePageWithStates, InfoBox } from "@/components/design-elements";
import { Table } from "@/components/design-elements";
import { ActionsMenu } from "@/components/actions-menu";
import { Dialog } from "@/components/inputs";
import { StatusPageTable } from "@/pages/status-page/StatusPageTable";
import { useState } from "react";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router";
import type { ActionMenuItem } from "@/components/actions-menu";
import type { Header } from "@/components/design-elements/Table";
import type { IStatusPage } from "@/types/status-page";
import { useGet, useDelete } from "@/hooks/UseApi";
import type { ApiResponse } from "@/hooks/UseApi";
import { useTranslation } from "react-i18next";
const StatusPages = () => {
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const { t } = useTranslation();
const navigate = useNavigate();
const theme = useTheme();
const [selectedStatusPage, setSelectedStatusPage] =
useState<IStatusPage | null>(null);
const open = Boolean(selectedStatusPage);
const { response, isValidating, error, refetch } = useGet<
ApiResponse<IStatusPage[]>
>("/status-pages", {}, {});
ApiResponse<{ statusPages: IStatusPage[]; count: number }>
>(`/status-pages?page=${page}&rowsPerPage=${rowsPerPage}`, {}, {});
const statusPages = response?.data || [];
const statusPages = response?.data?.statusPages || [];
const count = response?.data?.count || 0;
const { deleteFn, loading: isDeleting } = useDelete<any>();
const getActions = (statusPage: IStatusPage): ActionMenuItem[] => {
return [
{
id: 1,
label: "Configure",
action: () => {
navigate(`/status-pages/${statusPage._id}/configure`);
},
closeMenu: true,
},
{
id: 7,
label: <Typography color={theme.palette.error.main}>Remove</Typography>,
action: async () => {
setSelectedStatusPage(statusPage);
},
closeMenu: true,
},
];
};
const getHeaders = () => {
const headers: Header<IStatusPage>[] = [
{
id: "name",
content: "Name",
render: (row) => {
return <Typography>{row?.name}</Typography>;
},
},
{
id: "published",
content: "Published",
render: (row) => {
const published = row.isPublished ? "Yes" : "No";
return <Typography>{published}</Typography>;
},
},
{
id: "url",
content: "Public URL",
render: (row) => {
return (
<Typography textTransform={"capitalize"}>{row?.url}</Typography>
);
},
},
{
id: "actions",
content: t("actions"),
render: (row) => {
return <ActionsMenu items={getActions(row)} />;
},
},
];
return headers;
};
const headers = getHeaders();
const handleConfirm = async () => {
if (!selectedStatusPage) return;
const res = await deleteFn(`/status-pages/${selectedStatusPage._id}`);
@@ -127,12 +62,14 @@ const StatusPages = () => {
entitlement="statusPagesMax"
entitlementCount={statusPages.length}
/>
<Table
headers={headers}
data={statusPages}
onRowClick={(row) => {
navigate(`/status-pages/${row._id}`);
}}
<StatusPageTable
statusPages={statusPages}
setSelectedStatusPage={setSelectedStatusPage}
page={page}
setPage={setPage}
rowsPerPage={rowsPerPage}
setRowsPerPage={setRowsPerPage}
count={count}
/>
<Dialog
+1 -1
View File
@@ -9,7 +9,7 @@ import { HeaderFilter } from "@/components/monitors";
import { HeaderCreate } from "@/components/common";
import Stack from "@mui/material/Stack";
import { Dialog } from "@/components/inputs";
import { MonitorTable } from "@/pages/uptime/MonitorTable";
import { MonitorTable } from "@/pages/uptime/UptimeMonitorsTable";
import { InitializingStatusBox } from "@/components/design-elements/StatusBox";
import { useTranslation } from "react-i18next";
@@ -35,7 +35,14 @@ class StatusPageController {
throw new ApiError("No team ID", 400);
}
const statusPages = await this.statusPageService.getAll(teamId);
const page = req.validatedQuery.page || 0;
const rowsPerPage = req.validatedQuery.rowsPerPage || 10;
const statusPages = await this.statusPageService.getAll(
teamId,
page,
rowsPerPage
);
res.status(200).json({
message: "OK",
data: statusPages,
+3 -2
View File
@@ -5,8 +5,8 @@ import { verifyToken } from "@/middleware/VerifyToken.js";
import { verifyTeamPermission } from "@/middleware/VerifyPermission.js";
import { addUserContext } from "@/middleware/AddUserContext.js";
import { PERMISSIONS } from "@/types/permissions.js";
import { validateBody } from "@/middleware/validation.js";
import { statusPageSchema } from "@/validation/index.js";
import { validateBody, validateQuery } from "@/middleware/validation.js";
import { statusPageSchema, statusPageQuerySchema } from "@/validation/index.js";
import { enforceMax } from "@/middleware/VerifyEntitlements.js";
class StatusPageRoutes {
@@ -37,6 +37,7 @@ class StatusPageRoutes {
verifyToken,
addUserContext,
verifyTeamPermission([PERMISSIONS.statusPages.read]),
validateQuery(statusPageQuerySchema),
this.controller.getAll
);
@@ -7,6 +7,7 @@ import {
Monitor,
} from "@/db/models/index.js";
import ApiError from "@/utils/ApiError.js";
import { count } from "node:console";
const SERVICE_NAME = "StatusPageService";
@@ -15,7 +16,11 @@ export interface IStatusPageService {
tokenizedUser: IUserContext,
statusPage: IStatusPage
) => Promise<IStatusPage>;
getAll: (teamId: string) => Promise<IStatusPage[]>;
getAll: (
teamId: string,
page: number,
rowsPerPage: number
) => Promise<{ statusPages: IStatusPage[]; count: number }>;
get: (teamId: string, id: string) => Promise<IStatusPage>;
getPublic: (url: string) => Promise<IStatusPage>;
update: (
@@ -81,8 +86,13 @@ class StatusPageService implements IStatusPageService {
return statusPage;
};
getAll = async (teamId: string) => {
return StatusPage.find({ teamId });
getAll = async (teamId: string, page: number, rowsPerPage: number) => {
const count = await StatusPage.countDocuments({ teamId });
const statusPages = await StatusPage.find({ teamId })
.skip(page * rowsPerPage)
.limit(rowsPerPage);
return { statusPages, count };
};
update = async (
+54
View File
@@ -672,3 +672,57 @@ export const updateRetentionPolicySchema = z.object({
.min(1, "Retention days must be at least 1")
.max(365, "Retention days cannot exceed 365 days"),
});
export const statusPageQuerySchema = z.object({
page: z
.string()
.optional()
.transform((val, ctx) => {
const num = Number(val);
if (!val) return;
if (Number.isNaN(num)) {
ctx.addIssue({
code: "custom",
message: "page must be a number",
});
return z.NEVER;
}
if (num < 0) {
ctx.addIssue({
code: "custom",
message: "page must greater than 0",
});
return z.NEVER;
}
return num;
}),
rowsPerPage: z
.string()
.optional()
.transform((val, ctx) => {
if (!val) return;
const num = Number(val);
if (Number.isNaN(num)) {
ctx.addIssue({
code: "custom",
message: "rowsPerPage must be a number",
});
return z.NEVER;
}
if (num < 0) {
ctx.addIssue({
code: "custom",
message: "rowsPerPage must be greater than 0",
});
return z.NEVER;
}
if (num > 100) {
ctx.addIssue({
code: "custom",
message: "rowsPerPage must be less than or equal to 100",
});
return z.NEVER;
}
return num;
}),
});