initial commit

This commit is contained in:
Alex Holliday committed 2026-01-25 02:09:33 +00:00
1 parent 794018a6ab
commit d42d5f2325
12 files changed
+288 -3

No files matched your search

@@ -0,0 +1,19 @@
import Stack from "@mui/material/Stack";
import { MonitorStatus } from "@/Components/v2/design-elements";
import type { Monitor } from "@/Types/Monitor.js";
interface HeaderMonitorControlsProps {
monitor: Monitor;
}
export const HeaderMonitorControls = ({ monitor }: HeaderMonitorControlsProps) => {
return (
<Stack
direction="row"
justifyContent={"space-between"}
>
<MonitorStatus monitor={monitor} />
</Stack>
);
};
+1
View File
@@ -2,3 +2,4 @@ export * from "../common/charts/HistogramResponseTime";
export * from "../common/charts/HeatmapResponseTime";
export * from "../common/charts/HeatmapResponseTimeTooltip";
export * from "../common/controls/HeaderCreate";
export * from "../common/controls/HeaderMonitorControls";
@@ -55,7 +55,7 @@ export const BasePage = ({
spacing={theme.spacing(10)}
{...props}
>
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
<Breadcrumb />
{children}
</Stack>
);
@@ -0,0 +1,23 @@
import type { CSSProperties } from "react";
interface DotProps {
color?: string;
size?: string;
style?: CSSProperties;
}
export const Dot = ({ color = "gray", size = "4px", style }: DotProps) => {
return (
<span
style={{
content: '""',
width: size,
height: size,
borderRadius: "50%",
backgroundColor: color,
opacity: 0.8,
...style,
}}
/>
);
};
@@ -0,0 +1,59 @@
import type { Monitor } from "@/Types/Monitor";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { PulseDot, Dot } from "@/Components/v2/design-elements";
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles";
import prettyMilliseconds from "pretty-ms";
import { typographyLevels } from "@/Utils/Theme/v2Palette";
import useMediaQuery from "@mui/material/useMediaQuery";
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
if (!monitor) {
return null;
}
return (
<Stack>
<Typography
fontSize={typographyLevels.xl}
fontWeight={500}
color={theme.palette.primary.contrastText}
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
>
{monitor.name}
</Typography>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(4)}
>
<PulseDot color={getStatusColor(monitor.status, theme)} />
<Typography
color={theme.palette.text.secondary}
fontSize={typographyLevels.l}
fontWeight={"bolder"}
fontFamily={"monospace"}
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
>
{formatUrl(monitor?.url)}
</Typography>
{!isSmall && (
<>
<Dot />
<Typography>
Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })}
</Typography>
</>
)}
</Stack>
</Stack>
);
};
@@ -0,0 +1,48 @@
import { Box, Stack, useTheme } from "@mui/material";
interface PulseDotProps {
color: string;
}
export const PulseDot = ({ color }: PulseDotProps) => {
const theme = useTheme();
return (
<Stack
width="26px"
height="24px"
alignItems="center"
justifyContent="center"
>
<Box
minWidth="18px"
minHeight="18px"
sx={{
position: "relative",
backgroundColor: color,
borderRadius: "50%",
"&::before": {
content: `""`,
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "inherit",
borderRadius: "50%",
animation: "ripple 1.8s ease-out infinite",
},
"&::after": {
content: `""`,
position: "absolute",
width: "7px",
height: "7px",
borderRadius: "50%",
backgroundColor: theme.palette.background.paper,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
},
}}
/>
</Stack>
);
};
@@ -8,3 +8,6 @@ export * from "./BaseBox";
export * from "./Table";
export { DataTable as Table } from "./Table";
export * from "./StatusLabel";
export * from "./PulseDot";
export * from "./Dot";
export * from "./MonitorStatus";
+96
View File
@@ -0,0 +1,96 @@
import { useState, useMemo } from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { useGet } from "@/Hooks/UseApi";
import type { MonitorDetailsResponse } from "@/Types/Monitor";
import type { ChecksResponse } from "@/Types/Check";
import type { RootState } from "@/Types/state";
import useCertificateFetch from "./Hooks/useCertificateFetch";
import { BasePage } from "@/Components/v2/design-elements";
import { HeaderMonitorControls } from "@/Components/v2/common";
const certificateDateFormat = "MMM D, YYYY h A";
const UptimeDetailsPage = () => {
const { monitorId } = useParams<{ monitorId: string }>();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(5);
const [dateRange, setDateRange] = useState<string>("recent");
const monitorDetailsUrl = useMemo(() => {
if (!monitorId) {
return null;
}
const params = new URLSearchParams();
params.append("dateRange", dateRange);
params.append("normalize", "true");
return `/monitors/uptime/details/${monitorId}?${params.toString()}`;
}, [monitorId, dateRange]);
const {
data: monitorDetailsData,
isLoading: monitorIsLoading,
error: monitorError,
refetch: refetchMonitor,
} = useGet<MonitorDetailsResponse>(monitorDetailsUrl);
const monitorData = monitorDetailsData?.monitorData;
const monitor = monitorData?.monitor;
const monitorStats = monitorDetailsData?.monitorStats ?? null;
const [certificateExpiry] = useCertificateFetch({
monitor,
monitorId,
certificateDateFormat,
uiTimezone,
});
const checksUrl = useMemo(() => {
if (!monitorId || !monitor?.type) {
return null;
}
const params = new URLSearchParams();
params.append("type", monitor.type);
params.append("sortOrder", "desc");
params.append("dateRange", dateRange);
params.append("page", String(page));
params.append("rowsPerPage", String(rowsPerPage));
return `/checks/${monitorId}?${params.toString()}`;
}, [monitorId, monitor?.type, dateRange, page, rowsPerPage]);
const {
data: checksData,
isLoading: checksIsLoading,
error: checksError,
} = useGet<ChecksResponse>(checksUrl);
const checks = checksData?.checks ?? [];
const checksCount = checksData?.checksCount ?? 0;
// TODO: Add UI components here
// Placeholder to consume all data fetching results
console.log({
monitorIsLoading,
monitorError,
refetchMonitor,
monitorStats,
certificateExpiry,
checksIsLoading,
checksError,
checks,
checksCount,
setPage,
setRowsPerPage,
setDateRange,
});
return (
<BasePage>
<HeaderMonitorControls monitor={monitor} />
</BasePage>
);
};
export default UptimeDetailsPage;
+8 -2
View File
@@ -16,7 +16,7 @@ import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed.jsx";
// Uptime
import Uptime from "../Pages/Uptime/Monitors";
import UptimeDetails from "../Pages/Uptime/Details/index.jsx";
import UptimeDetails from "../Pages/Uptime/Details";
import UptimeCreate from "../Pages/Uptime/Create/index.jsx";
// PageSpeed
@@ -103,7 +103,13 @@ const Routes = () => {
/>
<Route
path="/uptime/:monitorId/"
element={<UptimeDetails />}
element={
<>
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
<UptimeDetails />
</ThemeProvider>
</>
}
/>
<Route
path="/uptime/configure/:monitorId/"
+5
View File
@@ -139,3 +139,8 @@ export interface LatestCheck {
checkedAt: string;
id: string;
}
export interface ChecksResponse {
checks: Check[];
checksCount: number;
}
+25
View File
@@ -47,3 +47,28 @@ export interface MonitorsWithChecksResponse {
monitors: MonitorWithChecks[];
summary: MonitorsSummary;
}
export interface MonitorGroupedCheck {
time: number;
responseTime: number;
originalResponseTime: number;
totalChecks: number;
}
export interface MonitorStats {
totalChecks: number;
upChecks: number;
downChecks: number;
uptimePercentage: number;
averageResponseTime: number;
}
export interface MonitorData {
monitor: Monitor;
groupedChecks: MonitorGroupedCheck[];
}
export interface MonitorDetailsResponse {
monitorData: MonitorData;
monitorStats: MonitorStats;
}