status page pagination
This commit is contained in:
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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
File renamed without changes.
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
Reference in new issue
Block a user