From abadc99bf1bffcbd92d11654605a02c9759a683d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 6 Oct 2025 13:59:31 -0700 Subject: [PATCH 1/8] status --- client/package-lock.json | 28 ++++++++ client/package.json | 1 + .../src/Components/v2/DesignElements/Dot.tsx | 23 +++++++ .../Components/v2/DesignElements/PulseDot.tsx | 44 ++++++++++++ .../src/Components/v2/Inputs/ButtonGroup.tsx | 27 ++++++++ client/src/Components/v2/Inputs/index.tsx | 1 + .../Components/v2/Monitors/HeaderControls.tsx | 67 +++++++++++++++++++ .../Components/v2/Monitors/MonitorStatus.tsx | 60 +++++++++++++++++ client/src/Hooks/v2/UseApi.tsx | 25 ++++++- client/src/Pages/v2/Uptime/Create.tsx | 4 +- client/src/Pages/v2/Uptime/Details.tsx | 39 +++++++++++ client/src/Pages/v2/Uptime/MonitorTable.tsx | 29 +++++--- client/src/Routes/v2router.tsx | 9 ++- client/src/Types/Monitor.ts | 3 +- client/src/Utils/ApiClient.ts | 6 ++ client/src/Utils/MonitorUtils.ts | 19 ++++++ 16 files changed, 368 insertions(+), 17 deletions(-) create mode 100644 client/src/Components/v2/DesignElements/Dot.tsx create mode 100644 client/src/Components/v2/DesignElements/PulseDot.tsx create mode 100644 client/src/Components/v2/Inputs/ButtonGroup.tsx create mode 100644 client/src/Components/v2/Monitors/HeaderControls.tsx create mode 100644 client/src/Components/v2/Monitors/MonitorStatus.tsx create mode 100644 client/src/Pages/v2/Uptime/Details.tsx create mode 100644 client/src/Utils/MonitorUtils.ts diff --git a/client/package-lock.json b/client/package-lock.json index 7d6350f45..7cf0e8631 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -26,6 +26,7 @@ "i18next": "25.4.2", "joi": "17.13.3", "mui-color-input": "^6.0.0", + "pretty-ms": "9.3.0", "react": "18.3.1", "react-dom": "^18.2.0", "react-hook-form": "7.63.0", @@ -5184,6 +5185,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5311,6 +5324,21 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/client/package.json b/client/package.json index 6d4858027..71f088909 100644 --- a/client/package.json +++ b/client/package.json @@ -31,6 +31,7 @@ "i18next": "25.4.2", "joi": "17.13.3", "mui-color-input": "^6.0.0", + "pretty-ms": "9.3.0", "react": "18.3.1", "react-dom": "^18.2.0", "react-hook-form": "7.63.0", diff --git a/client/src/Components/v2/DesignElements/Dot.tsx b/client/src/Components/v2/DesignElements/Dot.tsx new file mode 100644 index 000000000..ca3884cfb --- /dev/null +++ b/client/src/Components/v2/DesignElements/Dot.tsx @@ -0,0 +1,23 @@ +export const Dot = ({ + color = "gray", + size = "4px", + style, +}: { + color?: string; + size?: string; + style?: React.CSSProperties; +}) => { + return ( + + ); +}; diff --git a/client/src/Components/v2/DesignElements/PulseDot.tsx b/client/src/Components/v2/DesignElements/PulseDot.tsx new file mode 100644 index 000000000..a785b5a0d --- /dev/null +++ b/client/src/Components/v2/DesignElements/PulseDot.tsx @@ -0,0 +1,44 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +export const PulseDot = ({ color }: { color: string }) => { + const theme = useTheme(); + return ( + + + + ); +}; diff --git a/client/src/Components/v2/Inputs/ButtonGroup.tsx b/client/src/Components/v2/Inputs/ButtonGroup.tsx new file mode 100644 index 000000000..6dde0cb3f --- /dev/null +++ b/client/src/Components/v2/Inputs/ButtonGroup.tsx @@ -0,0 +1,27 @@ +import { useTheme } from "@mui/material/styles"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import type { ButtonGroupProps } from "@mui/material/ButtonGroup"; +export const ButtonGroupInput: React.FC = ({ + orientation, + ...props +}) => { + const theme = useTheme(); + + return ( + + ); +}; diff --git a/client/src/Components/v2/Inputs/index.tsx b/client/src/Components/v2/Inputs/index.tsx index 9b582f3d7..5f3eb2810 100644 --- a/client/src/Components/v2/Inputs/index.tsx +++ b/client/src/Components/v2/Inputs/index.tsx @@ -1 +1,2 @@ export { ButtonInput as Button } from "./Button"; +export { ButtonGroupInput as ButtonGroup } from "./ButtonGroup"; diff --git a/client/src/Components/v2/Monitors/HeaderControls.tsx b/client/src/Components/v2/Monitors/HeaderControls.tsx new file mode 100644 index 000000000..f7dfcf7c8 --- /dev/null +++ b/client/src/Components/v2/Monitors/HeaderControls.tsx @@ -0,0 +1,67 @@ +import Stack from "@mui/material/Stack"; +import { MonitorStatus } from "@/Components/v2/Monitors/MonitorStatus"; +import { ButtonGroup, Button } from "@/Components/v2/Inputs"; +import Tooltip from "@mui/material/Tooltip"; +import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined"; +import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined"; +import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined"; +import EmailIcon from "@mui/icons-material/Email"; +import BugReportOutlinedIcon from "@mui/icons-material/BugReportOutlined"; + +import { useMediaQuery } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; + +import type { IMonitor } from "@/Types/Monitor"; + +export const HeaderControls = ({ + monitor, + patch, + isPatching, + refetch, +}: { + monitor: IMonitor; + patch: Function; + isPatching: boolean; + refetch: Function; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + return ( + + + + + + + + + + + + ); +}; diff --git a/client/src/Components/v2/Monitors/MonitorStatus.tsx b/client/src/Components/v2/Monitors/MonitorStatus.tsx new file mode 100644 index 000000000..ff4d83352 --- /dev/null +++ b/client/src/Components/v2/Monitors/MonitorStatus.tsx @@ -0,0 +1,60 @@ +import type { IMonitor } from "@/Types/Monitor"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { PulseDot } from "@/Components/v2/DesignElements/PulseDot"; +import { Dot } from "@/Components/v2/DesignElements/Dot"; +import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils"; +import { useTheme } from "@mui/material/styles"; +import prettyMilliseconds from "pretty-ms"; +import { typographyLevels } from "@/Utils/Theme/v2/palette"; +import { useMediaQuery } from "@mui/material"; +export const MonitorStatus = ({ monitor }: { monitor: IMonitor }) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + if (!monitor) { + return null; + } + return ( + + + {monitor.name} + + + + + {formatUrl(monitor?.url)} + + {!isSmall && ( + <> + + + Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })} + + + )} + + + ); +}; diff --git a/client/src/Hooks/v2/UseApi.tsx b/client/src/Hooks/v2/UseApi.tsx index eab661793..e588c98c5 100644 --- a/client/src/Hooks/v2/UseApi.tsx +++ b/client/src/Hooks/v2/UseApi.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import useSWR from "swr"; import type { SWRConfiguration } from "swr"; import type { AxiosRequestConfig } from "axios"; -import { get, post } from "@/Utils/ApiClient"; // your axios wrapper +import { get, post, patch } from "@/Utils/ApiClient"; // your axios wrapper export type ApiResponse = { message: string; @@ -56,3 +56,26 @@ export const usePost = (endpoint: string) => { return { post: postFn, loading, error }; }; + +export const usePatch = (endpoint: string) => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const patchFn = async (body: B, config?: AxiosRequestConfig): Promise => { + setLoading(true); + setError(null); + + try { + const res = await patch(endpoint, body, config); + return res.data; + } catch (err: any) { + const errMsg = err?.response?.data?.msg || err.message || "An error occurred"; + setError(errMsg); + return null; + } finally { + setLoading(false); + } + }; + + return { patch: patchFn, loading, error }; +}; diff --git a/client/src/Pages/v2/Uptime/Create.tsx b/client/src/Pages/v2/Uptime/Create.tsx index 3aa73b5fb..cb8df803d 100644 --- a/client/src/Pages/v2/Uptime/Create.tsx +++ b/client/src/Pages/v2/Uptime/Create.tsx @@ -19,7 +19,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useGet, usePost } from "@/Hooks/v2/UseApi"; import type { ApiResponse } from "@/Hooks/v2/UseApi"; -const CreateUptimePage = () => { +const UptimeCreatePage = () => { const { t } = useTranslation(); const theme = useTheme(); @@ -259,4 +259,4 @@ const CreateUptimePage = () => { ); }; -export default CreateUptimePage; +export default UptimeCreatePage; diff --git a/client/src/Pages/v2/Uptime/Details.tsx b/client/src/Pages/v2/Uptime/Details.tsx new file mode 100644 index 000000000..5f743913a --- /dev/null +++ b/client/src/Pages/v2/Uptime/Details.tsx @@ -0,0 +1,39 @@ +import { BasePage } from "@/Components/v2/DesignElements"; +import { HeaderControls } from "@/Components/v2/Monitors/HeaderControls"; + +import { useParams } from "react-router"; +import { useGet, usePatch, type ApiResponse } from "@/Hooks/v2/UseApi"; +import { useState } from "react"; +const UptimeDetailsPage = () => { + const { id } = useParams(); + + // Local state + const [range, setRange] = useState("30m"); + + const { response, loading, error, refetch } = useGet( + `/monitors/${id}?range=${range}` + ); + const { + patch, + loading: isPatching, + error: postError, + } = usePatch(`/monitors/${id}/active`); + + const monitor = response?.data || null; + if (!monitor) { + return null; + } + + return ( + + + + ); +}; + +export default UptimeDetailsPage; diff --git a/client/src/Pages/v2/Uptime/MonitorTable.tsx b/client/src/Pages/v2/Uptime/MonitorTable.tsx index 93ae498f9..488533d8a 100644 --- a/client/src/Pages/v2/Uptime/MonitorTable.tsx +++ b/client/src/Pages/v2/Uptime/MonitorTable.tsx @@ -1,16 +1,22 @@ import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Table } from "@/Components/v2/DesignElements"; import { HistogramResponseTime } from "@/Components/v2/Monitors/HistogramResponseTime"; import type { Header } from "@/Components/v2/DesignElements/Table"; -import type { IMonitor } from "@/Types/Monitor"; -import { Table } from "@/Components/v2/DesignElements"; +import { ActionsMenu } from "@/Components/v2/ActionsMenu"; + import { useTranslation } from "react-i18next"; import { useMediaQuery } from "@mui/material"; import { useTheme } from "@mui/material/styles"; -import { ActionsMenu } from "@/Components/v2/ActionsMenu"; -import type { ActionMenuItem } from "@/Components/v2/ActionsMenu"; -import Typography from "@mui/material/Typography"; +import { useNavigate } from "react-router-dom"; -const getActions = (theme: any): ActionMenuItem[] => { +import type { IMonitor } from "@/Types/Monitor"; +import type { ActionMenuItem } from "@/Components/v2/ActionsMenu"; +const getActions = ( + theme: any, + monitor: IMonitor, + navigate: Function +): ActionMenuItem[] => { return [ { id: 1, @@ -24,7 +30,7 @@ const getActions = (theme: any): ActionMenuItem[] => { id: 2, label: "Details", action: () => { - console.log("Open details"); + navigate(`${monitor._id}`); }, }, { @@ -67,7 +73,7 @@ const getActions = (theme: any): ActionMenuItem[] => { ]; }; -const getHeaders = (theme: any, t: Function) => { +const getHeaders = (theme: any, t: Function, navigate: Function) => { const headers: Header[] = [ { id: "name", @@ -104,8 +110,8 @@ const getHeaders = (theme: any, t: Function) => { { id: "actions", content: t("actions"), - render: () => { - return ; + render: (row) => { + return ; }, }, ]; @@ -116,8 +122,9 @@ export const MonitorTable = ({ monitors }: { monitors: IMonitor[] }) => { const { t } = useTranslation(); const theme = useTheme(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const navigate = useNavigate(); - let headers = getHeaders(theme, t); + let headers = getHeaders(theme, t, navigate); if (isSmall) { headers = headers.filter((h) => h.id !== "histogram"); diff --git a/client/src/Routes/v2router.tsx b/client/src/Routes/v2router.tsx index 742302762..0e53deead 100644 --- a/client/src/Routes/v2router.tsx +++ b/client/src/Routes/v2router.tsx @@ -5,7 +5,8 @@ import { lightTheme, darkTheme } from "@/Utils/Theme/v2/theme"; import AuthLoginV2 from "@/Pages/v2/Auth/Login"; import AuthRegisterV2 from "@/Pages/v2/Auth/Register"; import UptimeMonitorsPage from "@/Pages/v2/Uptime/UptimeMonitors"; -import CreateUptimePage from "@/Pages/v2/Uptime/Create"; +import UptimeCreatePage from "@/Pages/v2/Uptime/Create"; +import UptimeDetailsPage from "@/Pages/v2/Uptime/Details"; import RootLayout from "@/Components/v2/Layouts/RootLayout"; const V2Routes = ({ mode = "light" }) => { @@ -34,9 +35,13 @@ const V2Routes = ({ mode = "light" }) => { path="uptime" element={} /> + } + /> } + element={} /> diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 8ceb4d009..05f7d7d6f 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -1,4 +1,5 @@ import type { Check } from "@/Types/Check"; +export type MonitorStatus = "up" | "down" | "initializing"; export interface IMonitor { checks: Check[]; @@ -9,7 +10,7 @@ export interface IMonitor { latestChecks: Check[]; n: number; name: string; - status: string; + status: MonitorStatus; type: string; updatedAt: string; updatedBy: string; diff --git a/client/src/Utils/ApiClient.ts b/client/src/Utils/ApiClient.ts index a44c35905..4f41efe2d 100644 --- a/client/src/Utils/ApiClient.ts +++ b/client/src/Utils/ApiClient.ts @@ -18,4 +18,10 @@ export const post = ( config: AxiosRequestConfig = {} ): Promise> => api.post(url, data, config); +export const patch = ( + url: string, + data: any, + config: AxiosRequestConfig = {} +): Promise> => api.patch(url, data, config); + export default api; diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/MonitorUtils.ts new file mode 100644 index 000000000..92afa8371 --- /dev/null +++ b/client/src/Utils/MonitorUtils.ts @@ -0,0 +1,19 @@ +import type { MonitorStatus } from "@/Types/Monitor"; + +export const getStatusColor = (status: MonitorStatus, theme: any): string => { + const statusColors: Record = { + up: theme.palette.success.lowContrast, + down: theme.palette.error.lowContrast, + initializing: theme.palette.warning.lowContrast, + }; + return statusColors[status]; +}; + +export const formatUrl = (url: string, maxLength: number = 55) => { + if (!url) return ""; + + const strippedUrl = url.replace(/^https?:\/\//, ""); + return strippedUrl.length > maxLength + ? `${strippedUrl.slice(0, maxLength)}…` + : strippedUrl; +}; From 8a1fc94c311bfe25d091af5c49b4e25edacc2eec Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 6 Oct 2025 15:08:55 -0700 Subject: [PATCH 2/8] stat boxes --- .../Components/v2/DesignElements/StatBox.tsx | 61 +++++++++++++++++++ .../Components/v2/DesignElements/index.tsx | 1 + client/src/Pages/v2/Uptime/Details.tsx | 52 +++++++++++++++- client/src/Pages/v2/Uptime/UptimeMonitors.tsx | 2 +- client/src/Utils/MonitorUtils.ts | 9 +++ client/src/Utils/Theme/v2/theme.ts | 7 +++ 6 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 client/src/Components/v2/DesignElements/StatBox.tsx diff --git a/client/src/Components/v2/DesignElements/StatBox.tsx b/client/src/Components/v2/DesignElements/StatBox.tsx new file mode 100644 index 000000000..e65454188 --- /dev/null +++ b/client/src/Components/v2/DesignElements/StatBox.tsx @@ -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 = ({ 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 ( + + {children} + + ); +}; + +type StatBoxProps = React.PropsWithChildren<{ + title: string; + subtitle: string; + palette?: PaletteKey; +}>; + +export const StatBox: React.FC = ({ + title, + subtitle, + palette, + children, +}) => { + const theme = useTheme(); + const textColor = palette ? theme.palette[palette].contrastText : "inherit"; + + return ( + + + {title} + {subtitle} + {children} + + + ); +}; diff --git a/client/src/Components/v2/DesignElements/index.tsx b/client/src/Components/v2/DesignElements/index.tsx index 41b4b319a..5e2f69c69 100644 --- a/client/src/Components/v2/DesignElements/index.tsx +++ b/client/src/Components/v2/DesignElements/index.tsx @@ -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"; diff --git a/client/src/Pages/v2/Uptime/Details.tsx b/client/src/Pages/v2/Uptime/Details.tsx index 5f743913a..eb8f986ad 100644 --- a/client/src/Pages/v2/Uptime/Details.tsx +++ b/client/src/Pages/v2/Uptime/Details.tsx @@ -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( - `/monitors/${id}?range=${range}` + `/monitors/${id}?embedChecks=true&range=${range}`, + + {}, + { refreshInterval: 30000 } ); const { patch, @@ -19,11 +29,27 @@ const UptimeDetailsPage = () => { error: postError, } = usePatch(`/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 ( { isPatching={isPatching} refetch={refetch} /> + + + = 0 + ? `${prettyMilliseconds(lastChecked, { secondsDecimalDigits: 0 })} ago` + : "N/A" + } + /> + + ); }; diff --git a/client/src/Pages/v2/Uptime/UptimeMonitors.tsx b/client/src/Pages/v2/Uptime/UptimeMonitors.tsx index e69da7ebb..8980c8260 100644 --- a/client/src/Pages/v2/Uptime/UptimeMonitors.tsx +++ b/client/src/Pages/v2/Uptime/UptimeMonitors.tsx @@ -18,7 +18,7 @@ const UptimeMonitors = () => { const theme = useTheme(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); - const { response, loading } = useGet("/monitors?embedChecks=true"); + const { response, loading } = useGet("/monitors?embedChecks=true", {}); const monitors = response?.data ?? ([] as IMonitor[]); if (monitors.length === 0 && !loading) { diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/MonitorUtils.ts index 92afa8371..5f598ba33 100644 --- a/client/src/Utils/MonitorUtils.ts +++ b/client/src/Utils/MonitorUtils.ts @@ -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 = { + up: "success", + down: "error", + initializing: "warning", + }; + return paletteMap[status]; +}; export const getStatusColor = (status: MonitorStatus, theme: any): string => { const statusColors: Record = { diff --git a/client/src/Utils/Theme/v2/theme.ts b/client/src/Utils/Theme/v2/theme.ts index 3d68613b9..d2581e236 100644 --- a/client/src/Utils/Theme/v2/theme.ts +++ b/client/src/Utils/Theme/v2/theme.ts @@ -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)"; From 29fbf0a36211c705daf2754cf5d2ffd20d7d001d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Oct 2025 14:44:55 -0700 Subject: [PATCH 3/8] charts --- .../Components/v2/DesignElements/BaseBox.tsx | 23 ++ .../Components/v2/DesignElements/StatBox.tsx | 9 +- .../v2/DesignElements/StatusBox.tsx | 19 +- .../Components/v2/DesignElements/index.tsx | 1 + .../v2/Monitors/ChartAvgResponse.tsx | 83 +++++++ .../v2/Monitors/ChartResponseTime.tsx | 153 ++++++++++++ .../Components/v2/Monitors/HeaderControls.tsx | 1 - .../v2/Monitors/HistogramStatus.tsx | 217 ++++++++++++++++++ client/src/Pages/v2/Uptime/Details.tsx | 63 ++++- client/src/Utils/MonitorUtils.ts | 10 + client/src/Utils/TimeUtils.ts | 28 +++ 11 files changed, 587 insertions(+), 20 deletions(-) create mode 100644 client/src/Components/v2/DesignElements/BaseBox.tsx create mode 100644 client/src/Components/v2/Monitors/ChartAvgResponse.tsx create mode 100644 client/src/Components/v2/Monitors/ChartResponseTime.tsx create mode 100644 client/src/Components/v2/Monitors/HistogramStatus.tsx diff --git a/client/src/Components/v2/DesignElements/BaseBox.tsx b/client/src/Components/v2/DesignElements/BaseBox.tsx new file mode 100644 index 000000000..14c4af1c4 --- /dev/null +++ b/client/src/Components/v2/DesignElements/BaseBox.tsx @@ -0,0 +1,23 @@ +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +import type { SxProps } from "@mui/material/styles"; + +type BaseBoxProps = React.PropsWithChildren<{ sx?: SxProps }>; + +export const BaseBox: React.FC = ({ children, sx }) => { + const theme = useTheme(); + return ( + + {children} + + ); +}; diff --git a/client/src/Components/v2/DesignElements/StatBox.tsx b/client/src/Components/v2/DesignElements/StatBox.tsx index e65454188..e5d4dcef5 100644 --- a/client/src/Components/v2/DesignElements/StatBox.tsx +++ b/client/src/Components/v2/DesignElements/StatBox.tsx @@ -4,6 +4,7 @@ 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"; +import { BaseBox } from "@/Components/v2/DesignElements"; type GradientBox = React.PropsWithChildren<{ palette?: PaletteKey }>; @@ -15,22 +16,18 @@ export const GradientBox: React.FC = ({ children, palette }) => { : `linear-gradient(340deg, ${theme.palette.tertiary.main} 10%, ${theme.palette.primary.main} 45%)`; return ( - {children} - + ); }; diff --git a/client/src/Components/v2/DesignElements/StatusBox.tsx b/client/src/Components/v2/DesignElements/StatusBox.tsx index 540255dfd..8cd5cfbc1 100644 --- a/client/src/Components/v2/DesignElements/StatusBox.tsx +++ b/client/src/Components/v2/DesignElements/StatusBox.tsx @@ -1,6 +1,7 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; +import { BaseBox } from "@/Components/v2/DesignElements"; import Background from "@/assets/Images/background-grid.svg?react"; import { useTranslation } from "react-i18next"; @@ -11,15 +12,13 @@ type StatusBoxProps = React.PropsWithChildren<{}>; export const BGBox: React.FC = ({ children }) => { const theme = useTheme(); return ( - = ({ children }) => { {children} - + ); }; diff --git a/client/src/Components/v2/DesignElements/index.tsx b/client/src/Components/v2/DesignElements/index.tsx index 5e2f69c69..359982c42 100644 --- a/client/src/Components/v2/DesignElements/index.tsx +++ b/client/src/Components/v2/DesignElements/index.tsx @@ -3,3 +3,4 @@ export { BasePage } from "./BasePage"; export { BGBox, UpStatusBox, DownStatusBox, PausedStatusBox } from "./StatusBox"; export { DataTable as Table } from "./Table"; export { GradientBox, StatBox } from "./StatBox"; +export { BaseBox } from "./BaseBox"; diff --git a/client/src/Components/v2/Monitors/ChartAvgResponse.tsx b/client/src/Components/v2/Monitors/ChartAvgResponse.tsx new file mode 100644 index 000000000..9b40a25c6 --- /dev/null +++ b/client/src/Components/v2/Monitors/ChartAvgResponse.tsx @@ -0,0 +1,83 @@ +import { BaseChart } from "./HistogramStatus"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Box from "@mui/material/Box"; +import AverageResponseIcon from "@/assets/icons/average-response-icon.svg?react"; +import { Cell, RadialBarChart, RadialBar, ResponsiveContainer } from "recharts"; + +import { getResponseTimeColor } from "@/Utils/MonitorUtils"; +import { useTheme } from "@mui/material/styles"; + +export const ChartAvgResponse = ({ avg, max }: { avg: number; max: number }) => { + const theme = useTheme(); + const chartData = [ + { name: "max", value: max - avg, color: "transparent" }, + { name: "avg", value: avg, color: "red" }, + ]; + + const palette = getResponseTimeColor(avg); + const msg: Record = { + success: "Excellent", + warning: "Average", + danger: "Poor", + }; + + return ( + }> + + + + + + + + + + + Low + High + + + + {msg[palette]} + + {`${avg?.toFixed()}ms`} + + + + ); +}; diff --git a/client/src/Components/v2/Monitors/ChartResponseTime.tsx b/client/src/Components/v2/Monitors/ChartResponseTime.tsx new file mode 100644 index 000000000..459a4f30b --- /dev/null +++ b/client/src/Components/v2/Monitors/ChartResponseTime.tsx @@ -0,0 +1,153 @@ +import { BaseChart } from "./HistogramStatus"; +import { BaseBox } from "../DesignElements"; +import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react"; +import { + AreaChart, + Area, + YAxis, + XAxis, + Tooltip, + CartesianGrid, + ResponsiveContainer, + Text, +} from "recharts"; +import Typography from "@mui/material/Typography"; + +import { + formatDateWithTz, + tickDateFormatLookup, + tooltipDateFormatLookup, +} from "@/Utils/TimeUtils"; +import { useTheme } from "@mui/material/styles"; +import type { GroupedCheck } from "@/Types/Check"; +import { useSelector } from "react-redux"; + +type XTickProps = { + x: number; + y: number; + payload: { value: any }; + range: string; +}; + +const XTick: React.FC = ({ x, y, payload, range }) => { + const format = tickDateFormatLookup(range); + const theme = useTheme(); + const uiTimezone = useSelector((state: any) => state.ui.timezone); + return ( + + {formatDateWithTz(payload?.value, format, uiTimezone)} + + ); +}; + +type ResponseTimeToolTipProps = { + active?: boolean | undefined; + payload?: any[]; + label?: string; + range: string; +}; + +const ResponseTimeToolTip: React.FC = ({ + active, + payload, + label, + range, +}) => { + if (!label) return null; + if (!payload) return null; + + const theme = useTheme(); + const format = tooltipDateFormatLookup(range); + const uiTimezone = useSelector((state: any) => state.ui.timezone); + const responseTime = Math.floor(payload?.[0]?.value || 0); + return ( + + {formatDateWithTz(label, format, uiTimezone)} + Response time: {responseTime} ms + + ); +}; + +export const ChartResponseTime = ({ + checks, + range, +}: { + checks: GroupedCheck[]; + range: string; +}) => { + const theme = useTheme(); + return ( + } + title="Response times" + > + + + + + + + + + + ( + + )} + /> + + ( + + )} + /> + + + + + ); +}; diff --git a/client/src/Components/v2/Monitors/HeaderControls.tsx b/client/src/Components/v2/Monitors/HeaderControls.tsx index f7dfcf7c8..056d88f0a 100644 --- a/client/src/Components/v2/Monitors/HeaderControls.tsx +++ b/client/src/Components/v2/Monitors/HeaderControls.tsx @@ -1,7 +1,6 @@ import Stack from "@mui/material/Stack"; import { MonitorStatus } from "@/Components/v2/Monitors/MonitorStatus"; import { ButtonGroup, Button } from "@/Components/v2/Inputs"; -import Tooltip from "@mui/material/Tooltip"; import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined"; import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined"; import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined"; diff --git a/client/src/Components/v2/Monitors/HistogramStatus.tsx b/client/src/Components/v2/Monitors/HistogramStatus.tsx new file mode 100644 index 000000000..f71f92b47 --- /dev/null +++ b/client/src/Components/v2/Monitors/HistogramStatus.tsx @@ -0,0 +1,217 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { BaseBox } from "@/Components/v2/DesignElements"; +import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; +import UptimeIcon from "@/assets/icons/uptime-icon.svg?react"; +import IncidentsIcon from "@/assets/icons/incidents.svg?react"; + +import type { GroupedCheck } from "@/Types/Check"; +import type { MonitorStatus } from "@/Types/Monitor"; + +import { useState } from "react"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; +import { useSelector } from "react-redux"; +import { useTheme } from "@mui/material/styles"; +import { getResponseTimeColor } from "@/Utils/MonitorUtils"; + +const XLabel = ({ + p1, + p2, + range, +}: { + p1: GroupedCheck; + p2: GroupedCheck; + range: string; +}) => { + const theme = useTheme(); + const uiTimezone = useSelector((state: any) => state.ui.timezone); + const dateFormat = range === "day" ? "MMM D, h:mm A" : "MMM D"; + return ( + <> + + {formatDateWithTz(p1._id, dateFormat, uiTimezone)} + + + {formatDateWithTz(p2._id, dateFormat, uiTimezone)} + + + ); +}; + +type BaseChartProps = React.PropsWithChildren<{ + icon: React.ReactNode; + title: string; +}>; + +export const BaseChart: React.FC = ({ children, icon, title }) => { + const theme = useTheme(); + + return ( + + + + + {icon} + + {title} + + {children} + + + ); +}; + +export const HistogramStatus = ({ + checks, + status, + range, + title, +}: { + checks: GroupedCheck[]; + status: MonitorStatus; + range: string; + title: string; +}) => { + const uiTimezone = useSelector((state: any) => state.ui.timezone); + + const icon = status === "up" ? : ; + const theme = useTheme(); + const [idx, setIdx] = useState(null); + const dateFormat = range === "1d" || range === "2h" ? "MMM D, h A" : "MMM D"; + + if (checks.length === 0) { + return ( + + + + {status === "up" ? "No checks yet" : "Great, no downtime yet!"} + + + + ); + } + + const totalChecks = checks.reduce((count, check) => { + return count + check.count; + }, 0); + + return ( + + + + + Total checks + {idx ? ( + + {checks[idx].count} + + {formatDateWithTz(checks[idx]._id, dateFormat, uiTimezone)} + + + ) : ( + {totalChecks} + )} + + + + + + } + /> + + {checks?.map((groupedCheck, idx) => { + const fillColor = getResponseTimeColor(groupedCheck.avgResponseTime); + return ( + setIdx(idx)} + onMouseLeave={() => setIdx(null)} + key={groupedCheck._id} + fill={theme.palette[fillColor].main} + /> + ); + })} + + + + + + ); +}; diff --git a/client/src/Pages/v2/Uptime/Details.tsx b/client/src/Pages/v2/Uptime/Details.tsx index eb8f986ad..d2cc20edf 100644 --- a/client/src/Pages/v2/Uptime/Details.tsx +++ b/client/src/Pages/v2/Uptime/Details.tsx @@ -2,20 +2,25 @@ 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 { HistogramStatus } from "@/Components/v2/Monitors/HistogramStatus"; +import { ChartAvgResponse } from "@/Components/v2/Monitors/ChartAvgResponse"; +import { useMediaQuery } from "@mui/material"; 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"; +import { ChartResponseTime } from "@/Components/v2/Monitors/ChartResponseTime"; const UptimeDetailsPage = () => { const { id } = useParams(); const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); // Local state - const [range, setRange] = useState("30m"); + const [range, setRange] = useState("2h"); const { response, loading, error, refetch } = useGet( `/monitors/${id}?embedChecks=true&range=${range}`, @@ -23,6 +28,27 @@ const UptimeDetailsPage = () => { {}, { refreshInterval: 30000 } ); + + const { + response: upResponse, + error: upError, + loading: upLoading, + } = useGet( + `/monitors/${id}?embedChecks=true&range=${range}&status=up`, + {}, + {} + ); + + const { + response: downResponse, + error: downError, + loading: downLoading, + } = useGet( + `/monitors/${id}?embedChecks=true&range=${range}&status=down`, + {}, + {} + ); + const { patch, loading: isPatching, @@ -35,6 +61,8 @@ const UptimeDetailsPage = () => { } const stats = response?.data?.stats || null; + const avgResponseTime = stats?.avgResponseTime || 0; + const maxResponseTime = stats?.maxResponseTime || 0; const streakDuration = stats?.currentStreakStartedAt ? Date.now() - stats?.currentStreakStartedAt @@ -44,9 +72,13 @@ const UptimeDetailsPage = () => { ? Date.now() - stats?.lastCheckTimestamp : -1; - const checks = response?.data?.checks || null; + const checks = response?.data?.checks || []; + const upChecks = upResponse?.data?.checks || []; + const downChecks = downResponse?.data?.checks || []; - console.log(response); + // TODO something with these + + console.log(loading, error, postError, checks, setRange); const palette = getStatusPalette(monitor.status); @@ -80,6 +112,31 @@ const UptimeDetailsPage = () => { subtitle={stats?.lastResponseTime ? `${stats?.lastResponseTime} ms` : "N/A"} /> + + + + + + ); }; diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/MonitorUtils.ts index 5f598ba33..dcd202361 100644 --- a/client/src/Utils/MonitorUtils.ts +++ b/client/src/Utils/MonitorUtils.ts @@ -18,6 +18,16 @@ export const getStatusColor = (status: MonitorStatus, theme: any): string => { return statusColors[status]; }; +export const getResponseTimeColor = (responseTime: number): PaletteKey => { + if (responseTime < 200) { + return "success"; + } else if (responseTime < 300) { + return "warning"; + } else { + return "error"; + } +}; + export const formatUrl = (url: string, maxLength: number = 55) => { if (!url) return ""; diff --git a/client/src/Utils/TimeUtils.ts b/client/src/Utils/TimeUtils.ts index 5dc9b23c1..618d9d1bc 100644 --- a/client/src/Utils/TimeUtils.ts +++ b/client/src/Utils/TimeUtils.ts @@ -23,3 +23,31 @@ export const formatDateWithTz = (timestamp: string, format: string, timezone: st const formattedDate = dayjs(timestamp).tz(timezone).format(format); return formattedDate; }; + +export const tickDateFormatLookup = (range: string) => { + const tickFormatLookup: Record = { + "2h": "h:mm A", + "24h": "h:mm A", + "7d": "MM/D, h:mm A", + "30d": "ddd. M/D", + }; + const format = tickFormatLookup[range]; + if (format === undefined) { + return ""; + } + return format; +}; + +export const tooltipDateFormatLookup = (range: string) => { + const dateFormatLookup: Record = { + "2h": "ddd. MMMM D, YYYY, hh:mm A", + "24h": "ddd. MMMM D, YYYY, hh:mm A", + "7d": "ddd. MMMM D, YYYY, hh:mm A", + "30d": "ddd. MMMM D, YYYY", + }; + const format = dateFormatLookup[range]; + if (format === undefined) { + return ""; + } + return format; +}; From 2246f227d104fa845f3462ba9c6850d0e08cb2dc Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Oct 2025 14:45:07 -0700 Subject: [PATCH 4/8] 30m -> 2h --- server/src/service/v2/business/MonitorService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/service/v2/business/MonitorService.ts b/server/src/service/v2/business/MonitorService.ts index 61fc8b114..c0570236b 100644 --- a/server/src/service/v2/business/MonitorService.ts +++ b/server/src/service/v2/business/MonitorService.ts @@ -63,8 +63,8 @@ class MonitorService implements IMonitorService { private getStartDate(range: string): Date { const now = new Date(); switch (range) { - case "30m": - return new Date(now.getTime() - 30 * 60 * 1000); + case "2h": + return new Date(now.getTime() - 2 * 60 * 60 * 1000); case "24h": return new Date(now.getTime() - 24 * 60 * 60 * 1000); case "7d": @@ -78,7 +78,7 @@ class MonitorService implements IMonitorService { private getDateFormat(range: string): string { switch (range) { - case "30m": + case "2h": return "%Y-%m-%dT%H:%M:00Z"; case "24h": case "7d": From a700f446fd313abe736244dc1c2d8b56420efd18 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Oct 2025 15:46:09 -0700 Subject: [PATCH 5/8] add header --- client/src/Components/v2/Inputs/Button.tsx | 6 +- .../src/Components/v2/Inputs/ButtonGroup.tsx | 11 ---- .../Components/v2/Monitors/HeaderRange.tsx | 60 +++++++++++++++++++ client/src/Pages/v2/Uptime/Create.tsx | 2 +- client/src/Pages/v2/Uptime/Details.tsx | 15 +++-- client/src/Types/mui.d.ts | 16 +++++ 6 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 client/src/Components/v2/Monitors/HeaderRange.tsx diff --git a/client/src/Components/v2/Inputs/Button.tsx b/client/src/Components/v2/Inputs/Button.tsx index 0bf1d14e1..bca6cee34 100644 --- a/client/src/Components/v2/Inputs/Button.tsx +++ b/client/src/Components/v2/Inputs/Button.tsx @@ -1,10 +1,12 @@ import Button from "@mui/material/Button"; import type { ButtonProps } from "@mui/material/Button"; -export const ButtonInput: React.FC = ({ ...props }) => { + +export const ButtonInput: React.FC = ({ filled, sx, ...props }) => { return ( + + + + + + ); +}; diff --git a/client/src/Pages/v2/Uptime/Create.tsx b/client/src/Pages/v2/Uptime/Create.tsx index cb8df803d..a135ec84c 100644 --- a/client/src/Pages/v2/Uptime/Create.tsx +++ b/client/src/Pages/v2/Uptime/Create.tsx @@ -5,7 +5,7 @@ import { ConfigBox, BasePage } from "@/Components/v2/DesignElements"; import RadioGroup from "@mui/material/RadioGroup"; import FormControl from "@mui/material/FormControl"; import { RadioWithDescription } from "@/Components/v2/Inputs/RadioInput"; -import Button from "@mui/material/Button"; +import { Button } from "@/Components/v2/Inputs"; import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; import { Typography } from "@mui/material"; import humanInterval from "human-interval"; diff --git a/client/src/Pages/v2/Uptime/Details.tsx b/client/src/Pages/v2/Uptime/Details.tsx index d2cc20edf..a1419178c 100644 --- a/client/src/Pages/v2/Uptime/Details.tsx +++ b/client/src/Pages/v2/Uptime/Details.tsx @@ -13,6 +13,7 @@ import { useState } from "react"; import { getStatusPalette } from "@/Utils/MonitorUtils"; import prettyMilliseconds from "pretty-ms"; import { ChartResponseTime } from "@/Components/v2/Monitors/ChartResponseTime"; +import { HeaderRange } from "@/Components/v2/Monitors/HeaderRange"; const UptimeDetailsPage = () => { const { id } = useParams(); @@ -73,8 +74,10 @@ const UptimeDetailsPage = () => { : -1; const checks = response?.data?.checks || []; - const upChecks = upResponse?.data?.checks || []; - const downChecks = downResponse?.data?.checks || []; + const upChecks = upResponse?.data?.checks ? [...upResponse.data.checks].reverse() : []; + const downChecks = downResponse?.data?.checks + ? [...downResponse.data.checks].reverse() + : []; // TODO something with these @@ -112,6 +115,10 @@ const UptimeDetailsPage = () => { subtitle={stats?.lastResponseTime ? `${stats?.lastResponseTime} ms` : "N/A"} /> + { diff --git a/client/src/Types/mui.d.ts b/client/src/Types/mui.d.ts index 84bcd799f..44dcd213e 100644 --- a/client/src/Types/mui.d.ts +++ b/client/src/Types/mui.d.ts @@ -22,3 +22,19 @@ declare module "@mui/material/Button" { accent: true; } } + +declare module "@mui/material/Button" { + interface ButtonPropsVariantOverrides { + group: true; + } +} + +declare module "@mui/material/ButtonGroup" { + interface ButtonGroupPropsColorOverrides { + accent: true; + } + + interface ButtonGroupPropsVariantOverrides { + group: true; + } +} From 16b68b3d00d44ce826f782dc56fdd5944e8e8d1d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Oct 2025 15:46:35 -0700 Subject: [PATCH 6/8] rever types --- client/src/Types/mui.d.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/client/src/Types/mui.d.ts b/client/src/Types/mui.d.ts index 44dcd213e..84bcd799f 100644 --- a/client/src/Types/mui.d.ts +++ b/client/src/Types/mui.d.ts @@ -22,19 +22,3 @@ declare module "@mui/material/Button" { accent: true; } } - -declare module "@mui/material/Button" { - interface ButtonPropsVariantOverrides { - group: true; - } -} - -declare module "@mui/material/ButtonGroup" { - interface ButtonGroupPropsColorOverrides { - accent: true; - } - - interface ButtonGroupPropsVariantOverrides { - group: true; - } -} From 86bb28e23953e399c1df6e0d9c11d2e509859d4e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Oct 2025 13:44:41 -0700 Subject: [PATCH 7/8] pagination --- client/package.json | 8 -- client/src/App.jsx | 15 +-- .../Components/v2/DesignElements/StatBox.tsx | 1 - .../v2/DesignElements/StatusLabel.tsx | 35 ++++++ .../Components/v2/DesignElements/Table.tsx | 118 ++++++++++++++++++ .../Components/v2/DesignElements/index.tsx | 3 +- client/src/Components/v2/Inputs/Button.tsx | 3 +- .../src/Components/v2/Inputs/ButtonGroup.tsx | 3 - .../v2/Monitors/ChartAvgResponse.tsx | 7 +- .../v2/Monitors/ChartResponseTime.tsx | 12 +- .../Components/v2/Monitors/HeaderControls.tsx | 1 + .../Components/v2/Monitors/HeaderRange.tsx | 8 +- .../v2/Monitors/HistogramStatus.tsx | 1 - client/src/Hooks/v2/UseApi.tsx | 3 +- client/src/Pages/v2/Uptime/CheckTable.tsx | 93 ++++++++++++++ client/src/Pages/v2/Uptime/Details.tsx | 30 +++-- client/src/Pages/v2/Uptime/MonitorTable.tsx | 3 +- client/src/Types/Check.ts | 30 +++++ 18 files changed, 327 insertions(+), 47 deletions(-) create mode 100644 client/src/Components/v2/DesignElements/StatusLabel.tsx create mode 100644 client/src/Pages/v2/Uptime/CheckTable.tsx diff --git a/client/package.json b/client/package.json index 71f088909..376671942 100644 --- a/client/package.json +++ b/client/package.json @@ -47,14 +47,6 @@ "vite-plugin-svgr": "^4.2.0", "zod": "4.1.11" }, - "unusedDepencies": { - "@solana/wallet-adapter-base": "0.9.25", - "@solana/wallet-adapter-material-ui": "0.16.35", - "@solana/wallet-adapter-react": "0.15.37", - "@solana/wallet-adapter-react-ui": "0.9.37", - "@solana/wallet-adapter-wallets": "0.19.34", - "@solana/web3.js": "1.98.0" - }, "devDependencies": { "@types/node": "24.5.2", "@types/react": "^18.2.66", diff --git a/client/src/App.jsx b/client/src/App.jsx index 04f0d5b65..d21850ebb 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -9,7 +9,6 @@ import { CssBaseline, GlobalStyles } from "@mui/material"; import { logger } from "./Utils/Logger"; // Import the logger import { networkService } from "./main"; import { Routes } from "./Routes"; -import WalletProvider from "./Components/WalletProvider"; import AppLayout from "@/Components/v1/Layouts/AppLayout"; function App() { @@ -24,16 +23,12 @@ function App() { }, []); return ( - /* Extract Themeprovider, baseline and global styles to Styles */ - - - - - - - - + + + + + ); } diff --git a/client/src/Components/v2/DesignElements/StatBox.tsx b/client/src/Components/v2/DesignElements/StatBox.tsx index e5d4dcef5..3450444b2 100644 --- a/client/src/Components/v2/DesignElements/StatBox.tsx +++ b/client/src/Components/v2/DesignElements/StatBox.tsx @@ -1,6 +1,5 @@ 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"; diff --git a/client/src/Components/v2/DesignElements/StatusLabel.tsx b/client/src/Components/v2/DesignElements/StatusLabel.tsx new file mode 100644 index 000000000..69b788e96 --- /dev/null +++ b/client/src/Components/v2/DesignElements/StatusLabel.tsx @@ -0,0 +1,35 @@ +import Box from "@mui/material/Box"; +import { BaseBox } from "@/Components/v2/DesignElements"; +import type { MonitorStatus } from "@/Types/Monitor"; + +import { getStatusPalette } from "@/Utils/MonitorUtils"; +import { useTheme } from "@mui/material/styles"; + +export const StatusLabel = ({ status }: { status: MonitorStatus }) => { + const theme = useTheme(); + const palette = getStatusPalette(status); + const transformedText = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase(); + + return ( + + + {transformedText} + + ); +}; diff --git a/client/src/Components/v2/DesignElements/Table.tsx b/client/src/Components/v2/DesignElements/Table.tsx index 6296d260c..58cc6aed1 100644 --- a/client/src/Components/v2/DesignElements/Table.tsx +++ b/client/src/Components/v2/DesignElements/Table.tsx @@ -5,7 +5,19 @@ import TableCell from "@mui/material/TableCell"; import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; + +import IconButton from "@mui/material/IconButton"; +import LastPageIcon from "@mui/icons-material/LastPage"; +import FirstPageIcon from "@mui/icons-material/FirstPage"; +import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"; +import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; + +import Box from "@mui/material/Box"; +import TablePagination from "@mui/material/TablePagination"; +import type { TablePaginationProps } from "@mui/material/TablePagination"; + import { useTheme } from "@mui/material/styles"; +import { useMediaQuery } from "@mui/material"; export type Header = { id: number | string; content: React.ReactNode; @@ -87,3 +99,109 @@ export function DataTable ); } + +interface TablePaginationActionsProps { + count: number; + page: number; + rowsPerPage: number; + onPageChange: (event: React.MouseEvent, newPage: number) => void; +} + +function TablePaginationActions(props: TablePaginationActionsProps) { + const theme = useTheme(); + const { count, page, rowsPerPage, onPageChange } = props; + + const handleFirstPageButtonClick = (event: React.MouseEvent) => { + onPageChange(event, 0); + }; + + const handleBackButtonClick = (event: React.MouseEvent) => { + onPageChange(event, page - 1); + }; + + const handleNextButtonClick = (event: React.MouseEvent) => { + onPageChange(event, page + 1); + }; + + const handleLastPageButtonClick = (event: React.MouseEvent) => { + onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)); + }; + + return ( + + + {theme.direction === "rtl" ? : } + + + {theme.direction === "rtl" ? : } + + = Math.ceil(count / rowsPerPage) - 1} + aria-label="next page" + > + {theme.direction === "rtl" ? : } + + = Math.ceil(count / rowsPerPage) - 1} + aria-label="last page" + > + {theme.direction === "rtl" ? : } + + + ); +} + +export const Pagination: React.FC = ({ ...props }) => { + const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm")); + const theme = useTheme(); + return ( + + ); +}; diff --git a/client/src/Components/v2/DesignElements/index.tsx b/client/src/Components/v2/DesignElements/index.tsx index 359982c42..6ba0a19ac 100644 --- a/client/src/Components/v2/DesignElements/index.tsx +++ b/client/src/Components/v2/DesignElements/index.tsx @@ -1,6 +1,7 @@ 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 { DataTable as Table, Pagination } from "./Table"; export { GradientBox, StatBox } from "./StatBox"; export { BaseBox } from "./BaseBox"; +export { StatusLabel } from "./StatusLabel"; diff --git a/client/src/Components/v2/Inputs/Button.tsx b/client/src/Components/v2/Inputs/Button.tsx index bca6cee34..2f28e8eda 100644 --- a/client/src/Components/v2/Inputs/Button.tsx +++ b/client/src/Components/v2/Inputs/Button.tsx @@ -1,10 +1,9 @@ import Button from "@mui/material/Button"; import type { ButtonProps } from "@mui/material/Button"; -export const ButtonInput: React.FC = ({ filled, sx, ...props }) => { +export const ButtonInput: React.FC = ({ sx, ...props }) => { return (