From fe03451393235068ba97eab9e358b61823cba5dd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 11 Dec 2025 18:41:38 +0000 Subject: [PATCH] status page pagination --- .../src/pages/status-page/StatusPageTable.tsx | 128 ++++++++++++++++++ client/src/pages/status-page/StatusPages.tsx | 93 ++----------- client/src/pages/uptime/UptimeMonitors.tsx | 2 +- ...nitorTable.tsx => UptimeMonitorsTable.tsx} | 0 .../src/controllers/StatusPageController.ts | 9 +- server/src/routes/statusPages.ts | 5 +- .../services/business/StatusPageService.ts | 16 ++- server/src/validation/index.ts | 54 ++++++++ 8 files changed, 222 insertions(+), 85 deletions(-) rename client/src/pages/uptime/{MonitorTable.tsx => UptimeMonitorsTable.tsx} (100%) diff --git a/client/src/pages/status-page/StatusPageTable.tsx b/client/src/pages/status-page/StatusPageTable.tsx index e69de29bb..4104d11e6 100644 --- a/client/src/pages/status-page/StatusPageTable.tsx +++ b/client/src/pages/status-page/StatusPageTable.tsx @@ -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 | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + 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: Remove, + action: async () => { + setSelectedStatusPage(statusPage); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: "Name", + render: (row) => { + return {row?.name}; + }, + }, + { + id: "published", + content: "Published", + render: (row) => { + const published = row.isPublished ? "Yes" : "No"; + return {published}; + }, + }, + { + id: "url", + content: "Public URL", + render: (row) => { + return ( + {row?.url} + ); + }, + }, + { + id: "actions", + content: t("actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(); + return ( + + { + navigate(`/status-pages/${row._id}`); + }} + /> + + + ); +}; diff --git a/client/src/pages/status-page/StatusPages.tsx b/client/src/pages/status-page/StatusPages.tsx index a035ff0ff..da19a3c85 100644 --- a/client/src/pages/status-page/StatusPages.tsx +++ b/client/src/pages/status-page/StatusPages.tsx @@ -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(null); const open = Boolean(selectedStatusPage); const { response, isValidating, error, refetch } = useGet< - ApiResponse - >("/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(); - const getActions = (statusPage: IStatusPage): ActionMenuItem[] => { - return [ - { - id: 1, - label: "Configure", - action: () => { - navigate(`/status-pages/${statusPage._id}/configure`); - }, - closeMenu: true, - }, - { - id: 7, - label: Remove, - action: async () => { - setSelectedStatusPage(statusPage); - }, - closeMenu: true, - }, - ]; - }; - - const getHeaders = () => { - const headers: Header[] = [ - { - id: "name", - content: "Name", - render: (row) => { - return {row?.name}; - }, - }, - { - id: "published", - content: "Published", - render: (row) => { - const published = row.isPublished ? "Yes" : "No"; - return {published}; - }, - }, - { - id: "url", - content: "Public URL", - render: (row) => { - return ( - {row?.url} - ); - }, - }, - { - id: "actions", - content: t("actions"), - render: (row) => { - return ; - }, - }, - ]; - 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} /> -
{ - navigate(`/status-pages/${row._id}`); - }} + Promise; - getAll: (teamId: string) => Promise; + getAll: ( + teamId: string, + page: number, + rowsPerPage: number + ) => Promise<{ statusPages: IStatusPage[]; count: number }>; get: (teamId: string, id: string) => Promise; getPublic: (url: string) => Promise; 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 ( diff --git a/server/src/validation/index.ts b/server/src/validation/index.ts index f93ac0d70..9e6ef91b1 100644 --- a/server/src/validation/index.ts +++ b/server/src/validation/index.ts @@ -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; + }), +});