stat boxes

This commit is contained in:
Alex Holliday
2025-10-06 15:08:55 -07:00
parent abadc99bf1
commit 8a1fc94c31
6 changed files with 129 additions and 3 deletions

View File

@@ -0,0 +1,61 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import { useMediaQuery } from "@mui/material";
import type { PaletteKey } from "@/Utils/Theme/v2/theme";
type GradientBox = React.PropsWithChildren<{ palette?: PaletteKey }>;
export const GradientBox: React.FC<GradientBox> = ({ children, palette }) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const bg = palette
? `linear-gradient(to bottom right, ${theme.palette[palette].main} 30%, ${theme.palette[palette].lowContrast} 70%)`
: `linear-gradient(340deg, ${theme.palette.tertiary.main} 10%, ${theme.palette.primary.main} 45%)`;
return (
<Box
border={1}
sx={{
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
width: isSmall
? `calc(50% - (1 * ${theme.spacing(8)} / 2))`
: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
borderStyle: "solid",
borderRadius: 4,
borderColor: theme.palette.primary.lowContrast,
background: bg,
}}
>
{children}
</Box>
);
};
type StatBoxProps = React.PropsWithChildren<{
title: string;
subtitle: string;
palette?: PaletteKey;
}>;
export const StatBox: React.FC<StatBoxProps> = ({
title,
subtitle,
palette,
children,
}) => {
const theme = useTheme();
const textColor = palette ? theme.palette[palette].contrastText : "inherit";
return (
<GradientBox palette={palette}>
<Stack>
<Typography color={textColor}>{title}</Typography>
<Typography color={textColor}>{subtitle}</Typography>
{children}
</Stack>
</GradientBox>
);
};

View File

@@ -2,3 +2,4 @@ export { SplitBox as HorizontalSplitBox, ConfigBox } from "./SplitBox";
export { BasePage } from "./BasePage";
export { BGBox, UpStatusBox, DownStatusBox, PausedStatusBox } from "./StatusBox";
export { DataTable as Table } from "./Table";
export { GradientBox, StatBox } from "./StatBox";

View File

@@ -1,17 +1,27 @@
import { BasePage } from "@/Components/v2/DesignElements";
import { HeaderControls } from "@/Components/v2/Monitors/HeaderControls";
import Stack from "@mui/material/Stack";
import { StatBox } from "@/Components/v2/DesignElements";
import { useTheme } from "@mui/material/styles";
import { useParams } from "react-router";
import { useGet, usePatch, type ApiResponse } from "@/Hooks/v2/UseApi";
import { useState } from "react";
import { getStatusPalette } from "@/Utils/MonitorUtils";
import prettyMilliseconds from "pretty-ms";
const UptimeDetailsPage = () => {
const { id } = useParams();
const theme = useTheme();
// Local state
const [range, setRange] = useState("30m");
const { response, loading, error, refetch } = useGet<ApiResponse>(
`/monitors/${id}?range=${range}`
`/monitors/${id}?embedChecks=true&range=${range}`,
{},
{ refreshInterval: 30000 }
);
const {
patch,
@@ -19,11 +29,27 @@ const UptimeDetailsPage = () => {
error: postError,
} = usePatch<ApiResponse>(`/monitors/${id}/active`);
const monitor = response?.data || null;
const monitor = response?.data?.monitor || null;
if (!monitor) {
return null;
}
const stats = response?.data?.stats || null;
const streakDuration = stats?.currentStreakStartedAt
? Date.now() - stats?.currentStreakStartedAt
: 0;
const lastChecked = stats?.lastCheckTimestamp
? Date.now() - stats?.lastCheckTimestamp
: -1;
const checks = response?.data?.checks || null;
console.log(response);
const palette = getStatusPalette(monitor.status);
return (
<BasePage>
<HeaderControls
@@ -32,6 +58,28 @@ const UptimeDetailsPage = () => {
isPatching={isPatching}
refetch={refetch}
/>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<StatBox
palette={palette}
title="Active for"
subtitle={prettyMilliseconds(streakDuration, { secondsDecimalDigits: 0 })}
/>
<StatBox
title="Last check"
subtitle={
lastChecked >= 0
? `${prettyMilliseconds(lastChecked, { secondsDecimalDigits: 0 })} ago`
: "N/A"
}
/>
<StatBox
title="Last response time"
subtitle={stats?.lastResponseTime ? `${stats?.lastResponseTime} ms` : "N/A"}
/>
</Stack>
</BasePage>
);
};

View File

@@ -18,7 +18,7 @@ const UptimeMonitors = () => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const { response, loading } = useGet<ApiResponse>("/monitors?embedChecks=true");
const { response, loading } = useGet<ApiResponse>("/monitors?embedChecks=true", {});
const monitors = response?.data ?? ([] as IMonitor[]);
if (monitors.length === 0 && !loading) {

View File

@@ -1,4 +1,13 @@
import type { MonitorStatus } from "@/Types/Monitor";
import type { PaletteKey } from "./Theme/v2/theme";
export const getStatusPalette = (status: MonitorStatus): PaletteKey => {
const paletteMap: Record<MonitorStatus, PaletteKey> = {
up: "success",
down: "error",
initializing: "warning",
};
return paletteMap[status];
};
export const getStatusColor = (status: MonitorStatus, theme: any): string => {
const statusColors: Record<MonitorStatus, string> = {

View File

@@ -1,5 +1,12 @@
import { createTheme } from "@mui/material";
import { lightPalette, darkPalette, typographyLevels } from "./palette";
import type { Theme } from "@mui/material/styles";
export type PaletteKey = {
[K in keyof Theme["palette"]]: Theme["palette"][K] extends { main: any } ? K : never;
}[keyof Theme["palette"]];
const fontFamilyPrimary = '"Inter" , sans-serif';
const shadow =
"0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03)";