This commit is contained in:
Alex Holliday committed 2025-12-22 16:28:14 +00:00
1 parent 6323f70bd2
commit b200157f28
6 files changed
+214 -222

No files matched your search

@@ -0,0 +1,116 @@
import Stack from "@mui/material/Stack";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { StatusLabel, ValueLabel } from "@/components/design-elements";
import { BaseBox } from "@/components/design-elements";
import { useAppSelector } from "@/hooks/AppHooks";
import type { ICheck } from "@/types/check";
import type { MonitorStatus } from "@/types/monitor";
import { formatDateWithTz } from "@/utils/TimeUtils";
type CheckDetailsCardProps = {
check: ICheck;
};
export const CheckDetailsCard = ({ check }: CheckDetailsCardProps) => {
const uiTimezone = useAppSelector((state: any) => state.ui.timezone);
const phases = check?.timings?.phases;
const phaseKeys = [
"wait",
"dns",
"tcp",
"tls",
"request",
"firstByte",
"download",
] as const;
const totalMs = phases
? phaseKeys.reduce(
(sum, key) => sum + Math.max(0, Number((phases as any)[key] ?? 0)),
0
)
: Number(check.responseTime ?? 0);
return (
<BaseBox p={4} flex={1} height={"100%"}>
<Stack spacing={2} sx={{ width: "100%" }}>
<Typography>Check details</Typography>
<Typography variant="body2">
{`Total request time: ${check.timings.phases.total} ms`}
</Typography>
<Grid container spacing={2}>
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="body1" color="text.secondary">
Status
</Typography>
</Grid>
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
<Box>
<StatusLabel status={check.status as MonitorStatus} />
</Box>
</Grid>
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="body1" color="text.secondary">
HTTP Code
</Typography>
</Grid>
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
<Box>
<ValueLabel
value={
check.httpStatusCode < 300
? "positive"
: check.httpStatusCode < 400
? "neutral"
: "negative"
}
text={String(check.httpStatusCode ?? "-")}
/>
</Box>
</Grid>
<Grid size={6}>
<Typography variant="body1" color="text.secondary">
Type
</Typography>
</Grid>
<Grid size={6}>
<Typography variant="body2">
{check.metadata?.type || "-"}
</Typography>
</Grid>
<Grid size={6}>
<Typography variant="body2">{Math.round(totalMs)} ms</Typography>
</Grid>
<Grid size={6}>
<Typography variant="body1" color="text.secondary">
Checked At
</Typography>
</Grid>
<Grid size={6}>
<Typography variant="body2">
{formatDateWithTz(
check.createdAt,
"ddd, MMM D, YYYY, HH:mm A",
uiTimezone
)}
</Typography>
</Grid>
<Grid size={6}>
<Typography variant="body1" color="text.secondary">
Message
</Typography>
</Grid>
<Grid size={6}>
<Typography variant="body2">{check.message || "-"}</Typography>
</Grid>
</Grid>
</Stack>
</BaseBox>
);
};
@@ -0,0 +1,88 @@
import type { ICheck, CheckTimingPhases } from "@/types/check";
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from "recharts";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { alpha, useTheme } from "@mui/material/styles";
import { BaseBox } from "@/components/design-elements";
type TimingsChartProps = { check: ICheck | null };
export type TimingSegment = {
key: keyof CheckTimingPhases;
label: string;
value: number;
percent: number;
};
const PHASE_ORDER: (keyof CheckTimingPhases)[] = [
"wait",
"dns",
"tcp",
"tls",
"request",
"firstByte",
"download",
];
const buildTimingSegments = (phases?: CheckTimingPhases): TimingSegment[] => {
if (!phases) return [];
const raw = PHASE_ORDER.map((key) => ({
key,
value: Math.max(0, Number((phases as any)[key] ?? 0)),
}));
const total = raw.reduce((sum, p) => sum + p.value, 0);
if (total <= 0) {
return raw.map(({ key, value }) => ({
key,
label: key,
value,
percent: 0,
}));
}
return raw.map(({ key, value }) => ({
key,
label: key,
value,
percent: (value / total) * 100,
}));
};
export const TimingsChart = ({ check }: TimingsChartProps) => {
const segments = buildTimingSegments(check?.timings?.phases);
if (!segments.length) return null;
if (!check) return null;
const theme = useTheme();
const strokeColor = alpha(theme.palette.success.main, 0.8);
return (
<BaseBox p={4} flex={1} height={"100%"}>
<Stack spacing={2} sx={{ width: "100%", height: 260 }}>
<Typography>Request timings</Typography>
<Typography variant="body2">
{`Total request time: ${check.timings.phases.total} ms`}
</Typography>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={segments}
dataKey="value"
nameKey="label"
fill="transparent"
stroke={strokeColor}
strokeWidth={1}
label={({ name, value }: any) => `${name}: ${Math.round(Number(value) || 0)} ms`}
>
{segments.map((entry) => (
<Cell key={`cell-${entry.key}`} fill="transparent" />
))}
</Pie>
<Tooltip
formatter={(value: any) => `${Math.round(value as number)} ms`}
/>
</PieChart>
</ResponsiveContainer>
</Stack>
</BaseBox>
);
};
@@ -1,111 +0,0 @@
import Stack from "@mui/material/Stack";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { StatusLabel, ValueLabel } from "@/components/design-elements";
import type { ICheck } from "@/types/check";
import type { MonitorStatus } from "@/types/monitor";
import { useAppSelector } from "@/hooks/AppHooks";
import { formatDateWithTz } from "@/utils/TimeUtils";
type CheckDetailsCardProps = {
check: ICheck;
};
export const CheckDetailsCard = ({ check }: CheckDetailsCardProps) => {
const uiTimezone = useAppSelector((state: any) => state.ui.timezone);
const phases = check?.timings?.phases;
const phaseKeys = [
"wait",
"dns",
"tcp",
"tls",
"request",
"firstByte",
"download",
] as const;
const totalMs = phases
? phaseKeys.reduce(
(sum, key) => sum + Math.max(0, Number((phases as any)[key] ?? 0)),
0
)
: Number(check.responseTime ?? 0);
return (
<Stack spacing={2} sx={{ width: "100%" }}>
<Typography>Check details</Typography>
<Grid container spacing={2}>
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="body1" color="text.secondary">
Status
</Typography>
</Grid>
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
<Box>
<StatusLabel status={check.status as MonitorStatus} />
</Box>
</Grid>
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="body1" color="text.secondary">
HTTP Code
</Typography>
</Grid>
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
<Box>
<ValueLabel
value={
check.httpStatusCode < 300
? "positive"
: check.httpStatusCode < 400
? "neutral"
: "negative"
}
text={String(check.httpStatusCode ?? "-")}
/>
</Box>
</Grid>
<Grid size={6}>
<Typography variant="body1" color="text.secondary">
Type
</Typography>
</Grid>
<Grid size={6}>
<Typography variant="body2">{check.metadata?.type || "-"}</Typography>
</Grid>
<Grid size={6}>
<Typography variant="body1" color="text.secondary">
Total request time
</Typography>
</Grid>
<Grid size={6}>
<Typography variant="body2">{Math.round(totalMs)} ms</Typography>
</Grid>
<Grid size={6}>
<Typography variant="body1" color="text.secondary">
Checked At
</Typography>
</Grid>
<Grid size={6}>
<Typography variant="body2">
{formatDateWithTz(
check.createdAt,
"ddd, MMM D, YYYY, HH:mm A",
uiTimezone
)}
</Typography>
</Grid>
<Grid size={6}>
<Typography variant="body1" color="text.secondary">
Message
</Typography>
</Grid>
<Grid size={6}>
<Typography variant="body2">{check.message || "-"}</Typography>
</Grid>
</Grid>
</Stack>
);
};
@@ -1,97 +0,0 @@
import type { ICheck, CheckTimingPhases } from "@/types/check";
import {
PieChart,
Pie,
Cell,
ResponsiveContainer,
Tooltip,
Legend,
} from "recharts";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { alpha } from "@mui/material/styles";
type TimingsChartProps = { check: ICheck | null };
export type TimingSegment = {
key: keyof CheckTimingPhases;
label: string;
value: number; // milliseconds
percent: number; // 0..100
};
const PHASE_ORDER: (keyof CheckTimingPhases)[] = [
"wait",
"dns",
"tcp",
"tls",
"request",
"firstByte",
"download",
];
const buildTimingSegments = (phases?: CheckTimingPhases): TimingSegment[] => {
if (!phases) return [];
const raw = PHASE_ORDER.map((key) => ({
key,
value: Math.max(0, Number((phases as any)[key] ?? 0)),
}));
const total = raw.reduce((sum, p) => sum + p.value, 0);
if (total <= 0) {
return raw.map(({ key, value }) => ({
key,
label: key,
value,
percent: 0,
}));
}
return raw.map(({ key, value }) => ({
key,
label: key,
value,
percent: (value / total) * 100,
}));
};
export const TimingsChart = ({ check }: TimingsChartProps) => {
console.log(check);
const segments = buildTimingSegments(check?.timings?.phases);
if (!segments.length) return null;
if (!check) return null;
const colors = [
alpha("#1DE9B6", 0.8),
alpha("#7C4DFF", 0.8),
alpha("#FFB200", 0.8),
alpha("#00AFFE", 0.8),
alpha("#FF4181", 0.8),
alpha("#59A14F", 0.8),
alpha("#E15759", 0.8),
];
return (
<Stack spacing={2} sx={{ width: "100%", height: 260 }}>
<Typography>Request timings</Typography>
<Typography variant="body2">
{" "}
{`Total request time: ${check.timings.phases.total} ms`}
</Typography>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie data={segments} dataKey="value" nameKey="label" stroke="none">
{segments.map((entry, index) => (
<Cell
key={`cell-${entry.key}`}
fill={colors[index % colors.length]}
/>
))}
</Pie>
<Tooltip
formatter={(value: any) => `${Math.round(value as number)} ms`}
/>
<Legend />
</PieChart>
</ResponsiveContainer>
</Stack>
);
};
+2 -2
View File
@@ -1,2 +1,2 @@
export { TimingsChart } from "./TimingsChart";
export { CheckDetailsCard } from "./CheckDetailsCard";
export { TimingsChart } from "./ChartTiming";
export { CheckDetailsCard } from "./CardChecktDetails";
+8 -12
View File
@@ -1,17 +1,13 @@
import { BasePage, BaseBox } from "@/components/design-elements";
import { BasePage } from "@/components/design-elements";
import { TimingsChart, CheckDetailsCard } from "@/components/checks/";
import Stack from "@mui/material/Stack";
import Grid from "@mui/material/Grid";
import { useParams } from "react-router";
import { useGet } from "@/hooks/UseApi";
import type { ICheck } from "@/types/check";
import useMediaQuery from "@mui/material/useMediaQuery";
import type { ApiResponse } from "@/types/api";
import { useTheme } from "@mui/material/styles";
const CheckDetails = () => {
const { id } = useParams();
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const { response, loading, error } = useGet<ApiResponse<ICheck>>(
`/checks/${id}`
@@ -20,14 +16,14 @@ const CheckDetails = () => {
const check = response?.data || null;
return (
<BasePage loading={loading} error={error}>
<Stack direction={isSmall ? "column" : "row"} spacing={4}>
<BaseBox p={4} flex={1}>
<Grid container spacing={4}>
<Grid size={{ xs: 12, md: 6 }}>
<CheckDetailsCard check={check!} />
</BaseBox>
<BaseBox p={4} flex={1}>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<TimingsChart check={check} />
</BaseBox>
</Stack>
</Grid>
</Grid>
</BasePage>
);
};