loading indicators

This commit is contained in:
Alex Holliday committed 2025-12-09 21:01:47 +00:00
1 parent 03d681f212
commit bda3a7e91c
10 files changed
+283 -9

No files matched your search

+113
View File
@@ -0,0 +1,113 @@
import { BasePage, InfoBox } from "@/components/design-elements";
import Stack from "@mui/material/Stack";
import MenuItem from "@mui/material/MenuItem";
import { Select } from "@/components/inputs";
import { HeaderRange } from "@/components/common/HeaderRange";
import { ChecksTable } from "@/pages/checks/ChecksTable";
import { LoadingSpinner } from "@/components/design-elements";
import { useTheme } from "@mui/material/styles";
import { useState } from "react";
import type { SelectChangeEvent } from "@mui/material/Select";
import { useGet } from "@/hooks/UseApi";
import type { ApiResponse } from "@/hooks/UseApi";
import type { IMonitor } from "@/types/monitor";
import type { ICheck } from "@/types/check";
const ChecksPage = () => {
const theme = useTheme();
const [selectedMonitorId, setSelectedMonitorId] = useState<string>("all");
const [range, setRange] = useState("2h");
const [status, setStatus] = useState<"up" | "down">("down");
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const {
response: monitorResponse,
loading: monitorsLoading,
isValidating: monitorsValidating,
} = useGet<ApiResponse<IMonitor[]>>(
`/monitors`,
{},
{ keepPreviousData: true }
);
const monitors: IMonitor[] = monitorResponse?.data || [];
const {
response: checksResponse,
loading: checksLoading,
isValidating: checksValidating,
} = useGet<ApiResponse<{ count: number; checks: ICheck[] }>>(
`/checks?page=${page}&rowsPerPage=${rowsPerPage}&range=${range}&status=${status}${
selectedMonitorId !== "all" ? `&monitorId=${selectedMonitorId}` : ""
}`,
{},
{ keepPreviousData: true },
{ useTeamIdAsKey: true }
);
const checks = checksResponse?.data?.checks || [];
const checksCount = checksResponse?.data?.count || 0;
return (
<BasePage>
<InfoBox
title="Checks"
description="Browse recent checks across your monitors and filter by monitor and time range."
/>
<Stack
direction={{ xs: "column", md: "row" }}
spacing={theme.spacing(8)}
justifyContent={"space-between"}
>
<Stack
direction={{ xs: "column", md: "row" }}
spacing={theme.spacing(8)}
>
<Select<string>
onChange={(e: SelectChangeEvent<string>) => {
setSelectedMonitorId(e.target.value);
}}
value={selectedMonitorId}
>
<MenuItem value="all">All monitors</MenuItem>
{monitors.map((monitor) => (
<MenuItem key={monitor._id} value={monitor._id}>
{monitor.name}
</MenuItem>
))}
</Select>
<Select<string>
onChange={(e: SelectChangeEvent<string>) => {
const val = e.target.value as "up" | "down";
setStatus(val);
}}
value={status}
>
<MenuItem value="up">Up</MenuItem>
<MenuItem value="down">Down</MenuItem>
</Select>
<LoadingSpinner
show={monitorsValidating || checksValidating}
sx={{ alignSelf: "center" }}
/>
</Stack>
<HeaderRange
range={range}
setRange={setRange}
loading={monitorsLoading || checksLoading}
/>
</Stack>
<ChecksTable
checks={checks}
count={checksCount}
page={page}
setPage={setPage}
rowsPerPage={rowsPerPage}
setRowsPerPage={setRowsPerPage}
/>
</BasePage>
);
};
export default ChecksPage;
+122
View File
@@ -0,0 +1,122 @@
import {
Table,
Pagination,
ValueLabel,
StatusLabel,
} from "@/components/design-elements";
import Box from "@mui/material/Box";
import type { Header } from "@/components/design-elements/Table";
import { useTranslation } from "react-i18next";
import { formatDateWithTz } from "@/utils/TimeUtils";
import { useAppSelector } from "@/hooks/AppHooks";
import type { ICheck } from "@/types/check";
import type { MonitorStatus } from "@/types/monitor";
type CheckWithMonitor = ICheck & {
metadata?: { monitorId?: { name?: string } };
};
export const ChecksTable = ({
checks,
count,
page,
setPage,
rowsPerPage,
setRowsPerPage,
}: {
checks: CheckWithMonitor[];
count: number;
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
}) => {
const { t } = useTranslation();
const uiTimezone = useAppSelector((state: any) => state.ui.timezone);
const getHeaders = (t: Function, uiTimezone: string) => {
const headers: Header<CheckWithMonitor>[] = [
{
id: "monitorName",
content: "Monitor name",
render: (row) => {
return row.metadata?.monitorId?.name || "N/A";
},
},
{
id: "status",
content: "Status",
render: (row) => {
return <StatusLabel status={row.status as MonitorStatus} />;
},
},
{
id: "date",
content: "Date and time",
render: (row) => {
return formatDateWithTz(
row.createdAt,
"ddd, MMMM D, YYYY, HH:mm A",
uiTimezone
);
},
},
{
id: "statusCode",
content: "Status code",
render: (row) => {
const code = row.httpStatusCode;
const value =
code < 300 ? "positive" : code < 400 ? "neutral" : "negative";
return <ValueLabel value={value} text={String(code)} />;
},
},
{
id: "message",
content: "Message",
render: (row) => {
return row.message || "N/A";
},
},
];
return headers;
};
const headers = getHeaders(t, uiTimezone);
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);
};
return (
<Box>
<Table
headers={headers}
data={checks}
onRowClick={(row) => {
alert("not yet");
}}
/>
<Pagination
component="div"
count={count}
page={page}
rowsPerPage={rowsPerPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handleRowsPerPageChange}
/>
</Box>
);
};
+17 -5
View File
@@ -1,6 +1,7 @@
import Stack from "@mui/material/Stack";
import MenuItem from "@mui/material/MenuItem";
import { Select } from "@/components/inputs";
import { LoadingSpinner } from "@/components/design-elements";
import { BasePage, InfoBox } from "@/components/design-elements";
import { IncidentTable } from "@/pages/incidents/IncidentTable";
import { HeaderRange } from "@/components/common/HeaderRange";
@@ -24,9 +25,12 @@ const IncidentsPage = () => {
const [range, setRange] = useState("all");
const [resolved, setResolved] = useState<boolean | null>(false);
const { response, loading, refetch } = useGet<
ApiResponse<{ count: number; incidents: IIncident[] }>
>(
const {
response,
loading: incidentsLoading,
isValidating: incidentsValidating,
refetch,
} = useGet<ApiResponse<{ count: number; incidents: IIncident[] }>>(
`/incidents?page=${page}&rowsPerPage=${rowsPerPage}&range=${range}${
selectedMonitorId && selectedMonitorId !== "all"
? `&monitorId=${selectedMonitorId}`
@@ -42,7 +46,11 @@ const IncidentsPage = () => {
: [];
const count = response?.data?.count || 0;
const { response: monitorResponse } = useGet<ApiResponse<IMonitor[]>>(
const {
response: monitorResponse,
loading: monitorsLoading,
isValidating: monitorsValidating,
} = useGet<ApiResponse<IMonitor[]>>(
`/monitors`,
{},
{ keepPreviousData: true }
@@ -103,11 +111,15 @@ const IncidentsPage = () => {
<MenuItem value={"resolved"}>Resolved</MenuItem>
<MenuItem value={"unresolved"}>Unresolved</MenuItem>
</Select>
<LoadingSpinner
show={incidentsValidating || monitorsValidating}
sx={{ alignSelf: "center" }}
/>
</Stack>
<HeaderRange
range={range}
setRange={setRange}
loading={loading}
loading={incidentsLoading || monitorsLoading}
all={true}
/>
</Stack>
@@ -4,6 +4,7 @@ import {
DownStatusBox,
PausedStatusBox,
InfoBox,
LoadingSpinner,
} from "@/components/design-elements";
import { HeaderFilter } from "@/components/monitors";
import { HeaderCreate } from "@/components/common";
@@ -137,6 +138,7 @@ const UptimeMonitors = () => {
entitlement="monitorsMax"
entitlementCount={count}
/>
<Stack direction={isSmall ? "column" : "row"} gap={theme.spacing(8)}>
<UpStatusBox n={upCount} />
<DownStatusBox n={downCount} />
@@ -152,6 +154,7 @@ const UptimeMonitors = () => {
onStatusesChange={setSelectedStatuses}
searchString={searchString}
onSearchStringChange={setSearchString}
loading={isValidating}
/>
<MonitorTable
monitors={monitors}