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

@@ -0,0 +1,12 @@
import CircularProgress, { type CircularProgressProps } from "@mui/material/CircularProgress";
type LoadingSpinnerProps = { show?: boolean } & CircularProgressProps;
export const LoadingSpinner = ({
show = true,
size = 18,
thickness = 5,
...props
}: LoadingSpinnerProps) => (show ? (
<CircularProgress size={size} thickness={thickness} {...props} />
) : null);
@@ -30,3 +30,4 @@ export { Breadcrumb } from "./Breadcrumb";
export { InfoBox } from "./InfoBox";
export { Tooltip, TooltipWithInfo } from "./Tooltip";
export { PlanCard } from "./PlanCard";
export { LoadingSpinner } from "./LoadingSpinner";
@@ -5,6 +5,7 @@ import {
Server,
AlertTriangle,
LayoutList,
ListChecks,
Wrench,
Activity,
LifeBuoy,
@@ -45,6 +46,11 @@ export const getMenu = (t: Function, user: IUser) => {
path: "incidents",
icon: <AlertTriangle size={20} strokeWidth={1.5} />,
},
{
name: t("menu.checks"),
path: "checks",
icon: <ListChecks size={20} strokeWidth={1.5} />,
},
{
name: t("menu.statusPages"),
@@ -3,6 +3,7 @@ import Box from "@mui/material/Box";
import MenuItem from "@mui/material/MenuItem";
import Typography from "@mui/material/Typography";
import { Select, Checkbox, TextInput } from "@/components/inputs";
import { LoadingSpinner } from "@/components/design-elements";
import { useEffect, useState } from "react";
import { useTheme } from "@mui/material/styles";
@@ -21,6 +22,7 @@ type HeaderFilterProps = {
onStatusesChange: (statuses: MonitorStatus[]) => void;
searchString: string;
onSearchStringChange?: (value: string) => void;
loading?: boolean;
};
const coerceValue = <T extends string>(value: string | string[]): T[] => {
@@ -37,6 +39,7 @@ export const HeaderFilter = ({
onStatusesChange,
searchString,
onSearchStringChange,
loading = false,
}: HeaderFilterProps) => {
const theme = useTheme();
const [localSearch, setLocalSearch] = useState<string>(searchString);
@@ -65,7 +68,6 @@ export const HeaderFilter = ({
return (
<Stack spacing={theme.spacing(4)} direction={{ xs: "column", sm: "row" }}>
<Select
fieldLabel="Monitor type"
placeholder="Monitor type"
multiple
value={selectedTypes}
@@ -81,7 +83,6 @@ export const HeaderFilter = ({
))}
</Select>
<Select
fieldLabel="Monitor status"
placeholder="Monitor status"
multiple
value={selectedStatuses}
@@ -96,9 +97,10 @@ export const HeaderFilter = ({
</MenuItem>
))}
</Select>
<LoadingSpinner show={loading} sx={{ alignSelf: "center" }} />
<Box flex={1} display={{ xs: "none", md: "inherit" }} />
<TextInput
fieldLabel="Search"
placeholder="Search monitors"
value={localSearch}
onChange={(event) => {
setLocalSearch(event.target.value);
+2 -1
View File
@@ -635,7 +635,8 @@
"statusPages": "Status pages",
"support": "Support",
"team": "Team",
"uptime": "Uptime"
"uptime": "Uptime",
"checks": "Checks"
},
"message": "Message",
"monitor": "monitor",
+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}
+2
View File
@@ -20,6 +20,7 @@ import NotificationChannelsCreatePage from "@/pages/notification-channels/Notifi
import NotificationChannelsConfigPage from "@/pages/notification-channels/NotificationChannelsConfig";
import IncidentsPage from "@/pages/incidents/IncidentsPage";
import IncidentDetailsPage from "@/pages/incidents/IncidentDetails";
import ChecksPage from "@/pages/checks/ChecksPage";
import PageSpeedMonitorsPage from "@/pages/pagespeed/PageSpeedMonitors";
import PageSpeedDetailsPage from "@/pages/pagespeed/PageSpeedDetails";
import PageSpeedConfigurePage from "@/pages/pagespeed/PageSpeedConfig";
@@ -112,6 +113,7 @@ const Router = () => {
/>
<Route path="incidents" element={<IncidentsPage />} />
<Route path="incidents/:id" element={<IncidentDetailsPage />} />
<Route path="checks" element={<ChecksPage />} />
<Route path="pagespeed/create" element={<PageSpeedCreatePage />} />
<Route
path="pagespeed/:id/configure"