diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx
new file mode 100644
index 000000000..509886434
--- /dev/null
+++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx
@@ -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 (
+ event.stopPropagation()}
+ overflow={"hidden"}
+ sx={{
+ cursor: "default",
+ position: "relative",
+ }}
+ >
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx
new file mode 100644
index 000000000..6fd18251c
--- /dev/null
+++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx
@@ -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) => {
+ const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
+ const theme = useTheme();
+ return (
+
+
+ {formatDateWithTz(
+ payload?.[0]?.payload?.date ?? 0,
+ "MMMM D, YYYY, HH:mm A",
+ uiTimezone
+ )}
+
+
+ Score: {payload?.[0]?.payload?.score}
+
+
+ );
+};
diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx
index de38d2cab..ca0aaa15e 100644
--- a/client/src/Components/v2/monitors/index.tsx
+++ b/client/src/Components/v2/monitors/index.tsx
@@ -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";
diff --git a/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx b/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx
new file mode 100644
index 000000000..0322948c3
--- /dev/null
+++ b/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx
@@ -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();
+
+ const handlePageChange = (
+ _e: React.MouseEvent | null,
+ newPage: number
+ ) => {
+ setPage(newPage);
+ };
+
+ const handleRowsPerPageChange = (
+ e: React.ChangeEvent
+ ) => {
+ 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: (
+
+ {t("common.buttons.delete")}
+
+ ),
+ action: () => {
+ // setSelectedMonitor(monitor);
+ },
+ closeMenu: true,
+ },
+ ];
+ };
+
+ const getHeaders = () => {
+ const renderSortIcon = (isActive: boolean) => (
+
+ {isActive ? (
+ sortOrder === "asc" ? (
+
+ ) : (
+
+ )
+ ) : null}
+
+ );
+
+ const headers: Header[] = [
+ {
+ id: "name",
+ content: (
+ handleSort(e, "name")}
+ sx={{ cursor: "pointer" }}
+ >
+ {t("common.table.headers.name")}
+ {renderSortIcon(sortField === "name")}
+
+ ),
+ render: (row) => {
+ return row.name;
+ },
+ },
+ {
+ id: "status",
+ content: (
+ handleSort(e, "status")}
+ sx={{ cursor: "pointer" }}
+ >
+ {t("common.table.headers.status")}
+ {renderSortIcon(sortField === "status")}
+
+ ),
+ render: (row) => {
+ return ;
+ },
+ },
+ {
+ id: "histogram",
+ content: t("pages.pageSpeed.table.headers.pageSpeedScore"),
+ render: (row) => {
+ return (
+
+
+
+ );
+ },
+ },
+
+ {
+ id: "actions",
+ content: t("common.table.headers.actions"),
+ render: (row) => {
+ return ;
+ },
+ },
+ ];
+ return headers;
+ };
+
+ let headers = getHeaders();
+
+ if (isSmall) {
+ headers = headers.filter((h) => h.id !== "histogram");
+ }
+ return (
+
+ {
+ navigate(`/pagespeed/${row.id}`);
+ }}
+ />
+
+
+ );
+};
diff --git a/client/src/Pages/PageSpeed/Monitors/index.tsx b/client/src/Pages/PageSpeed/Monitors/index.tsx
new file mode 100644
index 000000000..158260456
--- /dev/null
+++ b/client/src/Pages/PageSpeed/Monitors/index.tsx
@@ -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(monitorsUrl);
+
+ const settingsUrl = "/settings";
+ const {
+ data: settingsData,
+ isLoading: settingsIsLoading,
+ error: settingsError,
+ } = useGet(settingsUrl);
+
+ const monitors = monitorsData?.monitors;
+ const monitorsCount = monitorsData?.count;
+ // const summary = monitorsData?.summary;
+ const settings = settingsData;
+
+ const isLoading = monitorsIsLoading || settingsIsLoading;
+
+ const [sortField, setSortField] = useState("name");
+ const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(10);
+
+ return (
+
+
+ {/*
+
+
+
+ */}
+
+
+ );
+};
+
+export default PageSpeedMonitorsPage;
diff --git a/client/src/Pages/PageSpeed/Monitors/index.jsx b/client/src/Pages/PageSpeed/Monitors/old.jsx
similarity index 100%
rename from client/src/Pages/PageSpeed/Monitors/index.jsx
rename to client/src/Pages/PageSpeed/Monitors/old.jsx
diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx
index 21e014688..59daf1702 100644
--- a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx
+++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx
@@ -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}`);
},
},
// {
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 715d4374e..c9a1084d6 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -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 (
{
path="/uptime"
element={
<>
-
+
>
@@ -105,7 +106,7 @@ const Routes = () => {
path="/uptime/:monitorId/"
element={
<>
-
+
>
@@ -118,7 +119,13 @@ const Routes = () => {
}
+ element={
+ <>
+
+
+
+ >
+ }
/>