Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions client/src/Components/monitors/UptimeStatsByPeriod.tsx
Original file line number Diff line number Diff line change
@@ -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<MonitorDetailsResponse>(
monitorId ? `/monitors/uptime/details/${monitorId}?dateRange=recent` : null,
{},
swrOpts
);
const { data: dayData } = useGet<MonitorDetailsResponse>(
monitorId ? `/monitors/uptime/details/${monitorId}?dateRange=day` : null,
{},
swrOpts
);
const { data: weekData } = useGet<MonitorDetailsResponse>(
monitorId ? `/monitors/uptime/details/${monitorId}?dateRange=week` : null,
{},
swrOpts
);
const { data: monthData } = useGet<MonitorDetailsResponse>(
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 (
<Stack
direction={{ xs: "column", md: "row" }}
gap={theme.spacing(8)}
>
{results.map(({ key, data }) => {
const value = data?.monitorData?.groupedUptimePercentage ?? 0;
const percentage = (value * 100).toFixed(2);

return (
<StatBox
key={key}
title={t(`pages.uptime.details.uptimeStats.${key}`)}
subtitle=""
sx={{ flex: 1 }}
>
<Typography
fontWeight={600}
sx={{ color: getUptimeColor(value, theme) }}
>
{percentage}%
</Typography>
</StatBox>
);
})}
</Stack>
);
};
1 change: 1 addition & 0 deletions client/src/Components/monitors/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./ControlsFilter";
export * from "./MonitorStatBoxes";
export * from "./UptimeStatsByPeriod";
export * from "./HeaderMonitorControls";
export * from "./HeaderGeoTabs";
export * from "./GeoChecksMap";
Expand Down
3 changes: 2 additions & 1 deletion client/src/Pages/Uptime/Details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -186,6 +186,7 @@ const UptimeDetailsPage = () => {
monitorStats={monitorStats}
certificateExpiry={certificateExpiry}
/>
<UptimeStatsByPeriod monitorId={monitorId} />
<HeaderTimeRange
isLoading={monitorIsLoading || checksIsLoading}
hasDateRange={true}
Expand Down
6 changes: 6 additions & 0 deletions client/src/Utils/MonitorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ export const getPageSpeedPalette = (score: number): PaletteKey => {
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 "";

Expand Down
8 changes: 8 additions & 0 deletions client/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
Expand Down
9 changes: 6 additions & 3 deletions server/src/validation/authValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
});

Expand All @@ -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({
Expand All @@ -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"),
});
Expand Down
2 changes: 1 addition & 1 deletion server/src/validation/notificationValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
5 changes: 4 additions & 1 deletion server/src/validation/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down