Merge pull request #3188 from bluewave-labs/feat/v2-pagespeed
feat/v2 pagespeed
This commit is contained in:
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";
|
||||
@@ -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;
|
||||
File renamed without changes.
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user