diff --git a/client/src/Components/monitors/UptimeStatsByPeriod.tsx b/client/src/Components/monitors/UptimeStatsByPeriod.tsx new file mode 100644 index 0000000000..59191e5af3 --- /dev/null +++ b/client/src/Components/monitors/UptimeStatsByPeriod.tsx @@ -0,0 +1,78 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { StatBox } from "@/Components/design-elements"; +import { useGet } from "@/Hooks/UseApi"; +import { useTranslation } from "react-i18next"; +import { getUptimeColor } from "@/Utils/MonitorUtils"; +import type { MonitorDetailsResponse } from "@/Types/Monitor"; + +interface UptimeStatsByPeriodProps { + monitorId?: string; +} + +export const UptimeStatsByPeriod = ({ monitorId }: UptimeStatsByPeriodProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const swrOpts = { revalidateOnFocus: false, refreshInterval: 60000 }; + + const { data: recentData } = useGet( + monitorId ? `/monitors/uptime/details/${monitorId}?dateRange=recent` : null, + {}, + swrOpts + ); + const { data: dayData } = useGet( + monitorId ? `/monitors/uptime/details/${monitorId}?dateRange=day` : null, + {}, + swrOpts + ); + const { data: weekData } = useGet( + monitorId ? `/monitors/uptime/details/${monitorId}?dateRange=week` : null, + {}, + swrOpts + ); + const { data: monthData } = useGet( + monitorId ? `/monitors/uptime/details/${monitorId}?dateRange=month` : null, + {}, + swrOpts + ); + + const results = [ + { key: "recent", data: recentData }, + { key: "day", data: dayData }, + { key: "week", data: weekData }, + { key: "month", data: monthData }, + ]; + + // Don't render until we have at least one result + if (results.every((r) => !r.data)) return null; + + return ( + + {results.map(({ key, data }) => { + const value = data?.monitorData?.groupedUptimePercentage ?? 0; + const percentage = (value * 100).toFixed(2); + + return ( + + + {percentage}% + + + ); + })} + + ); +}; diff --git a/client/src/Components/monitors/index.tsx b/client/src/Components/monitors/index.tsx index efba79a08e..97c6c14d4d 100644 --- a/client/src/Components/monitors/index.tsx +++ b/client/src/Components/monitors/index.tsx @@ -1,5 +1,6 @@ export * from "./ControlsFilter"; export * from "./MonitorStatBoxes"; +export * from "./UptimeStatsByPeriod"; export * from "./HeaderMonitorControls"; export * from "./HeaderGeoTabs"; export * from "./GeoChecksMap"; diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index 8828aa29a5..5e6f6989c4 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -12,7 +12,7 @@ import { import { TrendingUp, AlertTriangle } from "lucide-react"; import { ChecksTable } from "@/Pages/Uptime/Details/Components/ChecksTable"; import { GeoChecksTable } from "@/Pages/Uptime/Details/Components/GeoChecksTable"; -import { MonitorStatBoxes } from "@/Components/monitors"; +import { MonitorStatBoxes, UptimeStatsByPeriod } from "@/Components/monitors"; import { useTheme } from "@mui/material/styles"; import { useIsAdmin } from "@/Hooks/useIsAdmin"; @@ -186,6 +186,7 @@ const UptimeDetailsPage = () => { monitorStats={monitorStats} certificateExpiry={certificateExpiry} /> + { else return "error"; }; +export const getUptimeColor = (value: number, theme: any) => { + if (value >= 0.99) return theme.palette.success.main; + if (value >= 0.95) return theme.palette.warning.main; + return theme.palette.error.main; +}; + export const formatUrl = (url: string, maxLength: number = 55) => { if (!url) return ""; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index c6bf414787..cd33a5a8e6 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1271,6 +1271,14 @@ "Track historical uptime and reliability trends" ], "title": "An uptime monitor is used to:" + }, + "details": { + "uptimeStats": { + "recent": "2h", + "day": "24h", + "week": "7d", + "month": "30d" + } } } } diff --git a/server/src/validation/authValidation.ts b/server/src/validation/authValidation.ts index 045868c403..d90125cce4 100644 --- a/server/src/validation/authValidation.ts +++ b/server/src/validation/authValidation.ts @@ -6,7 +6,10 @@ import { passwordPattern, nameValidation, lowercaseEmailValidation } from "./sha //**************************************** export const loginValidation = z.object({ - email: z.email("Must be a valid email address").transform((val) => val.toLowerCase()), + email: z + .string() + .email("Must be a valid email address") + .transform((val) => val.toLowerCase()), password: z.string().min(1, "Password is required"), }); @@ -23,7 +26,7 @@ export const registrationBodyValidation = z.object({ }); export const recoveryValidation = z.object({ - email: z.email("Must be a valid email address"), + email: z.string().email("Must be a valid email address"), }); export const recoveryTokenBodyValidation = z.object({ @@ -40,7 +43,7 @@ export const newPasswordValidation = z.object({ }); export const inviteBodyValidation = z.object({ - email: z.email("Must be a valid email address"), + email: z.string().email("Must be a valid email address"), role: z.array(z.string()).min(1, "At least one role is required"), teamId: z.string().min(1, "Team ID is required"), }); diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index d974d873ff..fdc29fb917 100644 --- a/server/src/validation/notificationValidation.ts +++ b/server/src/validation/notificationValidation.ts @@ -9,7 +9,7 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ z.object({ notificationName: z.string().min(1, "Notification name is required"), type: z.literal("email"), - address: z.email("Please enter a valid e-mail address"), + address: z.string().email("Please enter a valid e-mail address"), homeserverUrl: z.union([z.string(), z.literal("")]).optional(), roomId: z.union([z.string(), z.literal("")]).optional(), accessToken: z.union([z.string(), z.literal("")]).optional(), diff --git a/server/src/validation/shared.ts b/server/src/validation/shared.ts index 28fd13c82a..fca5553d2f 100644 --- a/server/src/validation/shared.ts +++ b/server/src/validation/shared.ts @@ -13,7 +13,10 @@ export const nameValidation = z "Names must contain at least 1 letter and may only include letters, currency symbols, spaces, apostrophes, hyphens (-), periods (.), and parentheses ()." ); -export const lowercaseEmailValidation = z.email().transform((val) => val.toLowerCase()); +export const lowercaseEmailValidation = z + .string() + .email() + .transform((val) => val.toLowerCase()); export const booleanCoercion = z.preprocess((val) => { if (val === "true" || val === true) return true;