Merge pull request #3188 from bluewave-labs/feat/v2-pagespeed

feat/v2 pagespeed
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-01-26 12:50:30 -08:00
commit 1cffd51bd3
17 files changed
+790 -19

No files matched your search

@@ -1,15 +1,51 @@
import Stack from "@mui/material/Stack";
import Alert from "@mui/material/Alert";
import Link from "@mui/material/Link";
import {
ErrorFallback,
EmptyFallback,
EmptyMonitorFallback,
BaseFallback,
} from "@/Components/v2/design-elements/Fallback";
import { Breadcrumb } from "@/Components/v2/design-elements/Breadcrumb";
import CircularProgress from "@mui/material/CircularProgress";
import type { StackProps } from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { Link as RouterLink } from "react-router-dom";
import { Typography } from "@mui/material";
export const PageSpeedKeyPriorityFallback = () => {
return (
<BaseFallback>
<Alert
severity="warning"
sx={{
width: "100%",
maxWidth: 600,
}}
>
<Typography>
<Trans
i18nKey="common.alerts.pageSpeedApiKey.content"
components={{
settingsLink: (
<Link
component={RouterLink}
to="/settings"
color="inherit"
fontWeight="inherit"
/>
),
}}
/>
</Typography>
</Alert>
</BaseFallback>
);
};
interface BasePageProps extends StackProps {
loading?: boolean;
error?: boolean;
@@ -114,6 +150,7 @@ interface MonitorBasePageWithStatesProps extends StackProps {
page: string;
actionLink?: string;
children: React.ReactNode;
priorityFallback?: React.ReactNode;
}
const isEmpty = (items: any[]) => {
@@ -129,12 +166,25 @@ export const MonitorBasePageWithStates = ({
page,
actionLink,
children,
priorityFallback,
...props
}: MonitorBasePageWithStatesProps) => {
const { t } = useTranslation();
const showLoading = loading && (!items || items.length === 0);
if (priorityFallback) {
return (
<BasePage
loading={loading}
error={error}
{...props}
>
{priorityFallback}
</BasePage>
);
}
if (!loading && isEmpty(items)) {
return (
<EmptyMonitorFallback
@@ -147,7 +147,7 @@ export function DataTable<
<TableContainer
component={Paper}
elevation={0}
sx={{ boxShadow: "none" }}
sx={{ boxShadow: "none", overflowX: "hidden" }}
>
<Table
sx={{
@@ -173,6 +173,7 @@ export function DataTable<
color: theme.palette.text.secondary,
padding: `${theme.spacing(7)} ${theme.spacing(8)}`,
fontSize: theme.typography.fontSize,
overflowX: "hidden",
},
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
borderBottom: "none",
@@ -0,0 +1,56 @@
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import { Button } from "@/Components/v2/inputs";
import { typographyLevels } from "@/Utils/Theme/v2Palette";
import { useTranslation } from "react-i18next";
export const DialogInput = ({
open,
title,
content,
onConfirm,
onCancel,
confirmColor = "primary",
cancelColor = "error",
loading = false,
}: {
open: boolean;
title?: string;
content?: string;
onConfirm?(item: any): any;
onCancel?(item: any): any;
confirmColor?: "error" | "primary";
cancelColor?: "error" | "primary";
loading?: boolean;
}) => {
const { t } = useTranslation();
return (
<Dialog open={open}>
<DialogTitle sx={{ fontSize: typographyLevels.l }}>{title}</DialogTitle>
<DialogContent>
<DialogContentText>{content}</DialogContentText>
</DialogContent>
<DialogActions>
<Button
loading={loading}
variant="contained"
color={cancelColor}
onClick={onCancel}
>
{t("common.buttons.cancel")}
</Button>
<Button
loading={loading}
variant="contained"
color={confirmColor}
onClick={onConfirm}
>
{t("common.buttons.confirm")}
</Button>
</DialogActions>
</Dialog>
);
};
@@ -8,3 +8,4 @@ export {
ToggleButtonInput as ToggleButton,
ToggleButtonGroupInput as ToggleButtonGroup,
} from "./ToggleButton";
export { DialogInput as Dialog } from "./Dialog";
@@ -0,0 +1,107 @@
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"
maxWidth="400px"
width="100%"
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";
+36 -1
View File
@@ -1,7 +1,7 @@
import useSWR from "swr";
import type { SWRConfiguration } from "swr";
import type { AxiosRequestConfig } from "axios";
import { get, patch, post } from "@/Utils/ApiClient";
import { get, patch, post, deleteOp } from "@/Utils/ApiClient";
import { useState } from "react";
import { useToast } from "@/Hooks/UseToast";
@@ -107,3 +107,38 @@ export const usePatch = <B = any, R = any>() => {
return { patch: patchFn, loading, error };
};
export const useDelete = <R = any>() => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { toastError, toastSuccess } = useToast();
const deleteFn = async (
endpoint: string,
config?: AxiosRequestConfig
): Promise<ApiResponse<R> | null> => {
setLoading(true);
setError(null);
try {
const res = await deleteOp<ApiResponse<R>>(endpoint, {
...config,
headers: {
...config?.headers,
},
});
toastSuccess(res.data?.msg || "Operation successful");
return res.data;
} catch (err: any) {
console.error(err);
const errMsg = err?.response?.data?.message || err.message || "An error occurred";
toastError(errMsg);
setError(errMsg);
return null;
} finally {
setLoading(false);
}
};
return { deleteFn, loading, error };
};
@@ -0,0 +1,254 @@
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 { Monitor, MonitorWithChecks } from "@/Types/Monitor";
import type { ActionMenuItem } from "@/Components/v2/actions-menu";
export const PageSpeedMonitorsTable = ({
monitors,
refetch,
setSelectedMonitor,
sortField,
setSortField,
sortOrder,
setSortOrder,
count,
page,
setPage,
rowsPerPage,
setRowsPerPage,
}: {
monitors: MonitorWithChecks[];
refetch: Function;
setSelectedMonitor: 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,117 @@
import {
MonitorBasePageWithStates,
UpStatusBox,
DownStatusBox,
PausedStatusBox,
PageSpeedKeyPriorityFallback,
} from "@/Components/v2/design-elements";
import { Dialog } from "@/Components/v2/inputs";
import { HeaderCreate } from "@/Components/v2/common";
import Stack from "@mui/material/Stack";
import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable";
import type { Monitor } from "@/Types/Monitor";
import { useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { useIsAdmin } from "@/Hooks/useIsAdmin";
import { useGet, useDelete } from "@/Hooks/UseApi";
import type { MonitorsWithChecksResponse } from "@/Types/Monitor";
import type { AppSettingsResponse } from "@/Types/Settings";
const PageSpeedMonitorsPage = () => {
const { t } = useTranslation();
const theme = useTheme();
const isAdmin = useIsAdmin();
const { deleteFn, loading: isDeleting } = useDelete();
const monitorsUrl = "/monitors/team/with-checks?type=pagespeed&limit=10";
const {
data: monitorsData,
isLoading: monitorsIsLoading,
error: monitorsError,
refetch,
} = useGet<MonitorsWithChecksResponse>(monitorsUrl, {}, { refreshInterval: 30000 });
const settingsUrl = "/settings";
const {
data: settingsData,
isLoading: settingsIsLoading,
error: settingsError,
} = useGet<AppSettingsResponse>(settingsUrl);
const [selectedMonitor, setSelectedMonitor] = useState<Monitor | null>(null);
const isDialogOpen = Boolean(selectedMonitor);
const [sortField, setSortField] = useState<string>("name");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const monitors = monitorsData?.monitors;
const monitorsCount = monitorsData?.count;
const summary = monitorsData?.summary;
const isLoading = monitorsIsLoading || settingsIsLoading;
const showApiKeyWarning = isAdmin && settingsData && !settingsData.pagespeedKeySet;
const handleConfirm = async () => {
if (!selectedMonitor) return;
await deleteFn(`/monitors/${selectedMonitor.id}`);
setSelectedMonitor(null);
refetch();
};
const handleCancel = () => {
setSelectedMonitor(null);
};
return (
<MonitorBasePageWithStates
loading={isLoading}
error={monitorsError || settingsError}
items={monitors || []}
page="pageSpeed"
actionLink="/pagespeed/create"
priorityFallback={showApiKeyWarning ? <PageSpeedKeyPriorityFallback /> : undefined}
>
<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}
setSelectedMonitor={setSelectedMonitor}
sortField={sortField}
setSortField={setSortField}
sortOrder={sortOrder}
setSortOrder={setSortOrder}
count={monitorsCount || 0}
page={page}
setPage={setPage}
rowsPerPage={rowsPerPage}
setRowsPerPage={setRowsPerPage}
/>
<Dialog
open={isDialogOpen}
title={t("common.dialogs.delete.title")}
content={t("common.dialogs.delete.description")}
onConfirm={handleConfirm}
onCancel={handleCancel}
loading={isDeleting}
/>
</MonitorBasePageWithStates>
);
};
export default PageSpeedMonitorsPage;
@@ -24,6 +24,7 @@ import type { RootState } from "@/Types/state";
export const MonitorTable = ({
monitors,
refetch,
setSelectedMonitor,
sortField,
setSortField,
sortOrder,
@@ -36,6 +37,7 @@ export const MonitorTable = ({
}: {
monitors: Monitor[];
refetch: Function;
setSelectedMonitor: (monitor: Monitor | null) => void;
sortField: string;
setSortField: (field: string) => void;
sortOrder: "asc" | "desc";
@@ -97,7 +99,7 @@ export const MonitorTable = ({
id: 4,
label: t("pages.common.monitors.actions.configure"),
action: () => {
navigate(`/uptime/create/${monitor.id}`);
navigate(`/uptime/configure/${monitor.id}`);
},
},
// {
@@ -126,9 +128,7 @@ export const MonitorTable = ({
{t("pages.common.monitors.actions.delete")}
</Typography>
),
action: () => {
// setSelectedMonitor(monitor);
},
action: () => setSelectedMonitor(monitor),
closeMenu: true,
},
];
+29 -3
View File
@@ -5,14 +5,14 @@ import {
DownStatusBox,
PausedStatusBox,
} from "@/Components/v2/design-elements";
import { TextField } from "@/Components/v2/inputs";
import { TextField, Dialog } from "@/Components/v2/inputs";
import Stack from "@mui/material/Stack";
import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable";
import { HeaderCreate } from "@/Components/v2/common";
import { useTranslation } from "react-i18next";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useGet } from "@/Hooks/UseApi";
import { useGet, useDelete } from "@/Hooks/UseApi";
import type { Monitor, MonitorType, MonitorsWithChecksResponse } from "@/Types/Monitor";
import { useState, useMemo, useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
@@ -40,6 +40,8 @@ const UptimeMonitorsPage = () => {
const [search, setSearch] = useState<string>("");
const [sortField, setSortField] = useState<string>("");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [selectedMonitor, setSelectedMonitor] = useState<Monitor | null>(null);
const isDialogOpen = Boolean(selectedMonitor);
// Convert filter selections to API filter values
// Status: "up" -> true, "down" -> false
@@ -88,6 +90,7 @@ const UptimeMonitorsPage = () => {
data: monitors,
isLoading: monitorsLoading,
error,
refetch: refetchMonitorsAndSummary,
} = useGet<Monitor[]>(
"/monitors/team?type=http&type=ping&type=port&type=docker",
{},
@@ -107,6 +110,9 @@ const UptimeMonitorsPage = () => {
const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {};
// Delete hook
const { deleteFn, loading: isDeleting } = useDelete();
// Handlers
const handleClearFilters = useCallback(() => {
setSelectedTypes([]);
@@ -115,6 +121,18 @@ const UptimeMonitorsPage = () => {
setSearch("");
}, []);
const handleConfirm = async () => {
if (!selectedMonitor) return;
await deleteFn(`/monitors/${selectedMonitor.id}`);
setSelectedMonitor(null);
refetch();
refetchMonitorsAndSummary();
};
const handleCancel = () => {
setSelectedMonitor(null);
};
const isLoading = monitorsLoading || monitorsWithChecksLoading;
return (
@@ -164,6 +182,7 @@ const UptimeMonitorsPage = () => {
<MonitorTable
monitors={monitorsWithChecks || []}
refetch={refetch}
setSelectedMonitor={setSelectedMonitor}
count={count || 0}
page={page}
setPage={setPage}
@@ -173,7 +192,6 @@ const UptimeMonitorsPage = () => {
sortOrder={sortOrder}
setSortOrder={setSortOrder}
setRowsPerPage={(rowsPerPage: number) => {
console.log("wtf");
dispatch(
setRowsPerPage({
value: rowsPerPage,
@@ -183,6 +201,14 @@ const UptimeMonitorsPage = () => {
setPage(0);
}}
/>
<Dialog
open={isDialogOpen}
title={t("common.dialogs.delete.title")}
content={t("common.dialogs.delete.description")}
onConfirm={handleConfirm}
onCancel={handleCancel}
loading={isDeleting}
/>
</MonitorBasePageWithStates>
);
};
+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;
}
+24 -1
View File
@@ -6,7 +6,23 @@
"toggleTheme": "Toggles light & dark",
"testNotifications": "Test notifications",
"create": "Create",
"incidents": "Incidents"
"incidents": "Incidents",
"pause": "Pause",
"resume": "Resume",
"delete": "Delete",
"cancel": "Cancel",
"confirm": "Confirm"
},
"alerts": {
"pageSpeedApiKey": {
"content": "Warning: You haven't added a Google PageSpeed API key yet. Visit <settingsLink>Settings</settingsLink> to add one. Without it, the PageSpeed monitor won't function."
}
},
"dialogs": {
"delete": {
"description": "This action cannot be undone.",
"title": "Are you sure you want to delete this?"
}
},
"toasts": {
"networkError": "Network error",
@@ -216,6 +232,13 @@
"statusCode": "Status code"
}
}
},
"pageSpeed": {
"table": {
"headers": {
"pageSpeedScore": "PageSpeed score"
}
}
}
},
"distributedUptimeStatusCreateStatusPage": "status page",
+22 -4
View File
@@ -342,7 +342,7 @@ export class MonitorService implements IMonitorService {
order?: "asc" | "desc";
explain?: boolean;
}): Promise<MonitorsWithChecksByTeamIdResult> => {
const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId);
const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId, { type });
const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter });
const monitors = await this.monitorsRepository.findByTeamId(teamId, {
limit,
@@ -402,9 +402,27 @@ export class MonitorService implements IMonitorService {
deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
const monitor = await this.monitorsRepository.deleteById(monitorId, teamId);
await this.monitorStatsRepository.deleteByMonitorId(monitor.id);
await this.checksRepository.deleteByMonitorId(monitor.id);
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id);
await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: any) => {
this.logger.warn({
message: `Error deleting monitor stats for monitor ${monitor.id} with name ${monitor.name}`,
service: SERVICE_NAME,
stack: err.stack,
});
});
await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: any) => {
this.logger.warn({
message: `Error deleting checks for monitor ${monitor.id} with name ${monitor.name}`,
service: SERVICE_NAME,
stack: err.stack,
});
});
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id).catch((err: any) => {
this.logger.warn({
message: `Error removing monitor ${monitor.id} with name ${monitor.name} from status pages`,
service: SERVICE_NAME,
stack: err.stack,
});
});
await this.jobQueue.deleteJob(monitor);
return monitor;
};