loading indicators
This commit is contained in:
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);
|
||||
|
||||
@@ -635,7 +635,8 @@
|
||||
"statusPages": "Status pages",
|
||||
"support": "Support",
|
||||
"team": "Team",
|
||||
"uptime": "Uptime"
|
||||
"uptime": "Uptime",
|
||||
"checks": "Checks"
|
||||
},
|
||||
"message": "Message",
|
||||
"monitor": "monitor",
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user