initial commit

This commit is contained in:
Alex Holliday committed 2026-01-26 18:24:58 +00:00
1 parent d0727f91dd
commit c7aa45cd77
10 files changed
+544 -6

No files matched your search

@@ -0,0 +1,106 @@
import Stack from "@mui/material/Stack";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
import { HistogramPageSpeedTooltip } from "@/Components/v2/monitors/charts/HistogramPageSpeedTooltip";
import { useTheme } from "@mui/material/styles";
import type { Check } from "@/Types/Check";
import type { MonitorStatus } from "@/Types/Monitor";
import { getStatusColor } from "@/Utils/MonitorUtils";
const processData = (checks: Check[]) => {
if (checks.length === 0) return [];
const formattedData: { score: number; date: string }[] = [];
const calculateScore = (entry: Check) => {
const accessibility = entry?.accessibility || 0;
const bestPractices = entry?.bestPractices || 0;
const performance = entry?.performance || 0;
const seo = entry?.seo || 0;
return Math.floor(((accessibility + bestPractices + performance + seo) / 4) * 100);
};
checks.forEach((entry) => {
const score = calculateScore(entry);
const date = entry.createdAt;
formattedData.push({ score, date });
});
return formattedData;
};
export const HistogramPageSpeed = ({
checks,
status,
}: {
checks: Check[];
status: MonitorStatus;
}) => {
const theme = useTheme();
const scores = processData(checks ?? []);
const color = getStatusColor(status, theme);
return (
<Stack
direction="row"
flexWrap="nowrap"
gap={theme.spacing(1.5)}
height="100px"
width="400px"
onClick={(event) => event.stopPropagation()}
overflow={"hidden"}
sx={{
cursor: "default",
position: "relative",
}}
>
<ResponsiveContainer
width="100%"
height={"100%"}
>
<AreaChart
data={scores}
margin={{ top: 10, bottom: -5 }}
style={{ cursor: "pointer" }}
>
<CartesianGrid
stroke={theme.palette.divider}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<Tooltip
cursor={{ stroke: theme.palette.divider }}
content={<HistogramPageSpeedTooltip />}
/>
<defs>
<linearGradient
id={`pagespeed-chart-${status}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={color}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={color}
stopOpacity={0}
/>
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="score"
stroke={color}
fill={`url(#pagespeed-chart-${status})`}
/>
</AreaChart>
</ResponsiveContainer>
</Stack>
);
};
@@ -0,0 +1,40 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { formatDateWithTz } from "@/Utils/TimeUtils";
import type { TooltipProps } from "recharts";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import type { RootState } from "@/Types/state";
export const HistogramPageSpeedTooltip = ({
payload,
}: TooltipProps<string | number, string>) => {
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const theme = useTheme();
return (
<Stack
alignItems={"flex-start"}
top={0}
left={0}
zIndex={10}
sx={{
backgroundColor: theme.palette.secondary.main,
border: 1,
borderColor: theme.palette.divider,
borderRadius: theme.shape.borderRadius,
p: theme.spacing(4),
}}
>
<Typography color={theme.palette.primary.contrastText}>
{formatDateWithTz(
payload?.[0]?.payload?.date ?? 0,
"MMMM D, YYYY, HH:mm A",
uiTimezone
)}
</Typography>
<Typography color={theme.palette.primary.contrastText}>
Score: {payload?.[0]?.payload?.score}
</Typography>
</Stack>
);
};
@@ -3,3 +3,5 @@ export * from "./MonitorStatBoxes";
export * from "./charts/HistogramStatus";
export * from "./charts/RadialAvgResponse";
export * from "./charts/HistogramDetails";
export * from "./charts/HistogramPageSpeed";
export * from "./charts/HistogramPageSpeedTooltip";
@@ -0,0 +1,253 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { Table, Pagination, StatusLabel } from "@/Components/v2/design-elements";
import { HistogramPageSpeed } from "@/Components/v2/monitors";
import { ArrowDown, ArrowUp } from "lucide-react";
import type { Header } from "@/Components/v2/design-elements/Table";
import { ActionsMenu } from "@/Components/v2/actions-menu";
import { useTranslation } from "react-i18next";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { usePatch } from "@/Hooks/UseApi";
import type { Check } from "@/Types/Check";
import type { Monitor, MonitorWithChecks } from "@/Types/Monitor";
import type { ActionMenuItem } from "@/Components/v2/actions-menu";
export const PageSpeedMonitorsTable = ({
monitors,
refetch,
sortField,
setSortField,
sortOrder,
setSortOrder,
count,
page,
setPage,
rowsPerPage,
setRowsPerPage,
}: {
monitors: MonitorWithChecks[];
refetch: Function;
sortField: string;
setSortField: (field: string) => void;
sortOrder: "asc" | "desc";
setSortOrder: (order: "asc" | "desc") => void;
count: number;
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const navigate = useNavigate();
const {
patch,
// loading: isPatching,
// error: postError,
} = usePatch<any, Monitor>();
const handlePageChange = (
_e: React.MouseEvent<HTMLButtonElement> | null,
newPage: number
) => {
setPage(newPage);
};
const handleRowsPerPageChange = (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => {
const value = Number(e.target.value);
setPage(0);
setRowsPerPage(value);
};
const handleSort = (e: any, field: string) => {
e.preventDefault();
e.stopPropagation();
if (sortField === field) {
const newOrder = sortOrder === "asc" ? "desc" : "asc";
setSortOrder(newOrder);
} else {
setSortField(field);
setSortOrder("asc");
}
refetch();
};
const getActions = (monitor: Monitor): ActionMenuItem[] => {
return [
{
id: 1,
label: t("pages.common.monitors.actions.openSite"),
action: () => {
window.open(monitor.url, "_blank", "noreferrer");
},
closeMenu: true,
},
{
id: 2,
label: t("pages.common.monitors.actions.details"),
action: () => {
navigate(`${monitor.id}`);
},
},
{
id: 3,
label: t("pages.common.monitors.actions.incidents"),
action: () => {
navigate(`/incidents?monitorId=${monitor.id}`);
},
},
{
id: 4,
label: t("pages.common.monitors.actions.configure"),
action: () => {
navigate(`/pagespeed/configure/${monitor.id}`);
},
},
// {
// id: 5,
// label: "Clone",
// action: () => {
// },
// },
{
id: 6,
label:
monitor.isActive === false
? t("common.buttons.resume")
: t("common.buttons.pause"),
action: async () => {
await patch(`/monitors/${monitor.id}/active`);
refetch();
},
closeMenu: true,
},
{
id: 7,
label: (
<Typography color={theme.palette.error.main}>
{t("common.buttons.delete")}
</Typography>
),
action: () => {
// setSelectedMonitor(monitor);
},
closeMenu: true,
},
];
};
const getHeaders = () => {
const renderSortIcon = (isActive: boolean) => (
<Box
width={16}
display="inline-flex"
justifyContent="center"
>
{isActive ? (
sortOrder === "asc" ? (
<ArrowUp size={16} />
) : (
<ArrowDown size={16} />
)
) : null}
</Box>
);
const headers: Header<Monitor>[] = [
{
id: "name",
content: (
<Stack
gap={theme.spacing(4)}
direction={"row"}
alignItems={"center"}
onClick={(e) => handleSort(e, "name")}
sx={{ cursor: "pointer" }}
>
{t("common.table.headers.name")}
{renderSortIcon(sortField === "name")}
</Stack>
),
render: (row) => {
return row.name;
},
},
{
id: "status",
content: (
<Stack
gap={theme.spacing(4)}
direction={"row"}
justifyContent={"center"}
alignItems={"center"}
onClick={(e) => handleSort(e, "status")}
sx={{ cursor: "pointer" }}
>
{t("common.table.headers.status")}
{renderSortIcon(sortField === "status")}
</Stack>
),
render: (row) => {
return <StatusLabel status={row.status} />;
},
},
{
id: "histogram",
content: t("pages.pageSpeed.table.headers.pageSpeedScore"),
render: (row) => {
return (
<Stack alignItems={"center"}>
<HistogramPageSpeed
checks={row.checks}
status={row.status}
/>
</Stack>
);
},
},
{
id: "actions",
content: t("common.table.headers.actions"),
render: (row) => {
return <ActionsMenu items={getActions(row)} />;
},
},
];
return headers;
};
let headers = getHeaders();
if (isSmall) {
headers = headers.filter((h) => h.id !== "histogram");
}
return (
<Box>
<Table
headers={headers}
data={monitors}
onRowClick={(row) => {
navigate(`/pagespeed/${row.id}`);
}}
/>
<Pagination
component="div"
count={count}
page={page}
rowsPerPage={rowsPerPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handleRowsPerPageChange}
/>
</Box>
);
};
@@ -0,0 +1,86 @@
import {
MonitorBasePageWithStates,
// UpStatusBox,
// DownStatusBox,
// PausedStatusBox,
} from "@/Components/v2/design-elements";
import { HeaderCreate } from "@/Components/v2/common";
// import Stack from "@mui/material/Stack";
import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable";
import { useState } from "react";
import { useTheme } from "@mui/material";
import { useIsAdmin } from "@/Hooks/useIsAdmin";
import { useGet } from "@/Hooks/UseApi";
import type { MonitorsWithChecksResponse } from "@/Types/Monitor";
import type { AppSettingsResponse } from "@/Types/Settings";
const PageSpeedMonitorsPage = () => {
const theme = useTheme();
const isAdmin = useIsAdmin();
const monitorsUrl = "/monitors/team/with-checks?type=pagespeed&limit=10";
const {
data: monitorsData,
isLoading: monitorsIsLoading,
error: monitorsError,
refetch,
} = useGet<MonitorsWithChecksResponse>(monitorsUrl);
const settingsUrl = "/settings";
const {
data: settingsData,
isLoading: settingsIsLoading,
error: settingsError,
} = useGet<AppSettingsResponse>(settingsUrl);
const monitors = monitorsData?.monitors;
const monitorsCount = monitorsData?.count;
// const summary = monitorsData?.summary;
const settings = settingsData;
const isLoading = monitorsIsLoading || settingsIsLoading;
const [sortField, setSortField] = useState<string>("name");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
return (
<MonitorBasePageWithStates
loading={isLoading}
error={monitorsError || settingsError}
items={monitors || []}
page="pageSpeed"
actionLink="/pagespeed/create"
>
<HeaderCreate
path="/pagespeed/create"
isLoading={isLoading}
isAdmin={isAdmin}
/>
{/* <Stack
direction={{ xs: "column", md: "row" }}
gap={theme.spacing(8)}
>
<UpStatusBox n={summary?.upMonitors || 0} />
<DownStatusBox n={summary?.downMonitors || 0} />
<PausedStatusBox n={summary?.pausedMonitors || 0} />
</Stack> */}
<PageSpeedMonitorsTable
monitors={monitors || []}
refetch={refetch}
sortField={sortField}
setSortField={setSortField}
sortOrder={sortOrder}
setSortOrder={setSortOrder}
count={monitorsCount || 0}
page={page}
setPage={setPage}
rowsPerPage={rowsPerPage}
setRowsPerPage={setRowsPerPage}
/>
</MonitorBasePageWithStates>
);
};
export default PageSpeedMonitorsPage;
@@ -97,7 +97,7 @@ export const MonitorTable = ({
id: 4,
label: t("pages.common.monitors.actions.configure"),
action: () => {
navigate(`/uptime/create/${monitor.id}`);
navigate(`/uptime/configure/${monitor.id}`);
},
},
// {
+11 -4
View File
@@ -20,7 +20,7 @@ import UptimeDetails from "../Pages/Uptime/Details";
import UptimeCreate from "../Pages/Uptime/Create/index.jsx";
// PageSpeed
import PageSpeed from "../Pages/PageSpeed/Monitors/index.jsx";
import PageSpeed from "../Pages/PageSpeed/Monitors/index";
import PageSpeedDetails from "../Pages/PageSpeed/Details/index.jsx";
import PageSpeedCreate from "../Pages/PageSpeed/Create/index.jsx";
@@ -63,6 +63,7 @@ import Logs from "../Pages/Logs/index.jsx";
const Routes = () => {
const mode = useSelector((state) => state.ui.mode);
const AdminCheckedRegister = withAdminCheck(AuthRegister);
const v2theme = mode === "light" ? lightTheme : darkTheme;
return (
<LibRoutes>
<Route
@@ -81,7 +82,7 @@ const Routes = () => {
path="/uptime"
element={
<>
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
<ThemeProvider theme={v2theme}>
<Uptime />
</ThemeProvider>
</>
@@ -105,7 +106,7 @@ const Routes = () => {
path="/uptime/:monitorId/"
element={
<>
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
<ThemeProvider theme={v2theme}>
<UptimeDetails />
</ThemeProvider>
</>
@@ -118,7 +119,13 @@ const Routes = () => {
<Route
path="pagespeed"
element={<PageSpeed />}
element={
<>
<ThemeProvider theme={v2theme}>
<PageSpeed />
</ThemeProvider>
</>
}
/>
<Route
path="pagespeed/create"
+34
View File
@@ -0,0 +1,34 @@
export interface SettingsThresholds {
cpu?: number;
memory?: number;
disk?: number;
temperature?: number;
}
export interface Settings {
id: string;
checkTTL: number;
language: string;
systemEmailHost?: string;
systemEmailPort?: number;
systemEmailAddress?: string;
systemEmailUser?: string;
systemEmailConnectionHost?: string;
systemEmailTLSServername?: string;
systemEmailSecure: boolean;
systemEmailPool: boolean;
systemEmailIgnoreTLS: boolean;
systemEmailRequireTLS: boolean;
systemEmailRejectUnauthorized: boolean;
showURL: boolean;
singleton: boolean;
globalThresholds?: SettingsThresholds;
createdAt: string;
updatedAt: string;
}
export interface AppSettingsResponse {
pagespeedKeySet: boolean;
emailPasswordSet: boolean;
settings: Settings;
}
+11 -1
View File
@@ -6,7 +6,10 @@
"toggleTheme": "Toggles light & dark",
"testNotifications": "Test notifications",
"create": "Create",
"incidents": "Incidents"
"incidents": "Incidents",
"pause": "Pause",
"resume": "Resume",
"delete": "Delete"
},
"toasts": {
"networkError": "Network error",
@@ -216,6 +219,13 @@
"statusCode": "Status code"
}
}
},
"pageSpeed": {
"table": {
"headers": {
"pageSpeedScore": "PageSpeed score"
}
}
}
},
"distributedUptimeStatusCreateStatusPage": "status page",