mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-20 16:39:07 -05:00
Merge branch 'develop' of github.com:bluewave-labs/Checkmate into fix/latest-incident-overflow
This commit is contained in:
@@ -79,9 +79,8 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
|
||||
|
||||
If you have any questions, suggestions or comments, you have several options:
|
||||
|
||||
- [Discord channel](https://discord.gg/NAb6H3UTjK)
|
||||
- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions)
|
||||
- [Reddit group](https://www.reddit.com/r/CheckmateMonitoring/)
|
||||
- [Discord channel](https://discord.gg/NAb6H3UTjK) (preferred)
|
||||
- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) (we check here from time to time)
|
||||
|
||||
Feel free to ask questions or share your ideas - we'd love to hear from you!
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import { getInfraGaugeColor } from "@/Utils/MonitorUtils";
|
||||
|
||||
const MINIMUM_VALUE = 0;
|
||||
const MAXIMUM_VALUE = 100;
|
||||
|
||||
export const Gauge = ({
|
||||
isLoading = false,
|
||||
progress = 0,
|
||||
radius = 70,
|
||||
strokeWidth = 15,
|
||||
precision = 1,
|
||||
unit = "%",
|
||||
}: {
|
||||
isLoading?: boolean;
|
||||
progress?: number;
|
||||
radius?: number;
|
||||
strokeWidth?: number;
|
||||
precision?: number;
|
||||
unit?: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
|
||||
|
||||
// Calculate the length of the stroke for the circle
|
||||
const { circumference, totalSize, strokeLength } = useMemo(
|
||||
() => ({
|
||||
circumference: 2 * Math.PI * radius,
|
||||
totalSize: radius * 2 + strokeWidth * 2,
|
||||
strokeLength: (progress / 100) * (2 * Math.PI * radius),
|
||||
}),
|
||||
[radius, strokeWidth, progress]
|
||||
);
|
||||
|
||||
const [offset, setOffset] = useState(circumference);
|
||||
useEffect(() => {
|
||||
setOffset(circumference);
|
||||
const timer = setTimeout(() => {
|
||||
setOffset(circumference - strokeLength);
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [progress, circumference, strokeLength]);
|
||||
|
||||
const fillColor = getInfraGaugeColor(progressWithinRange, theme);
|
||||
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
display={"inline-block"}
|
||||
position={"relative"}
|
||||
width={radius}
|
||||
height={radius}
|
||||
bgcolor={theme.palette.background.paper}
|
||||
borderRadius={"50%"}
|
||||
>
|
||||
<svg
|
||||
viewBox={`0 0 ${totalSize} ${totalSize}`}
|
||||
width={radius}
|
||||
height={radius}
|
||||
>
|
||||
<circle
|
||||
stroke={theme.palette.secondary.main}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
cx={totalSize / 2}
|
||||
cy={totalSize / 2}
|
||||
r={radius}
|
||||
/>
|
||||
<circle
|
||||
stroke={fillColor}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
strokeDashoffset={offset}
|
||||
fill="none"
|
||||
cx={totalSize / 2}
|
||||
cy={totalSize / 2}
|
||||
r={radius}
|
||||
style={{
|
||||
transform: "rotate(-90deg)",
|
||||
transformOrigin: "center",
|
||||
transition: "stroke-dashoffset 1.5s ease-in-out",
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<Typography
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
{`${progressWithinRange.toFixed(precision)}${unit}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import MuiTabs from "@mui/material/Tabs";
|
||||
import type { TabsProps } from "@mui/material/Tabs";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
interface CustomTabsProps extends TabsProps {}
|
||||
|
||||
export const Tabs = (props: CustomTabsProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<MuiTabs
|
||||
sx={{
|
||||
minHeight: 34,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
"& .MuiTabs-indicator": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
height: 2,
|
||||
bottom: 0,
|
||||
},
|
||||
"& .MuiTabs-flexContainer": {
|
||||
gap: theme.spacing(16),
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</MuiTabs>
|
||||
);
|
||||
};
|
||||
|
||||
import MuiTab from "@mui/material/Tab";
|
||||
import type { TabProps } from "@mui/material/Tab";
|
||||
interface CustomTabProps extends TabProps {}
|
||||
|
||||
export const Tab = (props: CustomTabProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<MuiTab
|
||||
disableRipple
|
||||
iconPosition="start"
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
minHeight: 34,
|
||||
padding: theme.spacing(1, 0),
|
||||
paddingBottom: 0,
|
||||
minWidth: "auto",
|
||||
alignItems: "flex-start",
|
||||
color: theme.palette.text.secondary,
|
||||
"&.Mui-selected": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
},
|
||||
"&:hover": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
"& .MuiTab-iconWrapper": {
|
||||
marginRight: theme.spacing(2),
|
||||
marginBottom: 0,
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -15,3 +15,5 @@ export { default as Icon } from "./Icon";
|
||||
export * from "./Tooltip";
|
||||
export * from "./StatBox";
|
||||
export * from "./BaseChart";
|
||||
export * from "./Gauge";
|
||||
export * from "./Tabs";
|
||||
|
||||
@@ -11,6 +11,7 @@ const statuses = ["up", "down"];
|
||||
const states = ["active", "paused"];
|
||||
|
||||
export const ControlsFilter = ({
|
||||
showTypes = true,
|
||||
selectedTypes,
|
||||
setSelectedTypes,
|
||||
selectedStatus,
|
||||
@@ -19,8 +20,9 @@ export const ControlsFilter = ({
|
||||
setSelectedState,
|
||||
onClearFilters,
|
||||
}: {
|
||||
selectedTypes: MonitorType[];
|
||||
setSelectedTypes: React.Dispatch<React.SetStateAction<MonitorType[]>>;
|
||||
showTypes?: boolean;
|
||||
selectedTypes?: MonitorType[];
|
||||
setSelectedTypes?: React.Dispatch<React.SetStateAction<MonitorType[]>>;
|
||||
selectedStatus: string;
|
||||
setSelectedStatus: React.Dispatch<React.SetStateAction<string>>;
|
||||
selectedState: string;
|
||||
@@ -30,27 +32,29 @@ export const ControlsFilter = ({
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const isFilterActive =
|
||||
selectedTypes.length > 0 || selectedStatus !== "" || selectedState !== "";
|
||||
(selectedTypes?.length ?? 0) > 0 || selectedStatus !== "" || selectedState !== "";
|
||||
return (
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Select
|
||||
multiple
|
||||
placeholder="Type"
|
||||
value={selectedTypes}
|
||||
onChange={(e) => setSelectedTypes(e.target.value as MonitorType[])}
|
||||
>
|
||||
{types.map((type) => (
|
||||
<MenuItem
|
||||
key={type}
|
||||
value={type}
|
||||
>
|
||||
<Typography textTransform={"capitalize"}>{type}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{showTypes && setSelectedTypes && (
|
||||
<Select
|
||||
multiple
|
||||
placeholder="Type"
|
||||
value={selectedTypes ?? []}
|
||||
onChange={(e) => setSelectedTypes(e.target.value as MonitorType[])}
|
||||
>
|
||||
{types.map((type) => (
|
||||
<MenuItem
|
||||
key={type}
|
||||
value={type}
|
||||
>
|
||||
<Typography textTransform={"capitalize"}>{type}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
<Select
|
||||
placeholder="Status"
|
||||
value={selectedStatus}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import { BaseChart } from "@/Components/v2/design-elements";
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { Fragment, useId } from "react";
|
||||
import { XTick } from "@/Components/v2/monitors";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { HardwareCheckStats } from "@/Types/Monitor";
|
||||
|
||||
const AREA_COLORS = [
|
||||
// Blues
|
||||
"#3182bd", // Deep blue
|
||||
"#6baed6", // Medium blue
|
||||
"#9ecae1", // Light blue
|
||||
|
||||
// Greens
|
||||
"#74c476", // Soft green
|
||||
"#a1d99b", // Light green
|
||||
"#c7e9c0", // Pale green
|
||||
|
||||
// Oranges
|
||||
"#fdae6b", // Warm orange
|
||||
"#fdd0a2", // Light orange
|
||||
"#feedde", // Pale orange
|
||||
|
||||
// Purples
|
||||
"#9467bd", // Lavender
|
||||
"#a55194", // Deep magenta
|
||||
"#c994c7", // Soft magenta
|
||||
|
||||
// Reds
|
||||
"#ff9896", // Soft red
|
||||
"#de2d26", // Deep red
|
||||
"#fc9272", // Medium red
|
||||
|
||||
// Cyans/Teals
|
||||
"#17becf", // Cyan
|
||||
"#7fcdbb", // Teal
|
||||
"#a1dab4", // Light teal
|
||||
|
||||
// Yellows
|
||||
"#fec44f", // Mustard
|
||||
"#fee391", // Light yellow
|
||||
"#ffffd4", // Pale yellow
|
||||
|
||||
// Additional colors
|
||||
"#e377c2", // Soft pink
|
||||
"#bcbd22", // Olive
|
||||
"#2ca02c", // Vibrant green
|
||||
];
|
||||
|
||||
const createGradient = ({
|
||||
id,
|
||||
startColor,
|
||||
endColor,
|
||||
startOpacity = 0.8,
|
||||
endOpacity = 0,
|
||||
direction = "vertical",
|
||||
}: {
|
||||
id: string;
|
||||
startColor: string;
|
||||
endColor: string;
|
||||
startOpacity?: number;
|
||||
endOpacity?: number;
|
||||
direction?: "vertical" | "horizontal";
|
||||
}) => (
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={id}
|
||||
x1={direction === "vertical" ? "0" : "0"}
|
||||
y1={direction === "vertical" ? "0" : "0"}
|
||||
x2={direction === "vertical" ? "0" : "1"}
|
||||
y2={direction === "vertical" ? "1" : "0"}
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={startColor}
|
||||
stopOpacity={startOpacity}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={endColor}
|
||||
stopOpacity={endOpacity}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
);
|
||||
|
||||
export const HistogramInfrastructure = ({
|
||||
dateRange,
|
||||
title,
|
||||
type,
|
||||
idx: _idx,
|
||||
checks,
|
||||
xKey,
|
||||
yDomain,
|
||||
dataKeys,
|
||||
gradient = false,
|
||||
gradientDirection = "vertical",
|
||||
gradientStartColor,
|
||||
gradientEndColor,
|
||||
strokeColor,
|
||||
fillColor,
|
||||
yAxisFormatter,
|
||||
}: {
|
||||
dateRange: string;
|
||||
title: string;
|
||||
type: string;
|
||||
idx: number | null;
|
||||
checks: HardwareCheckStats[];
|
||||
xKey: string;
|
||||
yDomain?: number[];
|
||||
dataKeys: string[];
|
||||
gradient?: boolean;
|
||||
gradientDirection?: "vertical" | "horizontal";
|
||||
gradientStartColor?: string;
|
||||
gradientEndColor?: string;
|
||||
strokeColor: string;
|
||||
fillColor?: string;
|
||||
yAxisFormatter?: (value: number) => string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const uniqueId = useId();
|
||||
const data = checks;
|
||||
|
||||
let avgTemps: { bucketDate: string; avg_temp: number | null }[] = [];
|
||||
let tempYDomain: number[] = [];
|
||||
if (type === "temp") {
|
||||
avgTemps = data.map((check) => {
|
||||
const temps = check.avgTemperature || [];
|
||||
if (temps.length === 0) return { bucketDate: check.bucketDate, avg_temp: null };
|
||||
const totalTemp = temps.reduce((sum, temp) => sum + (temp || 0), 0);
|
||||
const avgTemp = totalTemp / temps.length;
|
||||
return { bucketDate: check.bucketDate, avg_temp: avgTemp };
|
||||
});
|
||||
|
||||
const maxTemp: number = avgTemps.reduce((max, item) => {
|
||||
return item.avg_temp && item.avg_temp > max ? item.avg_temp : max;
|
||||
}, 0);
|
||||
|
||||
tempYDomain = [0, Math.ceil((maxTemp * 1.3) / 10) * 10];
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseChart
|
||||
icon={null}
|
||||
title={title}
|
||||
>
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={200}
|
||||
>
|
||||
<AreaChart data={type === "temp" ? avgTemps : data}>
|
||||
<XAxis
|
||||
dataKey={xKey}
|
||||
tick={(props) => (
|
||||
<XTick
|
||||
{...props}
|
||||
range={dateRange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<YAxis
|
||||
domain={type === "temp" ? tempYDomain : yDomain}
|
||||
tickFormatter={yAxisFormatter}
|
||||
/>
|
||||
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.divider}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
{dataKeys?.map((dataKey, index) => {
|
||||
const gradientId = `gradient-${uniqueId}-${index}`;
|
||||
return (
|
||||
<Fragment key={`${dataKey}-${index}`}>
|
||||
{gradient === true &&
|
||||
createGradient({
|
||||
id: gradientId,
|
||||
startColor: gradientStartColor || AREA_COLORS[index],
|
||||
endColor: gradientEndColor || "transparent",
|
||||
direction: gradientDirection,
|
||||
})}
|
||||
<Area
|
||||
key={dataKey}
|
||||
type="monotone"
|
||||
dataKey={dataKey}
|
||||
stroke={strokeColor || AREA_COLORS[index]}
|
||||
fill={gradient === true ? `url(#${gradientId})` : fillColor}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</BaseChart>
|
||||
);
|
||||
};
|
||||
@@ -9,3 +9,4 @@ export * from "./charts/PiePageSpeed";
|
||||
export * from "./charts/PiePageSpeedLegend";
|
||||
export * from "./charts/HistogramPageSpeedDetails";
|
||||
export * from "./charts/HistogramPageSpeedDetailsTooltip";
|
||||
export * from "./charts/HistogramInfrastructure";
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
//Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableSkeleton from "@/Components/v1/Table/skeleton.jsx";
|
||||
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
|
||||
|
||||
//Utils
|
||||
import { formatDateWithTz } from "@/Utils/timeUtilsLegacy.js";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFetchChecksTeam, useFetchChecksByMonitor } from "@/Hooks/checkHooks.js";
|
||||
import { Typography, useTheme } from "@mui/material";
|
||||
import { lighten } from "@mui/material/styles";
|
||||
|
||||
const GetTooltip = (row) => {
|
||||
const theme = useTheme();
|
||||
const phases = row?.timings?.phases;
|
||||
|
||||
const phaseKeyFormattingMap = {
|
||||
firstByte: "first byte",
|
||||
};
|
||||
return (
|
||||
<Stack
|
||||
backgroundColor={lighten(theme.palette.primary.main, 0.1)}
|
||||
border={`1px solid ${theme.palette.primary.lowContrast}`}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
py={theme.spacing(2)}
|
||||
px={theme.spacing(4)}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>{`Status code: ${row?.statusCode}`}</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>{`Response time: ${row?.responseTime} ms`}</Typography>
|
||||
{phases && (
|
||||
<>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>{`Request timing: `}</Typography>
|
||||
<Table
|
||||
size="small"
|
||||
sx={{ ml: theme.spacing(2), mt: theme.spacing(2) }}
|
||||
>
|
||||
<TableBody>
|
||||
{Object.keys(phases)?.map((phaseKey) => (
|
||||
<TableRow key={phaseKey}>
|
||||
<TableCell sx={{ border: "none", p: 0 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="success"
|
||||
>
|
||||
{`${phaseKeyFormattingMap[phaseKey] || phaseKey}:`}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ border: "none", p: 0 }}>
|
||||
<Typography
|
||||
color={theme.palette.primary.contrastText}
|
||||
variant="body2"
|
||||
>{`${phases[phaseKey]} ms`}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const IncidentTable = ({
|
||||
isLoading,
|
||||
monitors,
|
||||
selectedMonitor,
|
||||
filter,
|
||||
dateRange,
|
||||
updateTrigger,
|
||||
}) => {
|
||||
//Redux state
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
//Local state
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const selectedMonitorDetails = monitors?.[selectedMonitor];
|
||||
const selectedMonitorType = selectedMonitorDetails?.type;
|
||||
|
||||
//Hooks
|
||||
const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] =
|
||||
useFetchChecksByMonitor({
|
||||
monitorId: selectedMonitor === "0" ? undefined : selectedMonitor,
|
||||
type: selectedMonitorType,
|
||||
status: false,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange,
|
||||
filter: filter === "resolved" ? "all" : filter,
|
||||
ack: filter === "resolved" ? true : false,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
enabled: selectedMonitor !== "0",
|
||||
updateTrigger,
|
||||
});
|
||||
|
||||
const [checksTeam, checksCountTeam, isLoadingTeam, networkErrorTeam] =
|
||||
useFetchChecksTeam({
|
||||
status: false,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange,
|
||||
filter: filter === "resolved" ? "all" : filter,
|
||||
ack: filter === "resolved" ? true : false,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
enabled: selectedMonitor === "0",
|
||||
updateTrigger,
|
||||
});
|
||||
|
||||
const checks = selectedMonitor === "0" ? checksTeam : checksMonitor;
|
||||
const checksCount = selectedMonitor === "0" ? checksCountTeam : checksCountMonitor;
|
||||
isLoading = isLoadingTeam || isLoadingMonitor;
|
||||
const networkError = selectedMonitor === "0" ? networkErrorTeam : networkErrorMonitor;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
//Handlers
|
||||
const handleChangePage = (_, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(event.target.value);
|
||||
};
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "monitorName",
|
||||
content: t("incidentsTableMonitorName"),
|
||||
render: (row) => {
|
||||
return monitors?.[row.metadata?.monitorId]?.name || "N/A";
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("incidentsTableStatus"),
|
||||
render: (row) => {
|
||||
const status = row.status === true ? "up" : "down";
|
||||
return (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "dateTime",
|
||||
content: t("incidentsTableDateTime"),
|
||||
render: (row) => {
|
||||
const formattedDate = formatDateWithTz(
|
||||
row.createdAt,
|
||||
"YYYY-MM-DD HH:mm:ss A",
|
||||
uiTimezone
|
||||
);
|
||||
return formattedDate;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "statusCode",
|
||||
content: t("incidentsTableStatusCode"),
|
||||
render: (row) => <HttpStatusLabel status={row.statusCode} />,
|
||||
},
|
||||
{ id: "message", content: t("incidentsTableMessage"), render: (row) => row.message },
|
||||
];
|
||||
|
||||
if (isLoading) return <TableSkeleton />;
|
||||
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<NetworkError />
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && typeof checksCount === "undefined") {
|
||||
return <GenericFallback>{t("incidentsTableNoIncidents")}</GenericFallback>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={checks}
|
||||
config={{ tooltipContent: GetTooltip }}
|
||||
/>
|
||||
<Pagination
|
||||
paginationLabel={t("incidentsTablePaginationLabel")}
|
||||
itemCount={checksCount}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
IncidentTable.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
monitors: PropTypes.object,
|
||||
selectedMonitor: PropTypes.string,
|
||||
filter: PropTypes.string,
|
||||
dateRange: PropTypes.string,
|
||||
updateTrigger: PropTypes.bool,
|
||||
setUpdateTrigger: PropTypes.func,
|
||||
};
|
||||
export default IncidentTable;
|
||||
@@ -1,139 +0,0 @@
|
||||
// Components
|
||||
import { Stack, Typography, Button, ButtonGroup } from "@mui/material";
|
||||
import Select from "@/Components/v1/Inputs/Select/index.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import SkeletonLayout from "./skeleton.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const OptionsHeader = ({
|
||||
shouldRender,
|
||||
selectedMonitor = 0,
|
||||
setSelectedMonitor,
|
||||
monitors,
|
||||
filter = "all",
|
||||
setFilter,
|
||||
dateRange = "hour",
|
||||
setDateRange,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : [];
|
||||
const filterOptions = [
|
||||
{ id: "all", name: t("incidentsOptionsHeaderFilterAll") },
|
||||
{ id: "down", name: t("incidentsOptionsHeaderFilterDown") },
|
||||
{ id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") },
|
||||
{ id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") },
|
||||
];
|
||||
|
||||
// The stacks below which are three in number have the same style so
|
||||
const stackStyles = {
|
||||
direction: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(6),
|
||||
};
|
||||
|
||||
if (!shouldRender) return <SkeletonLayout />;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack {...stackStyles}>
|
||||
<Typography
|
||||
display="inline-block"
|
||||
component="h1"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{t("incidentsOptionsHeader")}
|
||||
</Typography>
|
||||
<Select
|
||||
id="incidents-select-monitor"
|
||||
placeholder={t("incidentsOptionsPlaceholderAllServers")}
|
||||
value={selectedMonitor}
|
||||
onChange={(e) => setSelectedMonitor(e.target.value)}
|
||||
items={monitorNames}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
}}
|
||||
maxWidth={250}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack {...stackStyles}>
|
||||
<Typography
|
||||
display="inline-block"
|
||||
component="h1"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{t("incidentsOptionsHeaderFilterBy")}
|
||||
</Typography>
|
||||
<Select
|
||||
id="incidents-select-filter"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
items={filterOptions}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack {...stackStyles}>
|
||||
<Typography
|
||||
display="inline-block"
|
||||
component="h1"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{t("incidentsOptionsHeaderShow")}
|
||||
</Typography>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(dateRange === "hour").toString()}
|
||||
onClick={() => setDateRange("hour")}
|
||||
>
|
||||
{t("incidentsOptionsHeaderLastHour")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(dateRange === "day").toString()}
|
||||
onClick={() => setDateRange("day")}
|
||||
>
|
||||
{t("incidentsOptionsHeaderLastDay")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(dateRange === "week").toString()}
|
||||
onClick={() => setDateRange("week")}
|
||||
>
|
||||
{t("incidentsOptionsHeaderLastWeek")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(dateRange === "all").toString()}
|
||||
onClick={() => setDateRange("all")}
|
||||
>
|
||||
{t("incidentsOptionsHeaderFilterAll")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
OptionsHeader.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
selectedMonitor: PropTypes.string,
|
||||
setSelectedMonitor: PropTypes.func,
|
||||
monitors: PropTypes.object,
|
||||
filter: PropTypes.string,
|
||||
setFilter: PropTypes.func,
|
||||
dateRange: PropTypes.string,
|
||||
setDateRange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default OptionsHeader;
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton height={40} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -1,129 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import Background from "@/assets/Images/background-grid.svg?react";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
|
||||
const StatusBox = ({ title, value, status }) => {
|
||||
const theme = useTheme();
|
||||
let sharedStyles = {
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
"& svg": {
|
||||
width: 20,
|
||||
height: 20,
|
||||
opacity: 0.9,
|
||||
"& path": { stroke: theme.palette.primary.contrastTextTertiary, strokeWidth: 1.7 },
|
||||
},
|
||||
};
|
||||
|
||||
let color;
|
||||
let icon;
|
||||
if (status === "up") {
|
||||
color = theme.palette.success.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<Icon
|
||||
name="CheckCircle"
|
||||
size={20}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
} else if (status === "down") {
|
||||
color = theme.palette.error.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<Icon
|
||||
name="X"
|
||||
size={20}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
} else if (status === "paused") {
|
||||
color = theme.palette.warning.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<Icon
|
||||
name="AlertTriangle"
|
||||
size={20}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
color = theme.palette.accent.main;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<Icon
|
||||
name="Bell"
|
||||
size={20}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
flex={1}
|
||||
border={1}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
p={theme.spacing(8)}
|
||||
overflow="hidden"
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-10%"
|
||||
left="5%"
|
||||
>
|
||||
<Background />
|
||||
</Box>
|
||||
<Stack direction="column">
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Typography
|
||||
variant={"h2"}
|
||||
textTransform="uppercase"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{icon}
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
fontSize={theme.typography.h1.fontSize}
|
||||
fontWeight={600}
|
||||
color={color}
|
||||
gap={theme.spacing(1)}
|
||||
>
|
||||
{value}
|
||||
|
||||
<Typography
|
||||
fontSize={theme.typography.label.fontSize}
|
||||
fontWeight={300}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
sx={{
|
||||
opacity: 0.3,
|
||||
}}
|
||||
>
|
||||
#
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
StatusBox.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
status: PropTypes.string,
|
||||
};
|
||||
|
||||
export default StatusBox;
|
||||
@@ -1,46 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Stack } from "@mui/material";
|
||||
import StatusBox from "./StatusBox.jsx";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SkeletonLayout from "./skeleton.jsx";
|
||||
|
||||
const StatusBoxes = ({ isLoading, summary }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
if (isLoading) return <SkeletonLayout shouldRender={isLoading} />;
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(12)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<StatusBox
|
||||
title={t("incidentsOptionsHeaderTotalIncidents")}
|
||||
value={summary?.totalChecks + summary?.cannotResolveChecks || 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title={t("incidentsOptionsHeaderFilterResolved")}
|
||||
status="up"
|
||||
value={summary?.resolvedChecks || 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title={t("incidentsOptionsHeaderFilterCannotResolve")}
|
||||
status="paused"
|
||||
value={summary?.cannotResolveChecks || 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title={t("incidentsOptionsHeaderFilterDown")}
|
||||
status="down"
|
||||
value={summary?.downChecks || 0}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
StatusBoxes.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
summary: PropTypes.object,
|
||||
};
|
||||
|
||||
export default StatusBoxes;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Skeleton, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(12)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -1,100 +0,0 @@
|
||||
// Components
|
||||
import { Stack } from "@mui/material";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import IncidentTable from "./Components/IncidentTable/index.jsx";
|
||||
import OptionsHeader from "./Components/OptionsHeader/index.jsx";
|
||||
import StatusBoxes from "./Components/StatusBoxes/index.jsx";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useFetchMonitorsByTeamId } from "@/Hooks/monitorHooks.js";
|
||||
import { useFetchChecksSummaryByTeamId } from "@/Hooks/checkHooks.js";
|
||||
import { useState, useEffect } from "react";
|
||||
import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
//Constants
|
||||
const Checks = () => {
|
||||
// Redux state
|
||||
const { t } = useTranslation();
|
||||
|
||||
const BREADCRUMBS = [{ name: t("checksPageTitle"), path: "/checks" }];
|
||||
|
||||
// Local state
|
||||
const [selectedMonitor, setSelectedMonitor] = useState("0");
|
||||
const [filter, setFilter] = useState(undefined);
|
||||
const [dateRange, setDateRange] = useState("hour");
|
||||
const [monitorLookup, setMonitorLookup] = useState(undefined);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
|
||||
//Hooks
|
||||
|
||||
//Utils
|
||||
const theme = useTheme();
|
||||
const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({});
|
||||
const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({
|
||||
dateRange,
|
||||
updateTrigger,
|
||||
});
|
||||
const { monitorId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (monitorId) {
|
||||
setSelectedMonitor(monitorId);
|
||||
}
|
||||
}, [monitorId]);
|
||||
|
||||
useEffect(() => {
|
||||
const monitorLookup = monitors?.reduce((acc, monitor) => {
|
||||
acc[monitor.id] = {
|
||||
id: monitor.id,
|
||||
name: monitor.name,
|
||||
type: monitor.type,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
setMonitorLookup(monitorLookup);
|
||||
}, [monitors]);
|
||||
|
||||
if (networkError || networkErrorSummary) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<NetworkError />
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
|
||||
<StatusBoxes
|
||||
isLoading={isLoadingSummary}
|
||||
summary={summary}
|
||||
/>
|
||||
<OptionsHeader
|
||||
shouldRender={!isLoading}
|
||||
monitors={monitorLookup}
|
||||
selectedMonitor={selectedMonitor}
|
||||
setSelectedMonitor={setSelectedMonitor}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
<IncidentTable
|
||||
isLoading={isLoading}
|
||||
monitors={monitorLookup ? monitorLookup : {}}
|
||||
selectedMonitor={selectedMonitor}
|
||||
filter={filter}
|
||||
dateRange={dateRange}
|
||||
updateTrigger={updateTrigger}
|
||||
setUpdateTrigger={setUpdateTrigger}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checks;
|
||||
@@ -1,37 +0,0 @@
|
||||
// Components
|
||||
import { Typography } from "@mui/material";
|
||||
import BaseContainer from "../BaseContainer/index.jsx";
|
||||
import AreaChart from "@/Components/v1/Charts/AreaChart/index.jsx";
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
|
||||
const InfraAreaChart = ({ config }) => {
|
||||
const theme = useTheme();
|
||||
const { getDimensions } = useHardwareUtils();
|
||||
return (
|
||||
<BaseContainer>
|
||||
<Typography
|
||||
component="h2"
|
||||
padding={theme.spacing(8)}
|
||||
>
|
||||
{config.heading}
|
||||
</Typography>
|
||||
<AreaChart
|
||||
height={getDimensions().areaChartHeight}
|
||||
data={config.data}
|
||||
dataKeys={config.dataKeys}
|
||||
xKey="_id"
|
||||
yDomain={config.yDomain}
|
||||
customTooltip={config.toolTip}
|
||||
xTick={config.xTick}
|
||||
yTick={config.yTick}
|
||||
strokeColor={config.strokeColor}
|
||||
gradient={true}
|
||||
gradientStartColor={config.gradientStartColor}
|
||||
gradientEndColor="#ffffff"
|
||||
/>
|
||||
</BaseContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfraAreaChart;
|
||||
@@ -1,140 +0,0 @@
|
||||
// Components
|
||||
import { Stack } from "@mui/material";
|
||||
import InfraAreaChart from "./InfraAreaChart.jsx";
|
||||
import SkeletonLayout from "./skeleton.jsx";
|
||||
|
||||
// Utils
|
||||
import {
|
||||
PercentTick,
|
||||
TzTick,
|
||||
InfrastructureTooltip,
|
||||
TemperatureTooltip,
|
||||
} from "@/Components/v1/Charts/Utils/chartUtils.jsx";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const AreaChartBoxes = ({ shouldRender, monitor, dateRange }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { buildTemps } = useHardwareUtils();
|
||||
|
||||
if (!shouldRender) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
const { stats } = monitor ?? {};
|
||||
const { checks } = stats;
|
||||
|
||||
let latestCheck = checks[0];
|
||||
const { temps, tempKeys } = buildTemps(checks);
|
||||
|
||||
const configs = [
|
||||
{
|
||||
type: "memory",
|
||||
data: checks,
|
||||
dataKeys: ["avgMemoryUsage"],
|
||||
heading: t("memoryUsage"),
|
||||
strokeColor: theme.palette.accent.main, // CAIO_REVIEW
|
||||
gradientStartColor: theme.palette.accent.main, // CAIO_REVIEW
|
||||
yLabel: t("memoryUsage"),
|
||||
yDomain: [0, 1],
|
||||
yTick: <PercentTick />,
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.primary.main}
|
||||
yKey={"avgMemoryUsage"}
|
||||
yLabel={"Memory usage"}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "cpu",
|
||||
data: checks,
|
||||
dataKeys: ["avgCpuUsage"],
|
||||
heading: t("cpuUsage"),
|
||||
strokeColor: theme.palette.success.main,
|
||||
gradientStartColor: theme.palette.success.main,
|
||||
yLabel: t("cpuUsage"),
|
||||
yDomain: [0, 1],
|
||||
yTick: <PercentTick />,
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.success.main}
|
||||
yKey={"avgCpuUsage"}
|
||||
yLabel={"CPU usage"}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "temperature",
|
||||
data: temps,
|
||||
dataKeys: tempKeys,
|
||||
strokeColor: theme.palette.error.main,
|
||||
gradientStartColor: theme.palette.error.main,
|
||||
heading: t("cpuTemperature"),
|
||||
yLabel: "Temperature",
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
yDomain: [
|
||||
0,
|
||||
Math.max(Math.max(...temps.flatMap((t) => tempKeys.map((k) => t[k]))) * 1.1, 200),
|
||||
],
|
||||
toolTip: (
|
||||
<TemperatureTooltip
|
||||
keys={tempKeys}
|
||||
dotColor={theme.palette.error.main}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(latestCheck?.disks?.map((disk, idx) => ({
|
||||
type: "disk",
|
||||
data: checks,
|
||||
diskIndex: idx,
|
||||
dataKeys: [`disks[${idx}].usagePercent`],
|
||||
heading: `Disk${idx} usage`,
|
||||
strokeColor: theme.palette.warning.main,
|
||||
gradientStartColor: theme.palette.warning.main,
|
||||
yLabel: t("diskUsage"),
|
||||
yDomain: [0, 1],
|
||||
yTick: <PercentTick />,
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.warning.main}
|
||||
yKey={`disks.usagePercent`}
|
||||
yLabel={"Disc usage"}
|
||||
yIdx={idx}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
),
|
||||
})) || []),
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
// height={chartContainerHeight} // FE team HELP! Possibly no longer needed?
|
||||
gap={theme.spacing(8)} // FE team HELP!
|
||||
flexWrap="wrap" // //FE team HELP! Better way to do this?
|
||||
sx={{
|
||||
"& > *": {
|
||||
flexBasis: `calc(50% - ${theme.spacing(8)})`,
|
||||
maxWidth: `calc(50% - ${theme.spacing(8)})`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{configs.map((config) => (
|
||||
<InfraAreaChart
|
||||
key={`${config.type}-${config.diskIndex ?? ""}`}
|
||||
config={config}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AreaChartBoxes;
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
flexWrap="wrap"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<Skeleton
|
||||
height={"33vh"}
|
||||
sx={{
|
||||
flex: 1,
|
||||
}}
|
||||
/>
|
||||
<Skeleton
|
||||
height={"33vh"}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Renders a base box with consistent styling
|
||||
* @param {Object} props - Component properties
|
||||
* @param {React.ReactNode} props.children - Child components to render inside the box
|
||||
* @param {Object} props.sx - Additional styling for the box
|
||||
* @returns {React.ReactElement} Styled box component
|
||||
*/
|
||||
|
||||
// Components
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const BaseContainer = ({ children, sx = {}, shouldExpand = false }) => {
|
||||
const theme = useTheme();
|
||||
const { getDimensions } = useHardwareUtils();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
padding: `${theme.spacing(getDimensions().baseBoxPaddingVertical)} ${theme.spacing(getDimensions().baseBoxPaddingHorizontal)}`,
|
||||
minWidth: 200,
|
||||
width: shouldExpand ? "100%" : 225,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
BaseContainer.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
sx: PropTypes.object,
|
||||
shouldExpand: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default BaseContainer;
|
||||
@@ -0,0 +1,104 @@
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { HistogramInfrastructure } from "@/Components/v2/monitors";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { HardwareCheckStats } from "@/Types/Monitor";
|
||||
import { useMemo } from "react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
const formatPercent = (value: number) => `${(value * 100).toFixed(0)}%`;
|
||||
const formatTemp = (value: number) => `${value.toFixed(0)}°C`;
|
||||
|
||||
const getChartConfigs = (theme: any, checks: HardwareCheckStats[], t: any) => {
|
||||
return [
|
||||
{
|
||||
title: t("pages.infrastructure.charts.labels.memory"),
|
||||
type: "memory",
|
||||
dataKeys: ["avgMemoryUsage"],
|
||||
strokeColor: theme.palette.primary.main,
|
||||
gradientStartColor: theme.palette.primary.main,
|
||||
yDomain: [0, 1],
|
||||
yAxisFormatter: formatPercent,
|
||||
idx: null,
|
||||
},
|
||||
{
|
||||
title: t("pages.infrastructure.charts.labels.cpu"),
|
||||
type: "cpu",
|
||||
dataKeys: ["avgCpuUsage"],
|
||||
strokeColor: theme.palette.success.main,
|
||||
gradientStartColor: theme.palette.success.main,
|
||||
yDomain: [0, 1],
|
||||
yAxisFormatter: formatPercent,
|
||||
idx: null,
|
||||
},
|
||||
{
|
||||
title: t("pages.infrastructure.charts.labels.temp"),
|
||||
type: "temp",
|
||||
dataKeys: ["avg_temp"],
|
||||
strokeColor: theme.palette.error.main,
|
||||
gradientStartColor: theme.palette.error.main,
|
||||
yDomain: [0, 150],
|
||||
yAxisFormatter: formatTemp,
|
||||
idx: null,
|
||||
},
|
||||
...(checks[0]?.disks?.map((_, idx) => ({
|
||||
title: t("pages.infrastructure.charts.labels.disk", { idx }),
|
||||
type: "disk",
|
||||
dataKeys: [`disks[${idx}].usagePercent`],
|
||||
strokeColor: theme.palette.warning.main,
|
||||
gradientStartColor: theme.palette.warning.main,
|
||||
yDomain: [0, 1],
|
||||
yAxisFormatter: formatPercent,
|
||||
idx,
|
||||
})) || []),
|
||||
];
|
||||
};
|
||||
|
||||
export const InfraDetailsCharts = ({
|
||||
checks,
|
||||
dateRange,
|
||||
}: {
|
||||
checks: HardwareCheckStats[];
|
||||
dateRange: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const { t } = useTranslation();
|
||||
const chartConfigs = useMemo(
|
||||
() => getChartConfigs(theme, checks, t),
|
||||
[theme, checks, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={theme.spacing(8)}
|
||||
>
|
||||
{chartConfigs.map((config) => {
|
||||
return (
|
||||
<Grid
|
||||
size={isSmall ? 12 : 6}
|
||||
key={`${config.type}-${config.idx ?? ""}`}
|
||||
>
|
||||
<HistogramInfrastructure
|
||||
dateRange={dateRange}
|
||||
title={config.title}
|
||||
type={config.type}
|
||||
idx={config.idx}
|
||||
key={`${config.type}-${config.idx ?? ""}`}
|
||||
checks={checks}
|
||||
xKey="bucketDate"
|
||||
yDomain={config.yDomain}
|
||||
dataKeys={config.dataKeys}
|
||||
gradient={true}
|
||||
gradientStartColor={config.gradientStartColor}
|
||||
gradientEndColor="#ffffff"
|
||||
strokeColor={config.strokeColor}
|
||||
yAxisFormatter={config.yAxisFormatter}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { HistogramInfrastructure } from "@/Components/v2/monitors";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { HardwareCheckStats } from "@/Types/Monitor";
|
||||
import { useMemo } from "react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
|
||||
const formatBytesToMB = (value: number) => `${(value / (1024 * 1024)).toFixed(2)} MB`;
|
||||
|
||||
interface ChartConfig {
|
||||
title: string;
|
||||
type: string;
|
||||
dataKeys: string[];
|
||||
strokeColor: string;
|
||||
gradientStartColor: string;
|
||||
idx: number | null;
|
||||
interfaceName?: string;
|
||||
}
|
||||
|
||||
const getChartConfigs = (
|
||||
theme: any,
|
||||
checks: HardwareCheckStats[],
|
||||
t: any
|
||||
): ChartConfig[] => {
|
||||
const configs: ChartConfig[] = [];
|
||||
|
||||
// Find the first check that has network data to get interface names
|
||||
const checkWithNet = checks.find((c) => c.net && c.net.length > 0);
|
||||
const netInterfaces = checkWithNet?.net || [];
|
||||
|
||||
netInterfaces.forEach((iface, idx) => {
|
||||
configs.push(
|
||||
{
|
||||
title: t("pages.infrastructure.charts.labels.netBytesSent", {
|
||||
name: iface.name,
|
||||
}),
|
||||
type: "netBytesSent",
|
||||
dataKeys: [`net[${idx}].bytesSentPerSecond`],
|
||||
strokeColor: theme.palette.primary.main,
|
||||
gradientStartColor: theme.palette.primary.main,
|
||||
idx,
|
||||
interfaceName: iface.name,
|
||||
},
|
||||
{
|
||||
title: t("pages.infrastructure.charts.labels.netBytesRecv", {
|
||||
name: iface.name,
|
||||
}),
|
||||
type: "netBytesRecv",
|
||||
dataKeys: [`net[${idx}].deltaBytesRecv`],
|
||||
strokeColor: theme.palette.success.main,
|
||||
gradientStartColor: theme.palette.success.main,
|
||||
idx,
|
||||
interfaceName: iface.name,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return configs;
|
||||
};
|
||||
|
||||
export const InfraNetworkCharts = ({
|
||||
checks,
|
||||
dateRange,
|
||||
}: {
|
||||
checks: HardwareCheckStats[];
|
||||
dateRange: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const { t } = useTranslation();
|
||||
const chartConfigs = useMemo(
|
||||
() => getChartConfigs(theme, checks, t),
|
||||
[theme, checks, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={theme.spacing(8)}
|
||||
>
|
||||
{chartConfigs.map((config) => {
|
||||
return (
|
||||
<Grid
|
||||
size={isSmall ? 12 : 6}
|
||||
key={`${config.type}-${config.interfaceName ?? config.idx ?? ""}`}
|
||||
>
|
||||
<HistogramInfrastructure
|
||||
dateRange={dateRange}
|
||||
title={config.title}
|
||||
type={config.type}
|
||||
idx={config.idx}
|
||||
checks={checks}
|
||||
xKey="bucketDate"
|
||||
dataKeys={config.dataKeys}
|
||||
gradient={true}
|
||||
gradientStartColor={config.gradientStartColor}
|
||||
gradientEndColor="#ffffff"
|
||||
strokeColor={config.strokeColor}
|
||||
yAxisFormatter={formatBytesToMB}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -1,127 +0,0 @@
|
||||
// Components
|
||||
import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx";
|
||||
import BaseContainer from "../BaseContainer/index.jsx";
|
||||
import { Stack, Typography, Box } from "@mui/material";
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Gauge = ({
|
||||
value,
|
||||
heading,
|
||||
metricOne,
|
||||
valueOne,
|
||||
metricTwo,
|
||||
valueTwo,
|
||||
metricThree,
|
||||
valueThree,
|
||||
metricFour,
|
||||
valueFour,
|
||||
shouldExpand = false,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const valueStyle = {
|
||||
borderRadius: theme.spacing(2),
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
minWidth: "40%",
|
||||
maxWidth: "60%",
|
||||
mb: theme.spacing(2),
|
||||
mt: theme.spacing(2),
|
||||
pr: theme.spacing(2),
|
||||
textAlign: "right",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseContainer shouldExpand={shouldExpand}>
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(2)}
|
||||
alignItems="center"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
backgroundColor: theme.palette.gradient.color1,
|
||||
}}
|
||||
>
|
||||
<CustomGauge
|
||||
progress={value}
|
||||
radius={100}
|
||||
/>
|
||||
<Typography
|
||||
component="h2"
|
||||
sx={{ fontWeight: 600 }}
|
||||
>
|
||||
{heading}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
borderTop: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
justifyContent={"space-between"}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Typography>{metricOne}</Typography>
|
||||
<Typography sx={valueStyle}>{valueOne}</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
justifyContent={"space-between"}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Typography>{metricTwo}</Typography>
|
||||
<Typography sx={valueStyle}>{valueTwo}</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
justifyContent={"space-between"}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Typography>{metricThree}</Typography>
|
||||
<Typography sx={valueStyle}>{valueThree}</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
justifyContent={"space-between"}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Typography>{metricFour}</Typography>
|
||||
<Typography sx={valueStyle}>{valueFour}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</BaseContainer>
|
||||
);
|
||||
};
|
||||
|
||||
Gauge.propTypes = {
|
||||
value: PropTypes.number,
|
||||
heading: PropTypes.string,
|
||||
metricOne: PropTypes.string,
|
||||
valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
metricTwo: PropTypes.string,
|
||||
valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
metricThree: PropTypes.string,
|
||||
valueThree: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
metricFour: PropTypes.string,
|
||||
valueFour: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
shouldExpand: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Gauge;
|
||||
@@ -1,112 +0,0 @@
|
||||
// Components
|
||||
import { Box } from "@mui/material";
|
||||
import Gauge from "./Gauge.jsx";
|
||||
import SkeletonLayout from "./skeleton.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
// Utils
|
||||
import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Gauges = ({ isLoading = false, monitor }) => {
|
||||
const { decimalToPercentage, formatBytes, formatDeviceName, formatMountpoint } =
|
||||
useHardwareUtils();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (isLoading) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
const latestCheck = monitor?.recentChecks?.[0];
|
||||
const memoryUsagePercent = latestCheck?.memory?.usage_percent ?? 0;
|
||||
const memoryUsedBytes = latestCheck?.memory?.used_bytes ?? 0;
|
||||
const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0;
|
||||
const cpuUsagePercent = latestCheck?.cpu?.usage_percent ?? 0;
|
||||
const cpuPhysicalCores = latestCheck?.cpu?.physical_core ?? 0;
|
||||
const cpuFrequency = latestCheck?.cpu?.frequency ?? 0;
|
||||
|
||||
const gauges = [
|
||||
{
|
||||
type: "memory",
|
||||
value: decimalToPercentage(memoryUsagePercent),
|
||||
heading: t("memoryUsage"),
|
||||
metricOne: t("used"),
|
||||
valueOne: formatBytes(memoryUsedBytes, true),
|
||||
metricTwo: t("total"),
|
||||
valueTwo: formatBytes(memoryTotalBytes, true),
|
||||
},
|
||||
{
|
||||
type: "cpu",
|
||||
value: decimalToPercentage(cpuUsagePercent),
|
||||
heading: t("cpuUsage"),
|
||||
metricOne: t("cores"),
|
||||
valueOne: cpuPhysicalCores ?? 0,
|
||||
metricTwo: t("frequency"),
|
||||
valueTwo: `${(cpuFrequency / 1000).toFixed(2)} Ghz`,
|
||||
},
|
||||
...(latestCheck?.disk ?? [])
|
||||
.filter((disk) => {
|
||||
if (!monitor?.selectedDisks || monitor.selectedDisks.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return monitor.selectedDisks.includes(disk.mountpoint || disk.device);
|
||||
})
|
||||
.map((disk, idx) => ({
|
||||
type: "disk",
|
||||
diskIndex: idx,
|
||||
value: decimalToPercentage(disk.usage_percent),
|
||||
heading: `Disk${idx} usage`,
|
||||
metricOne: t("used"),
|
||||
valueOne: formatBytes(disk.total_bytes - disk.free_bytes, true),
|
||||
metricTwo: t("total"),
|
||||
valueTwo: formatBytes(disk.total_bytes, true),
|
||||
metricThree: t("device"),
|
||||
valueThree: formatDeviceName(disk.device),
|
||||
metricFour: t("mountedOn"),
|
||||
valueFour: formatMountpoint(disk.mountpoint),
|
||||
})),
|
||||
];
|
||||
|
||||
// Only expand gauges to fill row when there are 4 or more
|
||||
const shouldExpand = gauges.length >= 4;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: shouldExpand
|
||||
? "repeat(auto-fill, minmax(200px, 1fr))"
|
||||
: "repeat(auto-fill, 225px)",
|
||||
gap: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{gauges.map((gauge) => {
|
||||
return (
|
||||
<Gauge
|
||||
key={`${gauge.type}-${gauge.diskIndex ?? ""}`}
|
||||
value={gauge.value}
|
||||
heading={gauge.heading}
|
||||
metricOne={gauge.metricOne}
|
||||
valueOne={gauge.valueOne}
|
||||
metricTwo={gauge.metricTwo}
|
||||
valueTwo={gauge.valueTwo}
|
||||
metricThree={gauge.metricThree}
|
||||
valueThree={gauge.valueThree}
|
||||
metricFour={gauge.metricFour}
|
||||
valueFour={gauge.valueFour}
|
||||
shouldExpand={shouldExpand}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Gauges.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
monitor: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Gauges;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
{Array.from({ length: 3 }).map((_, idx) => {
|
||||
return (
|
||||
<Skeleton
|
||||
key={`gauge-${idx}`}
|
||||
variant="rectangular"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -0,0 +1,109 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { BaseChart, Gauge } from "@/Components/v2/design-elements";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getGbs, getFrequency } from "@/Utils/InfraUtils";
|
||||
import { useTheme } from "@mui/material";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import type { CheckSnapshot } from "@/Types/Check";
|
||||
|
||||
const InfraDetailGauge = ({
|
||||
title,
|
||||
progress,
|
||||
upperLabel,
|
||||
upperValue,
|
||||
lowerLabel,
|
||||
lowerValue,
|
||||
}: {
|
||||
title: string;
|
||||
progress: number;
|
||||
upperLabel?: string;
|
||||
upperValue?: string | number;
|
||||
lowerLabel?: string;
|
||||
lowerValue?: string | number;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BaseChart
|
||||
icon={null}
|
||||
title={title}
|
||||
maxWidth={225}
|
||||
>
|
||||
<Stack
|
||||
alignItems={"center"}
|
||||
mb={theme.spacing(4)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Gauge progress={progress} />
|
||||
</Stack>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<Typography>{upperLabel}</Typography>
|
||||
<Typography>{upperValue}</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<Typography>{lowerLabel}</Typography>
|
||||
<Typography>{lowerValue}</Typography>
|
||||
</Stack>
|
||||
</BaseChart>
|
||||
);
|
||||
};
|
||||
|
||||
export const InfraDetailsGauges = ({
|
||||
snapshot,
|
||||
}: {
|
||||
snapshot: CheckSnapshot | undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
if (!snapshot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
spacing={theme.spacing(8)}
|
||||
alignItems={"center"}
|
||||
>
|
||||
<InfraDetailGauge
|
||||
title={t("pages.infrastructure.gauges.memory.title")}
|
||||
progress={(snapshot?.memory?.usage_percent || 0) * 100}
|
||||
upperLabel={t("pages.infrastructure.gauges.memory.upperLabel")}
|
||||
upperValue={getGbs(snapshot?.memory?.used_bytes || 0)}
|
||||
lowerLabel={t("pages.infrastructure.gauges.memory.lowerLabel")}
|
||||
lowerValue={getGbs(snapshot?.memory?.total_bytes || 0)}
|
||||
/>
|
||||
<InfraDetailGauge
|
||||
title={t("pages.infrastructure.gauges.cpu.title")}
|
||||
progress={(snapshot?.cpu?.usage_percent || 0) * 100}
|
||||
upperLabel={t("pages.infrastructure.gauges.cpu.upperLabel")}
|
||||
upperValue={getFrequency(snapshot?.cpu?.current_frequency || 0)}
|
||||
lowerLabel={t("pages.infrastructure.gauges.cpu.lowerLabel")}
|
||||
lowerValue={getFrequency(snapshot?.cpu?.frequency || 0)}
|
||||
/>
|
||||
{snapshot?.disk?.map((disk, idx) => {
|
||||
return (
|
||||
<InfraDetailGauge
|
||||
key={disk?.device || 0 + idx}
|
||||
// title={`Disk ${idx} usage`}
|
||||
title={t("pages.infrastructure.gauges.disk.title", { idx })}
|
||||
progress={(disk.usage_percent || 0) * 100}
|
||||
upperLabel={t("pages.infrastructure.gauges.disk.upperLabel")}
|
||||
upperValue={getGbs(disk?.used_bytes || 0)}
|
||||
lowerLabel={t("pages.infrastructure.gauges.disk.lowerLabel")}
|
||||
lowerValue={getGbs(disk?.total_bytes || 0)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,132 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import InfraAreaChart from "../AreaChartBoxes/InfraAreaChart.jsx";
|
||||
|
||||
import {
|
||||
TzTick,
|
||||
InfrastructureTooltip,
|
||||
NetworkTick,
|
||||
} from "@/Components/v1/Charts/Utils/chartUtils.jsx";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
|
||||
|
||||
const NetworkCharts = ({ ethernetData, dateRange }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { formatBytesPerSecondString, formatPacketsPerSecondString } = useHardwareUtils();
|
||||
|
||||
if (!ethernetData?.length) {
|
||||
return <Typography>{t("noNetworkStatsAvailable")}</Typography>;
|
||||
}
|
||||
|
||||
const configs = [
|
||||
{
|
||||
type: "network-bytes",
|
||||
data: ethernetData,
|
||||
dataKeys: ["bytesPerSec"],
|
||||
heading: t("dataReceived"),
|
||||
strokeColor: theme.palette.info.main,
|
||||
gradientStartColor: theme.palette.info.main,
|
||||
yLabel: t("rate"),
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
yTick: <NetworkTick formatter={formatBytesPerSecondString} />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.info.main}
|
||||
yKey={"bytesPerSec"}
|
||||
yLabel={t("dataRate") + ": "}
|
||||
dateRange={dateRange}
|
||||
formatter={formatBytesPerSecondString}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "network-packets",
|
||||
data: ethernetData,
|
||||
dataKeys: ["packetsPerSec"],
|
||||
heading: t("packetsReceivedRate"),
|
||||
strokeColor: theme.palette.success.main,
|
||||
gradientStartColor: theme.palette.success.main,
|
||||
yLabel: t("rate"),
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
yTick: <NetworkTick formatter={formatPacketsPerSecondString} />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.success.main}
|
||||
yKey={"packetsPerSec"}
|
||||
yLabel={t("packetsPerSecond") + ": "}
|
||||
dateRange={dateRange}
|
||||
formatter={formatPacketsPerSecondString}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "network-errors",
|
||||
data: ethernetData,
|
||||
dataKeys: ["errors"],
|
||||
heading: t("networkErrors"),
|
||||
strokeColor: theme.palette.error.main,
|
||||
gradientStartColor: theme.palette.error.main,
|
||||
yLabel: t("errors"),
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.error.main}
|
||||
yKey={"errors"}
|
||||
yLabel={t("errors") + ": "}
|
||||
dateRange={dateRange}
|
||||
formatter={(value) => Math.round(value).toLocaleString()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "network-drops",
|
||||
data: ethernetData,
|
||||
dataKeys: ["drops"],
|
||||
heading: t("networkDrops"),
|
||||
strokeColor: theme.palette.warning.main,
|
||||
gradientStartColor: theme.palette.warning.main,
|
||||
yLabel: t("drops"),
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.warning.main}
|
||||
yKey={"drops"}
|
||||
yLabel={t("drops") + ": "}
|
||||
dateRange={dateRange}
|
||||
formatter={(value) => Math.round(value).toLocaleString()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
gap={theme.spacing(8)}
|
||||
flexWrap="wrap"
|
||||
sx={{
|
||||
"& > *": {
|
||||
flexBasis: `calc(50% - ${theme.spacing(8)})`,
|
||||
maxWidth: `calc(50% - ${theme.spacing(8)})`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{configs.map((config) => (
|
||||
<InfraAreaChart
|
||||
key={config.type}
|
||||
config={config}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
NetworkCharts.propTypes = {
|
||||
ethernetData: PropTypes.array.isRequired,
|
||||
dateRange: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default NetworkCharts;
|
||||
@@ -1,81 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
|
||||
import StatBox from "@/Components/v1/StatBox/index.jsx";
|
||||
import { Typography } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
|
||||
|
||||
function formatNumber(num) {
|
||||
return num != null ? num.toLocaleString() : "0";
|
||||
}
|
||||
|
||||
const NetworkStatBoxes = ({ shouldRender, net, ifaceName }) => {
|
||||
const { t } = useTranslation();
|
||||
const { formatBytes } = useHardwareUtils();
|
||||
|
||||
const filtered = net?.filter((iface) => iface.name === ifaceName) || [];
|
||||
|
||||
if (!net?.length) {
|
||||
return <Typography>{t("noNetworkStatsAvailable")}</Typography>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StatusBoxes
|
||||
shouldRender={shouldRender}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
{filtered
|
||||
.map((iface) => [
|
||||
<StatBox
|
||||
key={`${iface.name}-bytes-sent`}
|
||||
heading={t("bytesSent")}
|
||||
subHeading={formatBytes(iface.bytes_sent)}
|
||||
/>,
|
||||
<StatBox
|
||||
key={`${iface.name}-bytes-recv`}
|
||||
heading={t("bytesReceived")}
|
||||
subHeading={formatBytes(iface.bytes_recv)}
|
||||
/>,
|
||||
<StatBox
|
||||
key={`${iface.name}-packets-sent`}
|
||||
heading={t("packetsSent")}
|
||||
subHeading={formatNumber(iface.packets_sent)}
|
||||
/>,
|
||||
<StatBox
|
||||
key={`${iface.name}-packets-recv`}
|
||||
heading={t("packetsReceived")}
|
||||
subHeading={formatNumber(iface.packets_recv)}
|
||||
/>,
|
||||
<StatBox
|
||||
key={`${iface.name}-err-in`}
|
||||
heading={t("errorsIn")}
|
||||
subHeading={formatNumber(iface.err_in)}
|
||||
/>,
|
||||
<StatBox
|
||||
key={`${iface.name}-err-out`}
|
||||
heading={t("errorsOut")}
|
||||
subHeading={formatNumber(iface.err_out)}
|
||||
/>,
|
||||
])
|
||||
.flat()}
|
||||
</StatusBoxes>
|
||||
);
|
||||
};
|
||||
|
||||
NetworkStatBoxes.propTypes = {
|
||||
shouldRender: PropTypes.bool.isRequired,
|
||||
net: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
bytes_sent: PropTypes.number,
|
||||
bytes_recv: PropTypes.number,
|
||||
packets_sent: PropTypes.number,
|
||||
packets_recv: PropTypes.number,
|
||||
err_in: PropTypes.number,
|
||||
err_out: PropTypes.number,
|
||||
})
|
||||
),
|
||||
ifaceName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default NetworkStatBoxes;
|
||||
@@ -1,101 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Select from "@/Components/v1/Inputs/Select/index.jsx";
|
||||
import NetworkStatBoxes from "./NetworkStatBoxes.jsx";
|
||||
import NetworkCharts from "./NetworkCharts.jsx";
|
||||
import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx";
|
||||
|
||||
const getAvailableInterfaces = (net) => {
|
||||
return (net || []).map((iface) => iface.name).filter(Boolean);
|
||||
};
|
||||
|
||||
const getNetworkInterfaceData = (checks, ifaceName) => {
|
||||
if (!ifaceName) return [];
|
||||
|
||||
// Transform backend data structure for the selected interface
|
||||
// Backend already calculates deltas, we just reshape the data
|
||||
return (checks || [])
|
||||
.map((check) => {
|
||||
const networkInterface = (check.net || []).find(
|
||||
(iface) => iface.name === ifaceName
|
||||
);
|
||||
if (!networkInterface) return null;
|
||||
return {
|
||||
_id: check._id,
|
||||
bytesPerSec: networkInterface.deltaBytesRecv,
|
||||
packetsPerSec: networkInterface.deltaPacketsRecv,
|
||||
errors: networkInterface.deltaErrOut ?? 0,
|
||||
drops: networkInterface.deltaDropOut ?? 0,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const availableInterfaces = getAvailableInterfaces(net);
|
||||
const [selectedInterface, setSelectedInterface] = useState("");
|
||||
|
||||
// Set default interface when data loads
|
||||
useEffect(() => {
|
||||
if (availableInterfaces.length > 0 && !selectedInterface) {
|
||||
setSelectedInterface(availableInterfaces[0]);
|
||||
}
|
||||
}, [availableInterfaces, selectedInterface]);
|
||||
|
||||
const ethernetData = getNetworkInterfaceData(checks, selectedInterface);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NetworkStatBoxes
|
||||
shouldRender={!isLoading}
|
||||
net={net}
|
||||
ifaceName={selectedInterface}
|
||||
/>
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-end"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
{availableInterfaces.length > 0 && (
|
||||
<Select
|
||||
name="networkInterface"
|
||||
label={t("networkInterface")}
|
||||
value={selectedInterface}
|
||||
onChange={(e) => setSelectedInterface(e.target.value)}
|
||||
items={availableInterfaces.map((interfaceName) => ({
|
||||
_id: interfaceName,
|
||||
name: interfaceName,
|
||||
}))}
|
||||
sx={{ minWidth: 200 }}
|
||||
/>
|
||||
)}
|
||||
<MonitorTimeFrameHeader
|
||||
isLoading={isLoading}
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
</Box>
|
||||
<NetworkCharts
|
||||
ethernetData={ethernetData}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Network.propTypes = {
|
||||
net: PropTypes.array,
|
||||
checks: PropTypes.array,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
dateRange: PropTypes.string.isRequired,
|
||||
setDateRange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Network;
|
||||
@@ -1,56 +0,0 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
} from "@mui/material";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Skeleton
|
||||
variant="text"
|
||||
width={180}
|
||||
height={32}
|
||||
/>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Bytes Sent</TableCell>
|
||||
<TableCell>Bytes Received</TableCell>
|
||||
<TableCell>Packets Sent</TableCell>
|
||||
<TableCell>Packets Received</TableCell>
|
||||
<TableCell>Errors In</TableCell>
|
||||
<TableCell>Errors Out</TableCell>
|
||||
<TableCell>Drops In</TableCell>
|
||||
<TableCell>Drops Out</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Array.from({ length: 5 }).map((_, idx) => (
|
||||
<TableRow key={idx}>
|
||||
{Array.from({ length: 9 }).map((__, colIdx) => (
|
||||
<TableCell key={colIdx}>
|
||||
<Skeleton
|
||||
variant="text"
|
||||
width={80}
|
||||
height={24}
|
||||
/>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -0,0 +1,73 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { StatBox } from "@/Components/v2/design-elements";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import {
|
||||
getAvgTemp,
|
||||
getCores,
|
||||
getFrequency,
|
||||
getGbs,
|
||||
getDiskTotalGbs,
|
||||
getOsAndPlatform,
|
||||
} from "@/Utils/InfraUtils";
|
||||
|
||||
export const StatusBoxes = ({ monitor }: { monitor: Monitor }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const latestCheck = monitor?.recentChecks?.[0];
|
||||
// Get data from latest check
|
||||
const physicalCores = getCores(latestCheck?.cpu?.physical_core);
|
||||
const logicalCores = getCores(latestCheck?.cpu?.logical_core);
|
||||
const cpuFrequency = getFrequency(latestCheck?.cpu?.frequency);
|
||||
const cpuTemps = latestCheck?.cpu?.temperature ?? [];
|
||||
const cpuTemperature = getAvgTemp(cpuTemps);
|
||||
const memoryTotalBytes = getGbs(latestCheck?.memory?.total_bytes);
|
||||
const diskTotalBytes = getDiskTotalGbs(latestCheck?.disk);
|
||||
const os = getOsAndPlatform(latestCheck?.host);
|
||||
|
||||
const platform = latestCheck?.host?.platform ?? undefined;
|
||||
const osPlatform =
|
||||
typeof os === "undefined" && typeof platform === "undefined"
|
||||
? undefined
|
||||
: `${os} ${platform}`;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
flexWrap={"wrap"}
|
||||
>
|
||||
<StatBox
|
||||
title={t("pages.infrastructure.statBoxes.cpuPhysical")}
|
||||
subtitle={physicalCores.toString()}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.infrastructure.statBoxes.cpuLogical")}
|
||||
subtitle={logicalCores.toString()}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.infrastructure.statBoxes.cpuFrequency")}
|
||||
subtitle={cpuFrequency}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.infrastructure.statBoxes.avgCpuTemperature")}
|
||||
subtitle={cpuTemperature}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.infrastructure.statBoxes.memory")}
|
||||
subtitle={memoryTotalBytes.toString()}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.infrastructure.statBoxes.disk")}
|
||||
subtitle={diskTotalBytes.toString()}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.infrastructure.statBoxes.os")}
|
||||
subtitle={osPlatform || "N/A"}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,115 +0,0 @@
|
||||
// Components
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
|
||||
import StatBox from "@/Components/v1/StatBox/index.jsx";
|
||||
|
||||
//Utils
|
||||
import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js";
|
||||
import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const InfraStatBoxes = ({ shouldRender, monitor }) => {
|
||||
// Utils
|
||||
const { formatBytes } = useHardwareUtils();
|
||||
const { determineState } = useMonitorUtils();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const latestCheck = monitor?.recentChecks?.[0];
|
||||
|
||||
// Get data from latest check
|
||||
const physicalCores = latestCheck?.cpu?.physical_core ?? 0;
|
||||
const logicalCores = latestCheck?.cpu?.logical_core ?? 0;
|
||||
const cpuFrequency = latestCheck?.cpu?.frequency ?? 0;
|
||||
const cpuTemperature =
|
||||
latestCheck?.cpu?.temperature?.length > 0
|
||||
? latestCheck.cpu.temperature.reduce((acc, curr) => acc + curr, 0) /
|
||||
latestCheck.cpu.temperature.length
|
||||
: 0;
|
||||
const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0;
|
||||
const diskTotalBytes = latestCheck?.disk[0]?.total_bytes ?? 0;
|
||||
const os = latestCheck?.host?.os ?? undefined;
|
||||
const platform = latestCheck?.host?.platform ?? undefined;
|
||||
const osPlatform =
|
||||
typeof os === "undefined" && typeof platform === "undefined"
|
||||
? undefined
|
||||
: `${os} ${platform}`;
|
||||
|
||||
return (
|
||||
<StatusBoxes
|
||||
shouldRender={shouldRender}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<StatBox
|
||||
gradient={true}
|
||||
status={determineState(monitor)}
|
||||
heading={t("status")}
|
||||
subHeading={determineState(monitor)}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("cpuPhysical")}
|
||||
subHeading={
|
||||
<>
|
||||
{physicalCores}
|
||||
<Typography component="span">
|
||||
{physicalCores === 1 ? "core" : "cores"}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StatBox
|
||||
key={2}
|
||||
heading={t("cpuLogical")}
|
||||
subHeading={
|
||||
<>
|
||||
{logicalCores}
|
||||
<Typography component="span">
|
||||
{logicalCores === 1 ? "core" : "cores"}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("cpuFrequency")}
|
||||
subHeading={
|
||||
<>
|
||||
{(cpuFrequency / 1000).toFixed(2)}
|
||||
<Typography component="span">Ghz</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("avgCpuTemperature")}
|
||||
subHeading={
|
||||
<>
|
||||
{cpuTemperature.toFixed(2)}
|
||||
<Typography component="span">°C</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("memory")}
|
||||
subHeading={formatBytes(memoryTotalBytes)}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("disk")}
|
||||
subHeading={formatBytes(diskTotalBytes)}
|
||||
/>
|
||||
{/* <StatBox
|
||||
heading={t("uptime")}
|
||||
subHeading={
|
||||
<>
|
||||
{(uptimePercentage * 100).toFixed(2)}
|
||||
<Typography component="span">%</Typography>
|
||||
</>
|
||||
}
|
||||
/> */}
|
||||
<StatBox
|
||||
key={8}
|
||||
heading={t("os")}
|
||||
subHeading={osPlatform}
|
||||
/>
|
||||
</StatusBoxes>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfraStatBoxes;
|
||||
@@ -0,0 +1,29 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { InfraNetworkCharts } from "./ChartsNetwork";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import type { HardwareStats } from "@/Types/Monitor";
|
||||
|
||||
export const TabNetwork = ({
|
||||
stats,
|
||||
dateRange,
|
||||
}: {
|
||||
stats: HardwareStats | undefined;
|
||||
dateRange: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!stats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checks = stats?.checks || [];
|
||||
return (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<InfraNetworkCharts
|
||||
checks={checks}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { InfraDetailsGauges } from "@/Pages/Infrastructure/Details/Components/Gauges";
|
||||
import { StatusBoxes } from "@/Pages/Infrastructure/Details/Components/StatusBoxes";
|
||||
import { InfraDetailsCharts } from "@/Pages/Infrastructure/Details/Components/Charts";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import type { HardwareStats, Monitor } from "@/Types/Monitor";
|
||||
|
||||
export const TabOverview = ({
|
||||
monitor,
|
||||
stats,
|
||||
dateRange,
|
||||
}: {
|
||||
monitor: Monitor | undefined;
|
||||
stats: HardwareStats | undefined;
|
||||
dateRange: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
if (!monitor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checks = stats?.checks || [];
|
||||
return (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<StatusBoxes monitor={monitor} />
|
||||
<InfraDetailsGauges snapshot={monitor.recentChecks?.[0]} />
|
||||
<InfraDetailsCharts
|
||||
checks={checks}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,270 +0,0 @@
|
||||
import { Typography, Tooltip } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// Constants
|
||||
const BASE_BOX_PADDING_VERTICAL = 4;
|
||||
const BASE_BOX_PADDING_HORIZONTAL = 8;
|
||||
const TYPOGRAPHY_PADDING = 8;
|
||||
const CHART_CONTAINER_HEIGHT = 300;
|
||||
|
||||
const useHardwareUtils = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getDimensions = () => {
|
||||
const totalTypographyPadding = parseInt(theme.spacing(TYPOGRAPHY_PADDING), 10) * 2;
|
||||
const totalChartContainerPadding =
|
||||
parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2;
|
||||
return {
|
||||
baseBoxPaddingVertical: BASE_BOX_PADDING_VERTICAL,
|
||||
baseBoxPaddingHorizontal: BASE_BOX_PADDING_HORIZONTAL,
|
||||
totalContainerPadding: parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2,
|
||||
areaChartHeight:
|
||||
CHART_CONTAINER_HEIGHT - totalChartContainerPadding - totalTypographyPadding,
|
||||
};
|
||||
};
|
||||
|
||||
const formatBytes = (bytes, space = false) => {
|
||||
if (bytes === undefined || bytes === null)
|
||||
return (
|
||||
<>
|
||||
{0}
|
||||
{space ? " " : ""}
|
||||
<Typography component="span">{t("gb")}</Typography>
|
||||
</>
|
||||
);
|
||||
if (typeof bytes !== "number")
|
||||
return (
|
||||
<>
|
||||
{0}
|
||||
{space ? " " : ""}
|
||||
<Typography component="span">{t("gb")}</Typography>
|
||||
</>
|
||||
);
|
||||
if (bytes === 0)
|
||||
return (
|
||||
<>
|
||||
{0}
|
||||
{space ? " " : ""}
|
||||
<Typography component="span">{t("gb")}</Typography>
|
||||
</>
|
||||
);
|
||||
|
||||
const GB = bytes / (1024 * 1024 * 1024);
|
||||
const MB = bytes / (1024 * 1024);
|
||||
|
||||
if (GB >= 1) {
|
||||
return (
|
||||
<>
|
||||
{Number(GB.toFixed(2))}
|
||||
{space ? " " : ""}
|
||||
<Typography component="span">{t("gb")}</Typography>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{Number(MB.toFixed(2))}
|
||||
{space ? " " : ""}
|
||||
<Typography component="span">{t("mb")}</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const formatBytesPerSecondString = (bytesPerSec, space = false) => {
|
||||
if (
|
||||
bytesPerSec === undefined ||
|
||||
bytesPerSec === null ||
|
||||
typeof bytesPerSec !== "number" ||
|
||||
bytesPerSec === 0
|
||||
) {
|
||||
return `0${space ? " " : ""}B/s`;
|
||||
}
|
||||
|
||||
const GB = bytesPerSec / (1024 * 1024 * 1024);
|
||||
const MB = bytesPerSec / (1024 * 1024);
|
||||
const KB = bytesPerSec / 1024;
|
||||
|
||||
if (GB >= 1) {
|
||||
return `${Number(GB.toFixed(1))}${space ? " " : ""}GB/s`;
|
||||
} else if (MB >= 1) {
|
||||
return `${Number(MB.toFixed(1))}${space ? " " : ""}MB/s`;
|
||||
} else if (KB >= 1) {
|
||||
return `${Number(KB.toFixed(1))}${space ? " " : ""}KB/s`;
|
||||
} else {
|
||||
return `${Number(bytesPerSec.toFixed(1))}${space ? " " : ""}B/s`;
|
||||
}
|
||||
};
|
||||
|
||||
const formatPacketsPerSecondString = (packetsPerSec, space = false) => {
|
||||
if (
|
||||
packetsPerSec === undefined ||
|
||||
packetsPerSec === null ||
|
||||
typeof packetsPerSec !== "number" ||
|
||||
packetsPerSec === 0
|
||||
) {
|
||||
return `0${space ? " " : ""}pps`;
|
||||
}
|
||||
|
||||
const M = packetsPerSec / (1000 * 1000);
|
||||
const K = packetsPerSec / 1000;
|
||||
|
||||
if (M >= 1) {
|
||||
return `${Number(M.toFixed(1))}${space ? " " : ""}Mpps`;
|
||||
} else if (K >= 1) {
|
||||
return `${Number(K.toFixed(1))}${space ? " " : ""}Kpps`;
|
||||
} else {
|
||||
return `${Math.round(packetsPerSec)}${space ? " " : ""}pps`;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDeviceName = (device) => {
|
||||
const deviceStr = String(device || "");
|
||||
|
||||
// Show full device path
|
||||
return (
|
||||
<Tooltip
|
||||
title={deviceStr}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
cursor: "default",
|
||||
display: "inline-block",
|
||||
userSelect: "none",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
>
|
||||
{deviceStr}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const formatMountpoint = (mountpoint) => {
|
||||
const mountpointStr = String(mountpoint || "");
|
||||
|
||||
if (!mountpointStr) {
|
||||
return (
|
||||
<Tooltip
|
||||
title="No mountpoint available"
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
cursor: "default",
|
||||
display: "inline-block",
|
||||
userSelect: "none",
|
||||
color: "text.secondary",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
N/A
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// Show full mountpoint path
|
||||
return (
|
||||
<Tooltip
|
||||
title={mountpointStr}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
cursor: "default",
|
||||
display: "inline-block",
|
||||
userSelect: "none",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
>
|
||||
{mountpointStr}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a decimal value to a percentage
|
||||
*
|
||||
* @function decimalToPercentage
|
||||
* @param {number} value - Decimal value to convert
|
||||
* @returns {number} Percentage representation
|
||||
*
|
||||
* @example
|
||||
* decimalToPercentage(0.75) // Returns 75
|
||||
* decimalToPercentage(null) // Returns 0
|
||||
*/
|
||||
const decimalToPercentage = (value) => {
|
||||
if (value === null || value === undefined) return 0;
|
||||
return value * 100;
|
||||
};
|
||||
|
||||
const buildTemps = (checks) => {
|
||||
let numCores = 1;
|
||||
if (checks === null) return { temps: [], tempKeys: [] };
|
||||
|
||||
for (const check of checks) {
|
||||
if (check?.avgTemperature?.length > numCores) {
|
||||
numCores = check.avgTemperature.length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const temps = checks.map((check) => {
|
||||
// If there's no data, set the temperature to 0
|
||||
if (
|
||||
check?.avgTemperature?.length === 0 ||
|
||||
check?.avgTemperature === undefined ||
|
||||
check?.avgTemperature === null
|
||||
) {
|
||||
check.avgTemperature = Array(numCores).fill(0);
|
||||
}
|
||||
const res = check?.avgTemperature?.reduce(
|
||||
(acc, cur, idx) => {
|
||||
acc[`core${idx + 1}`] = cur;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
_id: check._id,
|
||||
}
|
||||
);
|
||||
return res;
|
||||
});
|
||||
if (temps.length === 0 || !temps[0]) {
|
||||
return { temps: [], tempKeys: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
tempKeys: Object.keys(temps[0] || {}).filter((key) => key !== "_id"),
|
||||
temps,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
formatBytes,
|
||||
formatDeviceName,
|
||||
formatMountpoint,
|
||||
decimalToPercentage,
|
||||
buildTemps,
|
||||
getDimensions,
|
||||
formatBytesPerSecondString,
|
||||
formatPacketsPerSecondString,
|
||||
};
|
||||
};
|
||||
|
||||
export { useHardwareUtils };
|
||||
@@ -1,142 +0,0 @@
|
||||
// Components
|
||||
import { Stack, Typography, Tab } from "@mui/material";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx";
|
||||
import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx";
|
||||
import StatusBoxes from "./Components/StatusBoxes/index.jsx";
|
||||
import GaugeBoxes from "./Components/GaugeBoxes/index.jsx";
|
||||
import AreaChartBoxes from "./Components/AreaChartBoxes/index.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import NetworkStats from "./Components/NetworkStats/index.jsx";
|
||||
import CustomTabList from "@/Components/v1/Tab/index.jsx";
|
||||
import TabContext from "@mui/lab/TabContext";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks.js";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
// Constants
|
||||
const BREADCRUMBS = [
|
||||
{ name: "infrastructure monitors", path: "/infrastructure" },
|
||||
{ name: "details", path: "" },
|
||||
];
|
||||
const InfrastructureDetails = () => {
|
||||
// Local state
|
||||
const [dateRange, setDateRange] = useState("recent");
|
||||
const [trigger, setTrigger] = useState(false);
|
||||
const [tab, setTab] = useState("details");
|
||||
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const { monitorId } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
const [monitor, isLoading, networkError] = useFetchHardwareMonitorById({
|
||||
monitorId,
|
||||
dateRange,
|
||||
updateTrigger: trigger,
|
||||
});
|
||||
|
||||
const triggerUpdate = () => {
|
||||
setTrigger(!trigger);
|
||||
};
|
||||
|
||||
if (networkError === true) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{t("common.toasts.networkError")}
|
||||
</Typography>
|
||||
<Typography>{t("common.toasts.checkConnection")}</Typography>
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && monitor?.stats?.checks?.length === 0) {
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorDetailsControlHeader
|
||||
path={"infrastructure"}
|
||||
isLoading={isLoading}
|
||||
isAdmin={isAdmin}
|
||||
monitor={monitor}
|
||||
triggerUpdate={triggerUpdate}
|
||||
/>
|
||||
<GenericFallback>
|
||||
<Typography>{t("distributedUptimeDetailsNoMonitorHistory")}</Typography>
|
||||
</GenericFallback>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorDetailsControlHeader
|
||||
path={"infrastructure"}
|
||||
isLoading={isLoading}
|
||||
isAdmin={isAdmin}
|
||||
monitor={monitor}
|
||||
triggerUpdate={triggerUpdate}
|
||||
/>
|
||||
<TabContext value={tab}>
|
||||
<CustomTabList
|
||||
value={tab}
|
||||
onChange={(e, v) => setTab(v)}
|
||||
>
|
||||
<Tab
|
||||
label={t("details")}
|
||||
value="details"
|
||||
/>
|
||||
<Tab
|
||||
label={t("network")}
|
||||
value="network"
|
||||
/>
|
||||
</CustomTabList>
|
||||
{tab === "details" && (
|
||||
<>
|
||||
<StatusBoxes
|
||||
shouldRender={!isLoading}
|
||||
monitor={monitor}
|
||||
/>
|
||||
<GaugeBoxes
|
||||
isLoading={isLoading}
|
||||
monitor={monitor}
|
||||
/>
|
||||
<MonitorTimeFrameHeader
|
||||
isLoading={isLoading}
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
<AreaChartBoxes
|
||||
shouldRender={!isLoading}
|
||||
monitor={monitor}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{tab === "network" && (
|
||||
<NetworkStats
|
||||
net={monitor?.recentChecks?.[0]?.net || []}
|
||||
isLoading={isLoading}
|
||||
checks={monitor?.stats?.checks}
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
)}
|
||||
</TabContext>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfrastructureDetails;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { BasePage, Tab, Tabs } from "@/Components/v2/design-elements";
|
||||
import { HeaderMonitorControls, HeaderTimeRange } from "@/Components/v2/common";
|
||||
import { MonitorStatBoxes } from "@/Components/v2/monitors";
|
||||
import { TabNetwork } from "@/Pages/Infrastructure/Details/Components/TabNetwork";
|
||||
import { TabOverview } from "@/Pages/Infrastructure/Details/Components/TabOverview";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import type { HardwareDetailsResponse } from "@/Types/Monitor";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const InfrastructureDetails = () => {
|
||||
const { t } = useTranslation();
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
const { monitorId } = useParams<{ monitorId: string }>();
|
||||
|
||||
const [dateRange, setDateRange] = useState<string>("recent");
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
|
||||
const monitorDetailsUrl = useMemo(() => {
|
||||
if (!monitorId) {
|
||||
return null;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.append("dateRange", dateRange);
|
||||
return `/monitors/hardware/details/${monitorId}?${params.toString()}`;
|
||||
}, [monitorId, dateRange]);
|
||||
|
||||
const {
|
||||
data: monitorDetailsData,
|
||||
isLoading: monitorIsLoading,
|
||||
refetch: refetchMonitor,
|
||||
} = useGet<HardwareDetailsResponse>(
|
||||
monitorDetailsUrl,
|
||||
{},
|
||||
{ refreshInterval: 10000, keepPreviousData: true }
|
||||
);
|
||||
|
||||
const monitor = monitorDetailsData?.monitor;
|
||||
const monitorStats = monitorDetailsData?.monitorStats ?? null;
|
||||
const stats = monitorDetailsData?.stats;
|
||||
|
||||
return (
|
||||
<BasePage>
|
||||
<HeaderMonitorControls
|
||||
path="hardware"
|
||||
monitor={monitor}
|
||||
isAdmin={isAdmin}
|
||||
refetch={refetchMonitor}
|
||||
/>
|
||||
<MonitorStatBoxes
|
||||
monitor={monitor}
|
||||
monitorStats={monitorStats}
|
||||
/>
|
||||
<HeaderTimeRange
|
||||
isLoading={monitorIsLoading}
|
||||
hasDateRange={true}
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
<Tabs
|
||||
value={selectedTab}
|
||||
onChange={(_e, value) => {
|
||||
setSelectedTab(value);
|
||||
}}
|
||||
>
|
||||
<Tab label={t("pages.infrastructure.tabs.labels.overview")} />
|
||||
<Tab label={t("pages.infrastructure.tabs.labels.network")} />
|
||||
</Tabs>
|
||||
{selectedTab === 0 && (
|
||||
<TabOverview
|
||||
monitor={monitor}
|
||||
stats={stats}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
)}
|
||||
{selectedTab === 1 && (
|
||||
<TabNetwork
|
||||
stats={stats}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
)}
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfrastructureDetails;
|
||||
@@ -1,94 +0,0 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import FilterHeader from "@/Components/v1/FilterHeader/index.jsx";
|
||||
import { useMemo } from "react";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Filter Component
|
||||
*
|
||||
* A high-level component that provides filtering options for status in Infrastructure Page.
|
||||
* It allows users to select multiple options for each filter and reset the filters.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string[]} props.selectedStatus - An array of selected status values.
|
||||
* @param {function} props.setSelectedStatus - A function to set the selected status values.
|
||||
* @param {function} props.setToFilterStatus - A function to set the filter status based on selected status values.
|
||||
* @param {function} props.handleReset - A function to reset all filters.
|
||||
*
|
||||
* @returns {JSX.Element} The rendered Filter component.
|
||||
*/
|
||||
|
||||
const statusOptions = [
|
||||
{ value: "Up", label: "Up" },
|
||||
{ value: "Down", label: "Down" },
|
||||
];
|
||||
|
||||
const Filter = ({
|
||||
selectedStatus,
|
||||
setSelectedStatus,
|
||||
setToFilterStatus,
|
||||
handleReset,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleStatusChange = (event) => {
|
||||
const selectedValues = event.target.value;
|
||||
setSelectedStatus(selectedValues.length > 0 ? selectedValues : undefined);
|
||||
|
||||
if (selectedValues.length === 0 || selectedValues.length === 2) {
|
||||
setToFilterStatus(undefined);
|
||||
} else {
|
||||
setToFilterStatus(selectedValues[0] === "Up" ? "true" : "false");
|
||||
}
|
||||
};
|
||||
|
||||
const isFilterActive = useMemo(() => {
|
||||
return (selectedStatus?.length ?? 0) > 0;
|
||||
}, [selectedStatus]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
m: theme.spacing(2),
|
||||
ml: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<FilterHeader
|
||||
header={t("status")}
|
||||
options={statusOptions}
|
||||
value={selectedStatus}
|
||||
onChange={handleStatusChange}
|
||||
/>
|
||||
<Button
|
||||
color={theme.palette.primary.contrastText}
|
||||
onClick={handleReset}
|
||||
variant="contained"
|
||||
endIcon={
|
||||
<Icon
|
||||
name="X"
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
sx={{
|
||||
visibility: isFilterActive ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Filter.propTypes = {
|
||||
selectedStatus: PropTypes.arrayOf(PropTypes.string),
|
||||
setSelectedStatus: PropTypes.func.isRequired,
|
||||
setToFilterStatus: PropTypes.func.isRequired,
|
||||
handleReset: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Filter;
|
||||
@@ -0,0 +1,275 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Table, Pagination, StatusLabel, Gauge } from "@/Components/v2/design-elements";
|
||||
import type { Header } from "@/Components/v2/design-elements/Table";
|
||||
import { ActionsMenu, type ActionMenuItem } from "@/Components/v2/actions-menu";
|
||||
import { ArrowUp, ArrowDown } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { usePost } from "@/Hooks/UseApi";
|
||||
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
export const InfraMonitorsTable = ({
|
||||
monitors,
|
||||
refetch,
|
||||
setSelectedMonitor,
|
||||
sortField,
|
||||
setSortField,
|
||||
sortOrder,
|
||||
setSortOrder,
|
||||
count,
|
||||
page,
|
||||
setPage,
|
||||
rowsPerPage,
|
||||
setRowsPerPage,
|
||||
}: {
|
||||
monitors: Monitor[];
|
||||
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 {
|
||||
post,
|
||||
// loading: isPatching,
|
||||
// error: postError,
|
||||
} = usePost<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(`/infrastructure/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 post(`/monitors/pause/${monitor.id}`, {});
|
||||
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: (
|
||||
<Typography
|
||||
component="div"
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={(e) => handleSort(e, "name")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("common.table.headers.name")}
|
||||
{renderSortIcon(sortField === "name")}
|
||||
</Typography>
|
||||
),
|
||||
render: (row) => {
|
||||
return row.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: (
|
||||
<Typography
|
||||
component="div"
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={(e) => handleSort(e, "status")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("common.table.headers.status")}
|
||||
{renderSortIcon(sortField === "status")}
|
||||
</Typography>
|
||||
),
|
||||
render: (row) => {
|
||||
return (
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
isActive={row.isActive}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "cpu",
|
||||
content: t("pages.infrastructure.table.headers.cpu"),
|
||||
render: (row) => {
|
||||
const check = row.recentChecks?.[0];
|
||||
const cpuUsage = (check?.cpu?.usage_percent || 0) * 100;
|
||||
return <Gauge progress={cpuUsage} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "memory",
|
||||
content: t("pages.infrastructure.table.headers.memory"),
|
||||
render: (row) => {
|
||||
const check = row.recentChecks?.[0];
|
||||
const memoryUsage = (check?.memory?.usage_percent || 0) * 100;
|
||||
return <Gauge progress={memoryUsage} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "disk",
|
||||
content: t("pages.infrastructure.table.headers.disk"),
|
||||
render: (row) => {
|
||||
const check = row.recentChecks?.[0];
|
||||
|
||||
const totalDiskUsage = check?.disk?.reduce(
|
||||
(acc, disk) => acc + (disk?.usage_percent || 0),
|
||||
0
|
||||
);
|
||||
const diskCount = check?.disk?.length || 1;
|
||||
const diskUsage = ((totalDiskUsage || 0) / diskCount) * 100;
|
||||
return <Gauge progress={diskUsage} />;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
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(`/infrastructure/${row.id}`);
|
||||
}}
|
||||
/>
|
||||
<Pagination
|
||||
component="div"
|
||||
count={count}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onPageChange={handlePageChange}
|
||||
onRowsPerPageChange={handleRowsPerPageChange}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,162 +0,0 @@
|
||||
// Components
|
||||
import { Box } from "@mui/material";
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import Host from "@/Components/v1/Host/index.jsx";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
import { Stack } from "@mui/material";
|
||||
import { InfrastructureMenu } from "../MonitorsTableMenu/index.jsx";
|
||||
import LoadingSpinner from "../../../../Uptime/Monitors/Components/LoadingSpinner/index.jsx";
|
||||
// Assets
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const MonitorsTable = ({
|
||||
isLoading,
|
||||
monitors,
|
||||
isAdmin,
|
||||
handleActionMenuDelete,
|
||||
isSearching,
|
||||
}) => {
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { determineState } = useMonitorUtils();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Handlers
|
||||
const openDetails = (id) => {
|
||||
navigate(`/infrastructure/${id}`);
|
||||
};
|
||||
const headers = [
|
||||
{
|
||||
id: "host",
|
||||
content: t("host"),
|
||||
render: (row) => (
|
||||
<Host
|
||||
title={row.name}
|
||||
url={row.url}
|
||||
percentage={row.uptimePercentage}
|
||||
percentageColor={row.percentageColor}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("incidentsTableStatus"),
|
||||
render: (row) => (
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
text={row.status}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "frequency",
|
||||
content: t("frequency"),
|
||||
render: (row) => (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
gap=".25rem"
|
||||
>
|
||||
<Icon
|
||||
name="Cpu"
|
||||
size={20}
|
||||
/>
|
||||
{row.processor}
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{ id: "cpu", content: t("cpu"), render: (row) => <CustomGauge progress={row.cpu} /> },
|
||||
{
|
||||
id: "memory",
|
||||
content: t("memory"),
|
||||
render: (row) => <CustomGauge progress={row.mem} />,
|
||||
},
|
||||
{
|
||||
id: "disk",
|
||||
content: t("disk"),
|
||||
render: (row) => <CustomGauge progress={row.disk} />,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
content: t("actions"),
|
||||
render: (row) => (
|
||||
<InfrastructureMenu
|
||||
monitor={row}
|
||||
isAdmin={isAdmin}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const data = monitors?.map((monitor) => {
|
||||
const processor =
|
||||
((monitor.recentChecks?.[0]?.cpu?.frequency ?? 0) / 1000).toFixed(2) + " GHz";
|
||||
const cpu = (monitor?.recentChecks?.[0]?.cpu?.usage_percent ?? 0) * 100;
|
||||
const mem = (monitor?.recentChecks?.[0]?.memory?.usage_percent ?? 0) * 100;
|
||||
const disk = (monitor?.recentChecks?.[0]?.disk?.[0]?.usage_percent ?? 0) * 100;
|
||||
const status = determineState(monitor);
|
||||
const percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.error.main
|
||||
: monitor.uptimePercentage < 0.5
|
||||
? theme.palette.warning.main
|
||||
: theme.palette.success.main;
|
||||
|
||||
return {
|
||||
id: monitor.id,
|
||||
name: monitor.name,
|
||||
url: monitor.url,
|
||||
processor,
|
||||
cpu,
|
||||
mem,
|
||||
disk,
|
||||
status,
|
||||
percentageColor,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<LoadingSpinner shouldRender={isSearching} />
|
||||
<DataTable
|
||||
shouldRender={!isLoading}
|
||||
headers={headers}
|
||||
data={data}
|
||||
config={{
|
||||
/* TODO this behavior seems to be repeated. Put it on the root table? */
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover td": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
transition: "background-color .3s ease",
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => openDetails(row.id),
|
||||
emptyView: "No monitors found",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorsTable.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
monitors: PropTypes.array,
|
||||
isAdmin: PropTypes.bool,
|
||||
handleActionMenuDelete: PropTypes.func,
|
||||
isSearching: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default MonitorsTable;
|
||||
@@ -1,196 +0,0 @@
|
||||
/* TODO I basically copied and pasted this component from the actionsMenu. Check how we can make it reusable */
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createToast } from "@/Utils/toastUtils.jsx";
|
||||
import { IconButton, Menu, MenuItem } from "@mui/material";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import PropTypes from "prop-types";
|
||||
import Dialog from "@/Components/v1/Dialog/index.jsx";
|
||||
import { networkService } from "@/Utils/NetworkService.js";
|
||||
import { usePauseMonitor } from "@/Hooks/monitorHooks.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* InfrastructureMenu Component
|
||||
* Provides a dropdown menu for managing infrastructure monitors.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {Object} props.monitor - The monitor object containing details about the infrastructure monitor.
|
||||
* @param {string} props.monitor.id - Unique ID of the monitor.
|
||||
* @param {string} [props.monitor.url] - URL associated with the monitor.
|
||||
* @param {string} props.monitor.type - Type of monitor (e.g., uptime, infrastructure).
|
||||
* @param {boolean} props.monitor.isActive - Indicates if the monitor is currently active (true) or paused (false).
|
||||
* @param {boolean} props.isAdmin - Whether the user has admin privileges.
|
||||
* @param {Function} props.updateCallback - Callback to trigger when the monitor data is updated.
|
||||
* @returns {JSX.Element} The rendered component.
|
||||
*/
|
||||
const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
|
||||
const anchor = useRef(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const theme = useTheme();
|
||||
const [pauseMonitor] = usePauseMonitor();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const openMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const closeMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const openRemove = (e) => {
|
||||
closeMenu(e);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
const cancelRemove = () => {
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
function openDetails(id) {
|
||||
navigate(`/infrastructure/${id}`);
|
||||
}
|
||||
|
||||
const openConfigure = (id) => {
|
||||
navigate(`/infrastructure/configure/${id}`);
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
// Pass updateCallback as triggerUpdate to the hook
|
||||
await pauseMonitor({ monitorId: monitor.id, triggerUpdate: updateCallback });
|
||||
// Toast is already displayed in the hook, no need to display it again
|
||||
};
|
||||
|
||||
const handleRemove = async () => {
|
||||
try {
|
||||
await networkService.deleteMonitorById({
|
||||
monitorId: monitor.id,
|
||||
});
|
||||
createToast({
|
||||
body: t("monitorActions.deleteSuccess"),
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({ body: t("monitorActions.deleteFailed") });
|
||||
} finally {
|
||||
setIsDialogOpen(false);
|
||||
updateCallback();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="monitor actions"
|
||||
onClick={openMenu}
|
||||
disabled={!isAdmin}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
ref={anchor}
|
||||
>
|
||||
<Icon
|
||||
name="Settings"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
className="actions-menu"
|
||||
anchorEl={anchor.current}
|
||||
open={isOpen}
|
||||
onClose={closeMenu}
|
||||
disableScrollLock
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
"& ul": { p: theme.spacing(2.5) },
|
||||
"& li": { m: 0 },
|
||||
"& li:last-of-type": {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openDetails(monitor.id);
|
||||
closeMenu(e);
|
||||
}}
|
||||
>
|
||||
{t("monitorActions.details")}
|
||||
</MenuItem>
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openConfigure(monitor.id);
|
||||
closeMenu(e);
|
||||
}}
|
||||
>
|
||||
{t("configure")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await handlePause();
|
||||
closeMenu(e);
|
||||
}}
|
||||
>
|
||||
{!monitor.isActive ? t("resume") : t("pause")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={openRemove}
|
||||
sx={{ color: theme.palette.error.main }}
|
||||
>
|
||||
{t("remove")}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
theme={theme}
|
||||
title={t("deleteDialogTitle")}
|
||||
description={t("deleteDialogDescription")}
|
||||
onCancel={cancelRemove}
|
||||
confirmationButtonLabel={t("delete")}
|
||||
onConfirm={handleRemove}
|
||||
modelTitle="modal-delete-monitor"
|
||||
modelDescription="delete-monitor-confirmation"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
InfrastructureMenu.propTypes = {
|
||||
monitor: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
url: PropTypes.string,
|
||||
// Note: type must remain optional. Making it required (type: PropTypes.string.isRequired)
|
||||
// causes runtime errors as some monitors don't have a defined type property
|
||||
type: PropTypes.string,
|
||||
isActive: PropTypes.bool, // Determines whether the monitor is paused (false) or active (true)
|
||||
status: PropTypes.string, // Represents the monitor's operational status (e.g., 'up', 'down', etc.)
|
||||
}).isRequired,
|
||||
isAdmin: PropTypes.bool.isRequired,
|
||||
updateCallback: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { InfrastructureMenu };
|
||||
@@ -1,139 +0,0 @@
|
||||
// Components
|
||||
import { Stack } from "@mui/material";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx";
|
||||
import MonitorCreateHeader from "@/Components/v1/MonitorCreateHeader/index.jsx";
|
||||
import MonitorsTable from "./Components/MonitorsTable/index.jsx";
|
||||
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
|
||||
import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
|
||||
import Filter from "./Components/Filters/index.jsx";
|
||||
import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent/index.jsx";
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setRowsPerPage } from "../../../Features/UI/uiSlice.js";
|
||||
// Constants
|
||||
const TYPES = ["hardware"];
|
||||
const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }];
|
||||
|
||||
const InfrastructureMonitors = () => {
|
||||
// Redux state
|
||||
const rowsPerPage = useSelector((state) => state.ui?.infrastructure?.rowsPerPage ?? 5);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Local state
|
||||
const [page, setPage] = useState(0);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
const [selectedStatus, setSelectedStatus] = useState(undefined);
|
||||
const [toFilterStatus, setToFilterStatus] = useState(undefined);
|
||||
const [search, setSearch] = useState(undefined);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const isAdmin = useIsAdmin();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Handlers
|
||||
const handleActionMenuDelete = () => {
|
||||
setUpdateTrigger(!updateTrigger);
|
||||
};
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: "infrastructure",
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isSearching) {
|
||||
setPage(0);
|
||||
}
|
||||
}, [isSearching]);
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedStatus(undefined);
|
||||
setToFilterStatus(undefined);
|
||||
};
|
||||
|
||||
const field = toFilterStatus !== undefined ? "status" : undefined;
|
||||
|
||||
const [summary, monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({
|
||||
types: TYPES,
|
||||
limit: 1,
|
||||
page: page,
|
||||
field: field,
|
||||
filter: toFilterStatus ?? search,
|
||||
rowsPerPage: rowsPerPage,
|
||||
monitorUpdateTrigger: updateTrigger,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageStateWrapper
|
||||
networkError={networkError}
|
||||
isLoading={isLoading}
|
||||
items={monitors}
|
||||
type="infrastructureMonitor"
|
||||
fallbackLink="/infrastructure/create"
|
||||
>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorCreateHeader
|
||||
isAdmin={isAdmin}
|
||||
isLoading={isLoading}
|
||||
path="/infrastructure/create"
|
||||
/>
|
||||
<Stack direction={"row"}>
|
||||
<MonitorCountHeader
|
||||
isLoading={isLoading}
|
||||
monitorCount={count || 0}
|
||||
/>
|
||||
<Filter
|
||||
selectedStatus={selectedStatus}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
setToFilterStatus={setToFilterStatus}
|
||||
handleReset={handleReset}
|
||||
/>
|
||||
<SearchComponent
|
||||
monitors={monitors}
|
||||
onSearchChange={setSearch}
|
||||
setIsSearching={setIsSearching}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<MonitorsTable
|
||||
isLoading={isLoading}
|
||||
monitors={monitors}
|
||||
isAdmin={isAdmin}
|
||||
handleActionMenuDelete={handleActionMenuDelete}
|
||||
isSearching={isSearching}
|
||||
/>
|
||||
<Pagination
|
||||
itemCount={count || 0}
|
||||
paginationLabel={t("monitors")}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Stack>
|
||||
</PageStateWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfrastructureMonitors;
|
||||
@@ -0,0 +1,200 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import {
|
||||
MonitorBasePageWithStates,
|
||||
UpStatusBox,
|
||||
DownStatusBox,
|
||||
PausedStatusBox,
|
||||
} from "@/Components/v2/design-elements";
|
||||
import { HeaderCreate } from "@/Components/v2/common";
|
||||
import { ControlsFilter } from "@/Components/v2/monitors";
|
||||
import { TextField, Dialog } from "@/Components/v2/inputs";
|
||||
|
||||
import { useGet, useDelete } from "@/Hooks/UseApi";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
import type { Monitor, MonitorsWithChecksResponse } from "@/Types/Monitor";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { setRowsPerPage } from "@/Features/UI/uiSlice.js";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { InfraMonitorsTable } from "./Components/MonitorsTable";
|
||||
|
||||
const InfrastructureMonitors = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const isAdmin = useIsAdmin();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Redux state
|
||||
const rowsPerPage = useSelector(
|
||||
(state: RootState) => state.ui?.infrastructure?.rowsPerPage ?? 5
|
||||
);
|
||||
|
||||
// Local state
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>("");
|
||||
const [selectedState, setSelectedState] = useState<string>("");
|
||||
const [search, setSearch] = useState<string>("");
|
||||
const [page, setPage] = useState<number>(0);
|
||||
const [sortField, setSortField] = useState<string>("");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
|
||||
const [selectedMonitor, setSelectedMonitor] = useState<Monitor | null>(null);
|
||||
const isDialogOpen = Boolean(selectedMonitor);
|
||||
|
||||
const handleClearFilters = useCallback(() => {
|
||||
setSelectedStatus("");
|
||||
setSelectedState("");
|
||||
setSearch("");
|
||||
}, []);
|
||||
|
||||
// Convert filter selections to API filter values
|
||||
const toFilterStatus = useMemo(() => {
|
||||
if (selectedStatus === "up") return "true";
|
||||
if (selectedStatus === "down") return "false";
|
||||
return undefined;
|
||||
}, [selectedStatus]);
|
||||
|
||||
const toFilterActive = useMemo(() => {
|
||||
if (selectedState === "active") return "true";
|
||||
if (selectedState === "paused") return "false";
|
||||
return undefined;
|
||||
}, [selectedState]);
|
||||
|
||||
// Determine field and filter for the API request
|
||||
// Priority: status > isActive > search
|
||||
const filterLookup = new Map<string | undefined, string>([
|
||||
[toFilterStatus, "status"],
|
||||
[toFilterActive, "isActive"],
|
||||
]);
|
||||
const activeFilter = [...filterLookup].find(([key]) => key !== undefined);
|
||||
const field = activeFilter?.[1] || (search ? "name" : sortField || undefined);
|
||||
const filter = activeFilter?.[0] || search || undefined;
|
||||
|
||||
// Build URL for monitors with checks
|
||||
const monitorsWithChecksUrl = useMemo(() => {
|
||||
const params = new URLSearchParams();
|
||||
params.append("type", "hardware");
|
||||
params.append("limit", "1");
|
||||
if (page !== undefined) params.append("page", String(page));
|
||||
if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage));
|
||||
if (filter) params.append("filter", filter);
|
||||
if (field) params.append("field", field);
|
||||
if (sortOrder) params.append("order", sortOrder);
|
||||
return `/monitors/team/with-checks?${params.toString()}`;
|
||||
}, [page, rowsPerPage, filter, field, sortOrder]);
|
||||
|
||||
const {
|
||||
data: monitors,
|
||||
isLoading: monitorsLoading,
|
||||
error,
|
||||
refetch: refetchMonitors,
|
||||
} = useGet<Monitor[]>("/monitors/team?type=hardware", {}, { keepPreviousData: true });
|
||||
|
||||
const {
|
||||
data: monitorsWithChecksData,
|
||||
isLoading: monitorsWithChecksLoading,
|
||||
error: monitorsWithChecksError,
|
||||
refetch,
|
||||
} = useGet<MonitorsWithChecksResponse>(
|
||||
monitorsWithChecksUrl,
|
||||
{},
|
||||
{ refreshInterval: 5000, keepPreviousData: true }
|
||||
);
|
||||
|
||||
const { summary, count } = monitorsWithChecksData ?? {};
|
||||
const isLoading = monitorsLoading || monitorsWithChecksLoading;
|
||||
|
||||
// Delete hook
|
||||
const { deleteFn, loading: isDeleting } = useDelete();
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!selectedMonitor) return;
|
||||
await deleteFn(`/monitors/${selectedMonitor.id}`);
|
||||
setSelectedMonitor(null);
|
||||
refetch();
|
||||
refetchMonitors();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setSelectedMonitor(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<MonitorBasePageWithStates
|
||||
loading={isLoading}
|
||||
error={error ?? monitorsWithChecksError}
|
||||
items={monitors || []}
|
||||
page="infrastructure"
|
||||
actionLink="/infrastructure/create"
|
||||
>
|
||||
<HeaderCreate
|
||||
path="/infrastructure/create"
|
||||
isLoading={isLoading}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<UpStatusBox n={summary?.upMonitors || 0} />
|
||||
<DownStatusBox n={summary?.downMonitors || 0} />
|
||||
<PausedStatusBox n={summary?.pausedMonitors || 0} />
|
||||
</Stack>
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
justifyContent={isSmall ? "flex-start" : "space-between"}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<ControlsFilter
|
||||
showTypes={false}
|
||||
selectedStatus={selectedStatus}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
selectedState={selectedState}
|
||||
setSelectedState={setSelectedState}
|
||||
onClearFilters={handleClearFilters}
|
||||
/>
|
||||
<TextField
|
||||
placeholder={t("pages.uptime.filters.search.placeholder")}
|
||||
value={search}
|
||||
onChange={(event) => {
|
||||
setSearch(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<InfraMonitorsTable
|
||||
monitors={monitorsWithChecksData?.monitors || []}
|
||||
refetch={refetch}
|
||||
setSelectedMonitor={setSelectedMonitor}
|
||||
sortField={sortField}
|
||||
setSortField={setSortField}
|
||||
sortOrder={sortOrder}
|
||||
setSortOrder={setSortOrder}
|
||||
count={count || 0}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
setRowsPerPage={(value: number) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value,
|
||||
table: "infrastructure",
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
}}
|
||||
/>
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
title={t("common.dialogs.delete.title")}
|
||||
content={t("common.dialogs.delete.description")}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
</MonitorBasePageWithStates>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfrastructureMonitors;
|
||||
@@ -1,83 +0,0 @@
|
||||
// Components
|
||||
import Menu from "@mui/material/Menu";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
|
||||
// Utils
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const ActionMenu = ({ notification, onDelete }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const { t } = useTranslation();
|
||||
// Handlers
|
||||
const handleClick = (event) => {
|
||||
event.stopPropagation();
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = (event) => {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(notification.id);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleConfigure = (e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/notifications/${notification.id}`);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="notification actions"
|
||||
onClick={handleClick}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Icon
|
||||
name="Settings"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MenuItem onClick={handleConfigure}>{t("configure")}</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleRemove}
|
||||
sx={{ "&.MuiButtonBase-root": { color: theme.palette.error.main } }}
|
||||
>
|
||||
{t("delete")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ActionMenu.propTypes = {
|
||||
notification: PropTypes.object,
|
||||
onDelete: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionMenu;
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ActionsMenu, type ActionMenuItem } from "@/Components/v2/actions-menu";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import type { Header } from "@/Components/v2/design-elements/Table";
|
||||
import { Table } from "@/Components/v2/design-elements";
|
||||
|
||||
import type { Notification } from "@/Types/Notification";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material";
|
||||
|
||||
interface NotificationsTableProps {
|
||||
notifications: Notification[];
|
||||
setSelectedChannel: Function;
|
||||
}
|
||||
|
||||
export const NotificationsTable = ({
|
||||
notifications,
|
||||
setSelectedChannel,
|
||||
}: NotificationsTableProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const getActions = (channel: Notification): ActionMenuItem[] => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
label: t("pages.common.monitors.actions.configure"),
|
||||
action: () => {
|
||||
navigate(`/notifications/${channel.id}`);
|
||||
},
|
||||
closeMenu: true,
|
||||
},
|
||||
|
||||
{
|
||||
id: 7,
|
||||
label: (
|
||||
<Typography color={theme.palette.error.main}>
|
||||
{t("pages.common.monitors.actions.delete")}
|
||||
</Typography>
|
||||
),
|
||||
action: async () => {
|
||||
setSelectedChannel(channel);
|
||||
},
|
||||
closeMenu: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const getHeaders = () => {
|
||||
const headers: Header<Notification>[] = [
|
||||
{
|
||||
id: "name",
|
||||
content: t("common.table.headers.name"),
|
||||
render: (row) => {
|
||||
return <Typography>{row?.notificationName}</Typography>;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: "type",
|
||||
content: t("common.table.headers.type"),
|
||||
render: (row) => {
|
||||
return <Typography textTransform={"capitalize"}>{row?.type}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "destination",
|
||||
content: t("pages.notifications.table.headers.destination"),
|
||||
render: (row) => {
|
||||
return <Typography>{row?.address}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
content: t("common.table.headers.actions"),
|
||||
render: (row) => {
|
||||
return <ActionsMenu items={getActions(row)} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
return headers;
|
||||
};
|
||||
|
||||
const headers = getHeaders();
|
||||
|
||||
return (
|
||||
<Table
|
||||
headers={headers}
|
||||
data={notifications}
|
||||
onRowClick={(row) => {
|
||||
navigate(`/notifications/${row.id}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,121 +0,0 @@
|
||||
// Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import Button from "@mui/material/Button";
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import ActionMenu from "./components/ActionMenu.jsx";
|
||||
import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
|
||||
|
||||
// Utils
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
useGetNotificationsByTeamId,
|
||||
useDeleteNotification,
|
||||
} from "../../Hooks/useNotifications.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Notifications = () => {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const BREADCRUMBS = [{ name: "notifications", path: "/notifications" }];
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
const [notifications, isLoading, error] = useGetNotificationsByTeamId(updateTrigger);
|
||||
const [deleteNotification, isDeleting, deleteError] = useDeleteNotification();
|
||||
const { t } = useTranslation();
|
||||
// Handlers
|
||||
const triggerUpdate = () => {
|
||||
setUpdateTrigger(!updateTrigger);
|
||||
};
|
||||
|
||||
const onDelete = (id) => {
|
||||
deleteNotification(id, triggerUpdate);
|
||||
};
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "name",
|
||||
content: "Name",
|
||||
render: (row) => {
|
||||
return row.notificationName;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "target",
|
||||
content: "Target",
|
||||
render: (row) => {
|
||||
return row.address;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "platform",
|
||||
content: "Platform",
|
||||
render: (row) => {
|
||||
return row?.config?.platform || row.type;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: "actions",
|
||||
content: "Actions",
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
},
|
||||
render: (row) => {
|
||||
return (
|
||||
<ActionMenu
|
||||
notification={row}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageStateWrapper
|
||||
networkError={error}
|
||||
isLoading={isLoading}
|
||||
items={notifications}
|
||||
type="notifications"
|
||||
fallbackLink="/notifications/create"
|
||||
>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => navigate("/notifications/create")}
|
||||
>
|
||||
{t("notifications.createButton")}
|
||||
</Button>
|
||||
</Stack>
|
||||
<Typography variant="h1">{t("notifications.createTitle")}</Typography>
|
||||
<DataTable
|
||||
config={{
|
||||
onRowClick: (row) => navigate(`/notifications/${row.id}`),
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover td": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
transition: "background-color .3s ease",
|
||||
},
|
||||
},
|
||||
}}
|
||||
headers={headers}
|
||||
data={notifications}
|
||||
/>
|
||||
</Stack>
|
||||
</PageStateWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notifications;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { BasePageWithStates } from "@/Components/v2/design-elements";
|
||||
import { NotificationsTable } from "@/Pages/Notifications/components/NotificationsTable";
|
||||
import { Dialog } from "@/Components/v2/inputs";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useGet, useDelete } from "@/Hooks/UseApi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Notification } from "@/Types/Notification";
|
||||
|
||||
const NotificationsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selectedChannel, setSelectedChannel] = useState<Notification | null>(null);
|
||||
const isDialogOpen = Boolean(selectedChannel);
|
||||
|
||||
const {
|
||||
data: notifications,
|
||||
isLoading,
|
||||
isValidating,
|
||||
error,
|
||||
refetch,
|
||||
} = useGet<Notification[]>("/notifications/team", {}, { keepPreviousData: true });
|
||||
|
||||
const { deleteFn, loading: isDeleting } = useDelete();
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!selectedChannel) return;
|
||||
await deleteFn(`/notifications/${selectedChannel.id}`);
|
||||
setSelectedChannel(null);
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setSelectedChannel(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePageWithStates
|
||||
page={t("pages.notifications.fallback.title")}
|
||||
bullets={
|
||||
t("pages.notifications.fallback.checks", { returnObjects: true }) as string[]
|
||||
}
|
||||
loading={isLoading || isValidating}
|
||||
error={!!error}
|
||||
items={notifications ?? []}
|
||||
actionButtonText={t("pages.notifications.fallback.actionButton")}
|
||||
actionLink="/notifications/create"
|
||||
>
|
||||
<NotificationsTable
|
||||
notifications={notifications ?? []}
|
||||
setSelectedChannel={setSelectedChannel}
|
||||
/>
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
title={t("common.dialogs.delete.title")}
|
||||
content={t("common.dialogs.delete.description")}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
</BasePageWithStates>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationsPage;
|
||||
@@ -1,330 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
Tooltip,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Text,
|
||||
} from "recharts";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { formatDateWithTz } from "../../../../../Utils/timeUtilsLegacy.js";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const config = {
|
||||
seo: {
|
||||
id: "seo",
|
||||
text: "SEO",
|
||||
color: "secondary",
|
||||
},
|
||||
performance: {
|
||||
id: "performance",
|
||||
text: "performance",
|
||||
color: "success",
|
||||
},
|
||||
bestPractices: {
|
||||
id: "bestPractices",
|
||||
text: "best practices",
|
||||
color: "warning",
|
||||
},
|
||||
accessibility: {
|
||||
id: "accessibility",
|
||||
text: "accessibility",
|
||||
color: "accent",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom tooltip for the area chart.
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.active - Whether the tooltip is active.
|
||||
* @param {Array} props.payload - The payload data for the tooltip.
|
||||
* @param {string} props.label - The label for the tooltip.
|
||||
* @returns {JSX.Element|null} The tooltip element or null if not active.
|
||||
*/
|
||||
|
||||
const CustomToolTip = ({ active, payload, label, config }) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
py: theme.spacing(2),
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
|
||||
</Typography>
|
||||
{Object.keys(config)
|
||||
.reverse()
|
||||
.map((key) => {
|
||||
const { color } = config[key];
|
||||
const dotColor = theme.palette[color].main;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
key={`${key}-tooltip`}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(3)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"& span": {
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={dotColor}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Typography
|
||||
component="span"
|
||||
textTransform="capitalize"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{config[key].text}
|
||||
</Typography>{" "}
|
||||
<Typography component="span">{payload[0].payload[key]}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
CustomToolTip.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
payload: PropTypes.array,
|
||||
label: PropTypes.string,
|
||||
config: PropTypes.object,
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes data to insert gaps with null values based on the interval.
|
||||
* @param {Array} data
|
||||
* @param {number} interval - The interval in milliseconds for gaps.
|
||||
* @returns {Array} The formatted data with gaps.
|
||||
*/
|
||||
const processDataWithGaps = (data, interval) => {
|
||||
if (data.length === 0) return [];
|
||||
let formattedData = [];
|
||||
let last = new Date(data[0].createdAt).getTime();
|
||||
|
||||
// Helper function to add a null entry
|
||||
const addNullEntry = (timestamp) => {
|
||||
formattedData.push({
|
||||
accessibility: "N/A",
|
||||
bestPractices: "N/A",
|
||||
performance: "N/A",
|
||||
seo: "N/A",
|
||||
createdAt: timestamp,
|
||||
});
|
||||
};
|
||||
|
||||
data.forEach((entry) => {
|
||||
const current = new Date(entry.createdAt).getTime();
|
||||
|
||||
if (current - last > interval * 2) {
|
||||
// Insert null entries for each interval
|
||||
let temp = last + interval;
|
||||
while (temp < current) {
|
||||
addNullEntry(new Date(temp).toISOString());
|
||||
temp += interval;
|
||||
}
|
||||
}
|
||||
|
||||
formattedData.push(entry);
|
||||
last = current;
|
||||
});
|
||||
|
||||
return formattedData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom tick component to render ticks on the XAxis.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {number} props.x - The x coordinate for the tick.
|
||||
* @param {number} props.y - The y coordinate for the tick.
|
||||
* @param {Object} props.payload - The data object containing the tick value.
|
||||
* @param {number} props.index - The index of the tick in the array of ticks.
|
||||
*
|
||||
* @returns {JSX.Element|null} The tick element or null if the tick should be hidden.
|
||||
*/
|
||||
const CustomTick = ({ x, y, payload, index }) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
// Render nothing for the first tick
|
||||
if (index === 0) return null;
|
||||
|
||||
return (
|
||||
<Text
|
||||
x={x}
|
||||
y={y + 8}
|
||||
textAnchor="middle"
|
||||
fill={theme.palette.primary.contrastTextTertiary}
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
CustomTick.propTypes = {
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
}),
|
||||
index: PropTypes.number,
|
||||
};
|
||||
|
||||
/**
|
||||
* A chart displaying pagespeed details over time.
|
||||
* @param {Object} props
|
||||
* @param {Array} props.data - The data to display in the chart.
|
||||
* @param {number} props.interval - The interval in milliseconds for processing gaps.
|
||||
* @returns {JSX.Element} The area chart component.
|
||||
*/
|
||||
|
||||
const PageSpeedAreaChart = ({ data, monitor, metrics }) => {
|
||||
const theme = useTheme();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const memoizedData = useMemo(
|
||||
() => processDataWithGaps(data, monitor.interval),
|
||||
[data[0]]
|
||||
);
|
||||
|
||||
const filteredConfig = Object.keys(config).reduce((result, key) => {
|
||||
if (metrics[key]) {
|
||||
result[key] = config[key];
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={25}
|
||||
height={215}
|
||||
>
|
||||
<AreaChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={memoizedData}
|
||||
margin={{ top: 10 }}
|
||||
onMouseMove={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
dataKey="createdAt"
|
||||
tick={<CustomTick />}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
height={18}
|
||||
minTickGap={0}
|
||||
interval="equidistantPreserveStart"
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.primary.lowContrast }}
|
||||
content={<CustomToolTip config={filteredConfig} />}
|
||||
/>
|
||||
<defs>
|
||||
{Object.values(filteredConfig).map(({ id, color }) => {
|
||||
/* TODO not working? */
|
||||
const startColor = theme.palette[color].main;
|
||||
const endColor = theme.palette[color].lowContrast;
|
||||
|
||||
return (
|
||||
<linearGradient
|
||||
id={id}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
key={id}
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={startColor}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={endColor}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
);
|
||||
})}
|
||||
</defs>
|
||||
{Object.keys(filteredConfig).map((key) => {
|
||||
const { color } = filteredConfig[key];
|
||||
const strokeColor = theme.palette[color].main;
|
||||
const bgColor = theme.palette.primary.main;
|
||||
|
||||
return (
|
||||
<Area
|
||||
connectNulls
|
||||
key={key}
|
||||
dataKey={key}
|
||||
stackId={1}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={isHovered ? 2.5 : 1.5}
|
||||
fill={`url(#${filteredConfig[key].id})`}
|
||||
activeDot={{ stroke: bgColor, fill: strokeColor, r: 4.5 }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
PageSpeedAreaChart.propTypes = {
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
accessibility: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
bestPractices: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
performance: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
seo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
metrics: PropTypes.object,
|
||||
};
|
||||
|
||||
export default PageSpeedAreaChart;
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Box, Typography, Divider } from "@mui/material";
|
||||
import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const AreaChartLegend = ({ metrics, handleMetrics }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<LegendBox
|
||||
icon={
|
||||
<Icon
|
||||
name="Ruler"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
header="Metrics"
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
fontSize={11}
|
||||
fontWeight={500}
|
||||
>
|
||||
{t("shown")}
|
||||
</Typography>
|
||||
<Divider sx={{ mt: theme.spacing(2) }} />
|
||||
</Box>
|
||||
<Checkbox
|
||||
id="accessibility-toggle"
|
||||
label="Accessibility"
|
||||
isChecked={metrics.accessibility}
|
||||
onChange={() => handleMetrics("accessibility")}
|
||||
/>
|
||||
<Divider />
|
||||
<Checkbox
|
||||
id="best-practices-toggle"
|
||||
label="Best Practices"
|
||||
isChecked={metrics.bestPractices}
|
||||
onChange={() => handleMetrics("bestPractices")}
|
||||
/>
|
||||
<Divider />
|
||||
<Checkbox
|
||||
id="performance-toggle"
|
||||
label="Performance"
|
||||
isChecked={metrics.performance}
|
||||
onChange={() => handleMetrics("performance")}
|
||||
/>
|
||||
<Divider />
|
||||
<Checkbox
|
||||
id="seo-toggle"
|
||||
label="Search Engine Optimization"
|
||||
isChecked={metrics.seo}
|
||||
onChange={() => handleMetrics("seo")}
|
||||
/>
|
||||
<Divider />
|
||||
</LegendBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default AreaChartLegend;
|
||||
@@ -1,327 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { PieChart as MuiPieChart } from "@mui/x-charts/PieChart";
|
||||
import { useDrawingArea } from "@mui/x-charts";
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
/**
|
||||
* Renders a centered label within a pie chart.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {string | number} props.value - The value to display in the label.
|
||||
* @param {string} props.color - The color of the text.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const PieCenterLabel = ({ value, color, setExpand }) => {
|
||||
const { width, height } = useDrawingArea();
|
||||
return (
|
||||
<g
|
||||
transform={`translate(${width / 2}, ${height / 2})`}
|
||||
onMouseEnter={() => setExpand(true)}
|
||||
>
|
||||
<circle
|
||||
cx={0}
|
||||
cy={0}
|
||||
r={width / 4}
|
||||
fill="transparent"
|
||||
/>
|
||||
<text
|
||||
className="pie-label"
|
||||
style={{
|
||||
fill: color,
|
||||
fontSize: 48,
|
||||
textAnchor: "middle",
|
||||
dominantBaseline: "central",
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
PieCenterLabel.propTypes = {
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
color: PropTypes.string,
|
||||
setExpand: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* A component that renders a label on a pie chart slice.
|
||||
* The label is positioned relative to the center of the pie chart and is optionally highlighted.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {number} props.value - The value to display inside the pie slice.
|
||||
* @param {number} props.startAngle - The starting angle of the pie slice in degrees.
|
||||
* @param {number} props.endAngle - The ending angle of the pie slice in degrees.
|
||||
* @param {string} props.color - The color of the label text when highlighted.
|
||||
* @param {boolean} props.highlighted - Determines if the label should be highlighted or not.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const PieValueLabel = ({ value, startAngle, endAngle, color, highlighted }) => {
|
||||
const { width, height } = useDrawingArea();
|
||||
|
||||
// Compute the midpoint angle in radians
|
||||
const angle = (((startAngle + endAngle) / 2) * Math.PI) / 180;
|
||||
const radius = height / 4; // length from center of the circle to where the text is positioned
|
||||
|
||||
// Calculate x and y positions
|
||||
const x = Math.sin(angle) * radius;
|
||||
const y = -Math.cos(angle) * radius;
|
||||
|
||||
return (
|
||||
<g transform={`translate(${width / 2}, ${height / 2})`}>
|
||||
<text
|
||||
className="pie-value-label"
|
||||
x={x}
|
||||
y={y}
|
||||
style={{
|
||||
fill: highlighted ? color : "rgba(0,0,0,0)",
|
||||
fontSize: 12,
|
||||
textAnchor: "middle",
|
||||
dominantBaseline: "central",
|
||||
userSelect: "none",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
+{value}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
// Validate props using PropTypes
|
||||
PieValueLabel.propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
startAngle: PropTypes.number.isRequired,
|
||||
endAngle: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
highlighted: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Weight constants for different performance metrics.
|
||||
* @type {Object}
|
||||
*/
|
||||
const weights = {
|
||||
fcp: 10,
|
||||
si: 10,
|
||||
lcp: 25,
|
||||
tbt: 30,
|
||||
cls: 25,
|
||||
};
|
||||
|
||||
const PieChart = ({ audits }) => {
|
||||
const theme = useTheme();
|
||||
const [highlightedItem, setHighLightedItem] = useState(null);
|
||||
const [expand, setExpand] = useState(false);
|
||||
|
||||
/**
|
||||
* Retrieves color properties based on the performance value.
|
||||
*
|
||||
* @param {number} value - The performance score used to determine the color properties.
|
||||
* @returns {{stroke: string, strokeBg: string, text: string, bg: string}} The color properties for the given performance value.
|
||||
*/
|
||||
const getColors = (value) => {
|
||||
if (value >= 90 && value <= 100)
|
||||
return {
|
||||
stroke: theme.palette.success.main,
|
||||
strokeBg: theme.palette.success.lowContrast,
|
||||
text: theme.palette.success.contrastText,
|
||||
bg: theme.palette.success.lowContrast,
|
||||
};
|
||||
else if (value >= 50 && value < 90)
|
||||
return {
|
||||
stroke: theme.palette.warning.main,
|
||||
strokeBg: theme.palette.warning.lowContrast,
|
||||
text: theme.palette.warning.contrastText,
|
||||
bg: theme.palette.warning.lowContrast,
|
||||
};
|
||||
else if (value >= 0 && value < 50)
|
||||
return {
|
||||
stroke: theme.palette.error.contrastText,
|
||||
strokeBg: theme.palette.error.lowContrast,
|
||||
text: theme.palette.error.contrastText,
|
||||
bg: theme.palette.error.lowContrast,
|
||||
};
|
||||
return {
|
||||
stroke: theme.palette.tertiary.contrastText,
|
||||
strokeBg: theme.palette.tertiary.contrastText,
|
||||
text: theme.palette.tertiary.contrastText,
|
||||
bg: theme.palette.tertiary.main,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates and formats the data needed for rendering a pie chart based on audit scores and weights.
|
||||
* This function generates properties for each pie slice, including angles, radii, and colors.
|
||||
* It also calculates performance based on the weighted values.
|
||||
*
|
||||
* @returns {Array<Object>} An array of objects, each representing the properties for a slice of the pie chart.
|
||||
* @returns {number} performance - A variable updated with the rounded sum of weighted values.
|
||||
*/
|
||||
let performance = 0;
|
||||
const getPieData = (audits) => {
|
||||
if (typeof audits === "undefined") return undefined;
|
||||
|
||||
let data = [];
|
||||
let startAngle = 0;
|
||||
const padding = 3; // padding between arcs
|
||||
const max = 360 - padding * (Object.keys(audits).length - 1); // _id is a child of audits
|
||||
|
||||
Object.keys(audits).forEach((key) => {
|
||||
if (audits[key].score) {
|
||||
let value = audits[key].score * weights[key];
|
||||
let endAngle = startAngle + (weights[key] * max) / 100;
|
||||
|
||||
let theme = getColors(audits[key].score * 100);
|
||||
data.push({
|
||||
id: key,
|
||||
data: [
|
||||
{
|
||||
value: value,
|
||||
color: theme.stroke,
|
||||
label: key.toUpperCase(),
|
||||
},
|
||||
{
|
||||
value: weights[key] - value,
|
||||
color: theme.strokeBg,
|
||||
label: "",
|
||||
},
|
||||
],
|
||||
arcLabel: (item) => `${item.label}`,
|
||||
arcLabelRadius: 95,
|
||||
startAngle: startAngle,
|
||||
endAngle: endAngle,
|
||||
innerRadius: 73,
|
||||
outerRadius: 80,
|
||||
cornerRadius: 2,
|
||||
highlightScope: { faded: "global", highlighted: "series" },
|
||||
faded: {
|
||||
innerRadius: 73,
|
||||
outerRadius: 80,
|
||||
additionalRadius: -20,
|
||||
arcLabelRadius: 5,
|
||||
},
|
||||
cx: pieSize.width / 2,
|
||||
});
|
||||
|
||||
performance += Math.floor(value);
|
||||
startAngle = endAngle + padding;
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const pieSize = { width: 230, height: 230 };
|
||||
const pieData = getPieData(audits);
|
||||
const colorMap = getColors(performance);
|
||||
|
||||
return (
|
||||
<Box
|
||||
onMouseLeave={() => setExpand(false)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{expand ? (
|
||||
<MuiPieChart
|
||||
series={[
|
||||
{
|
||||
data: [
|
||||
{
|
||||
value: 100,
|
||||
color: colorMap.bg,
|
||||
},
|
||||
],
|
||||
outerRadius: 77,
|
||||
cx: pieSize.width / 2,
|
||||
},
|
||||
...pieData,
|
||||
]}
|
||||
width={pieSize.width}
|
||||
height={pieSize.height}
|
||||
margin={{ left: 0, top: 0, right: 0, bottom: 0 }}
|
||||
onHighlightChange={setHighLightedItem}
|
||||
slotProps={{
|
||||
legend: { hidden: true },
|
||||
}}
|
||||
tooltip={{ trigger: "none" }}
|
||||
sx={{
|
||||
"&:has(.MuiPieArcLabel-faded) .pie-label": {
|
||||
fill: "rgba(0,0,0,0) !important",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PieCenterLabel
|
||||
value={performance}
|
||||
color={colorMap.text}
|
||||
setExpand={setExpand}
|
||||
/>
|
||||
{pieData?.map((pie) => (
|
||||
<PieValueLabel
|
||||
key={pie.id}
|
||||
value={Math.round(pie.data[0].value * 10) / 10}
|
||||
startAngle={pie.startAngle}
|
||||
endAngle={pie.endAngle}
|
||||
color={pie.data[0].color}
|
||||
highlighted={highlightedItem?.seriesId === pie.id}
|
||||
/>
|
||||
))}
|
||||
</MuiPieChart>
|
||||
) : (
|
||||
<MuiPieChart
|
||||
series={[
|
||||
{
|
||||
data: [
|
||||
{
|
||||
value: 100,
|
||||
color: colorMap.bg,
|
||||
},
|
||||
],
|
||||
outerRadius: 77,
|
||||
cx: pieSize.width / 2,
|
||||
},
|
||||
{
|
||||
data: [
|
||||
{
|
||||
value: performance,
|
||||
color: colorMap.stroke,
|
||||
},
|
||||
],
|
||||
innerRadius: 73,
|
||||
outerRadius: 80,
|
||||
paddingAngle: 5,
|
||||
cornerRadius: 2,
|
||||
startAngle: 0,
|
||||
endAngle: (performance / 100) * 360,
|
||||
cx: pieSize.width / 2,
|
||||
},
|
||||
]}
|
||||
width={pieSize.width}
|
||||
height={pieSize.height}
|
||||
margin={{ left: 0, top: 0, right: 0, bottom: 0 }}
|
||||
tooltip={{ trigger: "none" }}
|
||||
>
|
||||
<PieCenterLabel
|
||||
value={performance}
|
||||
color={colorMap.text}
|
||||
setExpand={setExpand}
|
||||
/>
|
||||
</MuiPieChart>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
PieChart.propTypes = {
|
||||
audits: PropTypes.object,
|
||||
};
|
||||
|
||||
export default PieChart;
|
||||
@@ -1,101 +0,0 @@
|
||||
import { Stack, Box, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const PieChartLegend = ({ audits }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<LegendBox
|
||||
icon={
|
||||
<Icon
|
||||
name="Gauge"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
header="Performance metrics"
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
{typeof audits !== "undefined" &&
|
||||
Object.keys(audits).map((key) => {
|
||||
if (key === "_id") return;
|
||||
|
||||
let audit = audits[key];
|
||||
let score = audit.score * 100;
|
||||
let bg =
|
||||
score >= 90
|
||||
? theme.palette.success.main
|
||||
: score >= 50
|
||||
? theme.palette.warning.main
|
||||
: score >= 0
|
||||
? theme.palette.error.main
|
||||
: theme.palette.tertiary.main;
|
||||
|
||||
// Find the position where the number ends and the unit begins
|
||||
const match = audit?.displayValue?.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/);
|
||||
let value;
|
||||
let unit;
|
||||
if (match) {
|
||||
value = match[1];
|
||||
match[2] === "s" ? (unit = "seconds") : (unit = match[2]);
|
||||
} else {
|
||||
value = audit.displayValue;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
flex={1}
|
||||
key={`${key}-box`}
|
||||
justifyContent="space-between"
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
p={theme.spacing(3)}
|
||||
border={1}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={4}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
fontSize={12}
|
||||
fontWeight={500}
|
||||
lineHeight={1}
|
||||
mb={1}
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{audit.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize={14}
|
||||
fontWeight={500}
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{value}
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
ml={2}
|
||||
>
|
||||
{unit}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
width={4}
|
||||
backgroundColor={bg}
|
||||
borderRadius={4}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</LegendBox>
|
||||
);
|
||||
};
|
||||
|
||||
PieChartLegend.propTypes = {
|
||||
audits: PropTypes.object,
|
||||
};
|
||||
|
||||
export default PieChartLegend;
|
||||
@@ -1,60 +0,0 @@
|
||||
import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
|
||||
import AreaChart from "../Charts/AreaChart.jsx";
|
||||
import AreaChartLegend from "../Charts/AreaChartLegend.jsx";
|
||||
import SkeletonLayout from "./skeleton.jsx";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import { Stack } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const PageSpeedAreaChart = ({ shouldRender, monitor, metrics, handleMetrics }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!shouldRender) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
const data = monitor?.checks ? [...monitor.checks].reverse() : [];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<ChartBox
|
||||
justifyContent="flex-start"
|
||||
icon={
|
||||
<Icon
|
||||
name="TrendingUp"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
header="Score history"
|
||||
height="100%"
|
||||
borderRadiusRight={16}
|
||||
Legend={
|
||||
<AreaChartLegend
|
||||
metrics={metrics}
|
||||
handleMetrics={handleMetrics}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AreaChart
|
||||
data={data}
|
||||
monitor={monitor}
|
||||
metrics={metrics}
|
||||
/>
|
||||
</ChartBox>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
PageSpeedAreaChart.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
monitor: PropTypes.object,
|
||||
metrics: PropTypes.object,
|
||||
handleMetrics: PropTypes.func,
|
||||
};
|
||||
|
||||
export default PageSpeedAreaChart;
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Box, Skeleton } from "@mui/material";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
return (
|
||||
<Box
|
||||
height={"100%"}
|
||||
width={"100%"}
|
||||
>
|
||||
<Skeleton
|
||||
height={"100%"}
|
||||
width={"100%"}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -1,49 +0,0 @@
|
||||
import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
|
||||
import StatBox from "@/Components/v1/StatBox/index.jsx";
|
||||
import { Typography } from "@mui/material";
|
||||
import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const PageSpeedStatusBoxes = ({ shouldRender, monitorStats }) => {
|
||||
const lastChecked = monitorStats?.lastCheckTimestamp
|
||||
? Date.now() - monitorStats.lastCheckTimestamp
|
||||
: 0;
|
||||
|
||||
// Determine time since last failure
|
||||
const timeOfLastFailure = monitorStats?.timeOfLastFailure;
|
||||
const timeSinceLastFailure =
|
||||
timeOfLastFailure > 0
|
||||
? Date.now() - timeOfLastFailure
|
||||
: Date.now() - new Date(monitorStats?.createdAt);
|
||||
|
||||
const streakTime = getHumanReadableDuration(timeSinceLastFailure);
|
||||
|
||||
const time = getHumanReadableDuration(lastChecked);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<StatusBoxes shouldRender={shouldRender}>
|
||||
<StatBox
|
||||
heading="checks since"
|
||||
subHeading={
|
||||
<>
|
||||
{streakTime}
|
||||
<Typography component="span">{t("ago")}</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StatBox
|
||||
heading="last check"
|
||||
subHeading={
|
||||
<>
|
||||
{time}
|
||||
<Typography component="span">{t("ago")}</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</StatusBoxes>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSpeedStatusBoxes;
|
||||
@@ -1,61 +0,0 @@
|
||||
import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import PieChart from "../Charts/PieChart.jsx";
|
||||
import { Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PieChartLegend from "../Charts/PieChartLegend.jsx";
|
||||
import SkeletonLayout from "./skeleton.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const PerformanceReport = ({ shouldRender, audits }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!shouldRender) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartBox
|
||||
icon={
|
||||
<Icon
|
||||
name="Layers"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
header="Performance report"
|
||||
Legend={<PieChartLegend audits={audits} />}
|
||||
borderRadiusRight={16}
|
||||
>
|
||||
<PieChart audits={audits} />
|
||||
<Typography
|
||||
variant="body1"
|
||||
mt="auto"
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{t("pageSpeedDetailsPerformanceReport")}{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
sx={{
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
fontWeight: 500,
|
||||
textDecoration: "underline",
|
||||
textUnderlineOffset: 2,
|
||||
transition: "all 200ms",
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
textUnderlineOffset: 4,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("pageSpeedDetailsPerformanceReportCalculator")}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</ChartBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default PerformanceReport;
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Box, Skeleton } from "@mui/material";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
return (
|
||||
<Box
|
||||
height={"100%"}
|
||||
width={"100%"}
|
||||
>
|
||||
<Skeleton
|
||||
height={"100%"}
|
||||
width={"100%"}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -1,120 +0,0 @@
|
||||
// Components
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx";
|
||||
import PageSpeedStatusBoxes from "./Components/PageSpeedStatusBoxes/index.jsx";
|
||||
import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx";
|
||||
import PageSpeedAreaChart from "./Components/PageSpeedAreaChart/index.jsx";
|
||||
import PerformanceReport from "./Components/PerformanceReport/index.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useFetchPageSpeedMonitorById } from "../../../Hooks/monitorHooks.js";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
// Constants
|
||||
const BREADCRUMBS = [
|
||||
{ name: "pagespeed", path: "/pagespeed" },
|
||||
{ name: "details", path: `` },
|
||||
];
|
||||
|
||||
const PageSpeedDetails = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const isAdmin = useIsAdmin();
|
||||
const { monitorId } = useParams();
|
||||
|
||||
// Local state
|
||||
const [metrics, setMetrics] = useState({
|
||||
accessibility: true,
|
||||
bestPractices: true,
|
||||
performance: true,
|
||||
seo: true,
|
||||
});
|
||||
const [trigger, setTrigger] = useState(false);
|
||||
// Network
|
||||
const [monitor, monitorStats, isLoading, networkError] = useFetchPageSpeedMonitorById({
|
||||
monitorId,
|
||||
dateRange: "day",
|
||||
updateTrigger: trigger,
|
||||
});
|
||||
|
||||
// Handlers
|
||||
const handleMetrics = (id) => {
|
||||
setMetrics((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
const triggerUpdate = () => {
|
||||
setTrigger(!trigger);
|
||||
};
|
||||
if (networkError === true) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{t("common.toasts.networkError")}
|
||||
</Typography>
|
||||
<Typography>{t("common.toasts.checkConnection")}</Typography>
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
// Empty view, displayed when loading is complete and there are no checks
|
||||
if (!isLoading && monitor?.checks?.length === 0) {
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorDetailsControlHeader
|
||||
path={"pagespeed"}
|
||||
isLoading={isLoading}
|
||||
isAdmin={isAdmin}
|
||||
monitor={monitor}
|
||||
triggerUpdate={triggerUpdate}
|
||||
/>
|
||||
<GenericFallback>
|
||||
<Typography>{t("distributedUptimeDetailsNoMonitorHistory")}</Typography>
|
||||
</GenericFallback>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorDetailsControlHeader
|
||||
path={"pagespeed"}
|
||||
isLoading={isLoading}
|
||||
isAdmin={isAdmin}
|
||||
monitor={monitor}
|
||||
triggerUpdate={triggerUpdate}
|
||||
/>
|
||||
<PageSpeedStatusBoxes
|
||||
shouldRender={!isLoading}
|
||||
monitorStats={monitorStats}
|
||||
/>
|
||||
<MonitorTimeFrameHeader
|
||||
shouldRender={!isLoading}
|
||||
dateRange={"day"}
|
||||
hasDateRange={false}
|
||||
/>
|
||||
|
||||
<PageSpeedAreaChart
|
||||
shouldRender={!isLoading}
|
||||
monitor={monitor}
|
||||
metrics={metrics}
|
||||
handleMetrics={handleMetrics}
|
||||
/>
|
||||
<PerformanceReport
|
||||
shouldRender={!isLoading}
|
||||
audits={monitor?.checks[0]?.audits}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSpeedDetails;
|
||||
@@ -1,331 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
import { Box, Grid, Stack, Typography } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
|
||||
import { useSelector } from "react-redux";
|
||||
import { formatDateWithTz, formatDurationSplit } from "@/Utils/timeUtilsLegacy.js";
|
||||
import { useMonitorUtils } from "@/Hooks/useMonitorUtils.js";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import IconBox from "@/Components/v1/IconBox/index.jsx";
|
||||
/**
|
||||
* CustomToolTip displays a tooltip with formatted date and score information.
|
||||
* @param {Object} props
|
||||
* @param {Array} props.payload - Data to display in the tooltip
|
||||
* @returns {JSX.Element} The rendered tooltip component
|
||||
*/
|
||||
const CustomToolTip = ({ payload }) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
py: theme.spacing(2),
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{formatDateWithTz(
|
||||
payload[0]?.payload.createdAt,
|
||||
"ddd, MMMM D, YYYY, h:mm A",
|
||||
uiTimezone
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(3)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"& span": {
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={payload[0]?.color}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Typography
|
||||
component="span"
|
||||
textTransform="capitalize"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{payload[0]?.name}
|
||||
</Typography>{" "}
|
||||
<Typography component="span">{payload[0]?.payload.score}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
CustomToolTip.propTypes = {
|
||||
payload: PropTypes.array,
|
||||
};
|
||||
|
||||
/* TODO separate utils in folder*/
|
||||
/**
|
||||
* Processes the raw data to include a score for each entry.
|
||||
* @param {Array<Object>} data - The raw data array.
|
||||
* @returns {Array<Object>} - The formatted data array with scores.
|
||||
*/
|
||||
const processData = (data) => {
|
||||
if (data.length === 0) return [];
|
||||
let formattedData = [];
|
||||
|
||||
const calculateScore = (entry) => {
|
||||
return (
|
||||
(entry.accessibility + entry.bestPractices + entry.performance + entry.seo) / 4
|
||||
);
|
||||
};
|
||||
|
||||
data.forEach((entry) => {
|
||||
entry = { ...entry, score: calculateScore(entry) };
|
||||
formattedData.push(entry);
|
||||
});
|
||||
|
||||
return formattedData;
|
||||
};
|
||||
|
||||
/* TODO separate component*/
|
||||
/**
|
||||
* Renders an area chart displaying page speed scores.
|
||||
* @param {Object} props
|
||||
* @param {Array<Object>} props.data - The raw data to be displayed in the chart.
|
||||
* @param {string} props.status - The status of the page speed which determines the chart's color scheme.
|
||||
* @returns {JSX.Element} - The rendered area chart.
|
||||
*/
|
||||
const PagespeedAreaChart = ({ data, status }) => {
|
||||
const theme = useTheme();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const { statusToTheme } = useMonitorUtils();
|
||||
|
||||
const themeColor = statusToTheme[status];
|
||||
|
||||
const formattedData = processData(data);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={25}
|
||||
height={85}
|
||||
>
|
||||
<AreaChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={formattedData}
|
||||
margin={{ top: 10, bottom: -5 }}
|
||||
style={{ cursor: "pointer" }}
|
||||
onMouseMove={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.primary.lowContrast }}
|
||||
content={<CustomToolTip />}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={`pagespeed-chart-${status}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme.palette[themeColor].lowContrast}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme.palette[themeColor].main}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
dataKey="score"
|
||||
stroke={theme.palette[themeColor].lowContrast}
|
||||
strokeWidth={isHovered ? 2.5 : 1.5}
|
||||
fill={`url(#pagespeed-chart-${status})`}
|
||||
activeDot={{
|
||||
stroke: theme.palette[themeColor].main,
|
||||
fill: theme.palette[themeColor].lowContrast,
|
||||
r: 4.5,
|
||||
}}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
PagespeedAreaChart.propTypes = {
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
accessibility: PropTypes.number.isRequired,
|
||||
bestPractices: PropTypes.number.isRequired,
|
||||
performance: PropTypes.number.isRequired,
|
||||
seo: PropTypes.number.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
/* TODO separate component */
|
||||
/**
|
||||
* Renders a card displaying monitor details and an area chart.
|
||||
* @param {Object} props
|
||||
* @param {Object} props.monitor - The monitor data to be displayed in the card.
|
||||
* @returns {JSX.Element} - The rendered card.
|
||||
*/
|
||||
const Card = ({ monitor }) => {
|
||||
const { determineState, pagespeedStatusMsg } = useMonitorUtils();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const monitorState = determineState(monitor);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
item
|
||||
lg={6}
|
||||
flexGrow={1}
|
||||
>
|
||||
<Box
|
||||
position="relative"
|
||||
p={theme.spacing(8)}
|
||||
onClick={() => navigate(`/pagespeed/${monitor.id}`)}
|
||||
border={1}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "34px 2fr 1fr",
|
||||
columnGap: theme.spacing(5),
|
||||
gridTemplateRows: "34px 1fr 3fr",
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
},
|
||||
"& path": {
|
||||
transition: "stroke-width 400ms ease",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconBox>
|
||||
<Icon
|
||||
name="Gauge"
|
||||
size={20}
|
||||
/>
|
||||
</IconBox>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="monitorUrl"
|
||||
fontWeight={500}
|
||||
alignSelf="center"
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<StatusLabel
|
||||
status={monitorState}
|
||||
text={pagespeedStatusMsg[monitorState] || "Pending..."}
|
||||
customStyles={{
|
||||
width: "max-content",
|
||||
textTransform: "capitalize",
|
||||
alignSelf: "flex-start",
|
||||
justifySelf: "flex-end",
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
mt={theme.spacing(-2)}
|
||||
sx={{ gridColumnStart: 2 }}
|
||||
>
|
||||
{monitor.url}
|
||||
</Typography>
|
||||
<Box
|
||||
mx={theme.spacing(-8)}
|
||||
mt={theme.spacing(4)}
|
||||
mb={theme.spacing(-8)}
|
||||
sx={{ gridColumnStart: 1, gridColumnEnd: 4 }}
|
||||
>
|
||||
<PagespeedAreaChart
|
||||
data={monitor?.checks?.slice().reverse()}
|
||||
status={monitorState}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
py={theme.spacing(1)}
|
||||
px={theme.spacing(4)}
|
||||
borderTop={1}
|
||||
borderRight={1}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
backgroundColor={theme.palette.tertiary.main}
|
||||
sx={{
|
||||
pointerEvents: "none",
|
||||
userSelect: "none",
|
||||
borderTopRightRadius: 8,
|
||||
borderBottomLeftRadius: 4,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
fontSize={11}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{t("checkingEvery")}{" "}
|
||||
{(() => {
|
||||
const { time, format } = formatDurationSplit(monitor?.interval);
|
||||
return (
|
||||
<>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize={12}
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{time}{" "}
|
||||
</Typography>
|
||||
{format}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
monitor: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Card;
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Grid } from "@mui/material";
|
||||
import Card from "../Card/index.jsx";
|
||||
|
||||
const MonitorGrid = ({ monitors, shouldRender = true }) => {
|
||||
if (!shouldRender) return null;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={12}
|
||||
>
|
||||
{monitors?.map((monitor) => (
|
||||
<Card
|
||||
monitor={monitor}
|
||||
key={monitor._id || monitor.id}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonitorGrid;
|
||||
@@ -1,86 +0,0 @@
|
||||
// Components
|
||||
import { useState } from "react";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import { Stack } from "@mui/material";
|
||||
import CreateMonitorHeader from "@/Components/v1/MonitorCreateHeader/index.jsx";
|
||||
import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx";
|
||||
import MonitorGrid from "./Components/MonitorGrid/index.jsx";
|
||||
import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
|
||||
import FallbackPageSpeedWarning from "@/Components/v1/Fallback/FallbackPageSpeedWarning.jsx";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js";
|
||||
import { useFetchSettings } from "@/Hooks/settingsHooks.js";
|
||||
// Constants
|
||||
const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }];
|
||||
const TYPES = ["pagespeed"];
|
||||
const PageSpeed = () => {
|
||||
const theme = useTheme();
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
const [
|
||||
summary,
|
||||
monitorsWithChecks,
|
||||
monitorsWithChecksCount,
|
||||
monitorsWithChecksIsLoading,
|
||||
monitorsWithChecksNetworkError,
|
||||
] = useFetchMonitorsWithChecks({
|
||||
types: TYPES,
|
||||
limit: 10,
|
||||
page: null,
|
||||
rowsPerPage: null,
|
||||
filter: null,
|
||||
field: null,
|
||||
order: null,
|
||||
monitorUpdateTrigger: null,
|
||||
});
|
||||
|
||||
const [settingsData, setSettingsData] = useState(undefined);
|
||||
const [isSettingsLoading, settingsError] = useFetchSettings({
|
||||
setSettingsData,
|
||||
setIsApiKeySet: () => {},
|
||||
setIsEmailPasswordSet: () => {},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageStateWrapper
|
||||
networkError={monitorsWithChecksNetworkError}
|
||||
isLoading={monitorsWithChecksIsLoading}
|
||||
items={monitorsWithChecks}
|
||||
type="pageSpeed"
|
||||
fallbackLink="/pagespeed/create"
|
||||
fallbackChildren={
|
||||
isAdmin &&
|
||||
settingsData &&
|
||||
!settingsData.pagespeedApiKey && (
|
||||
<FallbackPageSpeedWarning settingsData={settingsData} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<CreateMonitorHeader
|
||||
isAdmin={isAdmin}
|
||||
isLoading={monitorsWithChecksIsLoading}
|
||||
path="/pagespeed/create"
|
||||
/>
|
||||
<MonitorCountHeader
|
||||
isLoading={monitorsWithChecksIsLoading}
|
||||
monitorCount={monitorsWithChecksCount}
|
||||
sx={{ mb: theme.spacing(8) }}
|
||||
/>
|
||||
<MonitorGrid
|
||||
size={6}
|
||||
shouldRender={!monitorsWithChecksIsLoading}
|
||||
monitors={monitorsWithChecks}
|
||||
/>
|
||||
</Stack>
|
||||
</PageStateWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSpeed;
|
||||
@@ -1,44 +0,0 @@
|
||||
import { logger } from "../../../../Utils/Logger.js";
|
||||
import { useEffect, useState } from "react";
|
||||
import { networkService } from "../../../../main.jsx";
|
||||
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
|
||||
|
||||
const useCertificateFetch = ({
|
||||
monitor,
|
||||
monitorId,
|
||||
certificateDateFormat,
|
||||
uiTimezone,
|
||||
}) => {
|
||||
const [certificateExpiry, setCertificateExpiry] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCertificate = async () => {
|
||||
if (monitor?.type !== "http") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.getCertificateExpiry({
|
||||
monitorId: monitorId,
|
||||
});
|
||||
if (res?.data?.data?.certificateDate) {
|
||||
const date = res.data.data.certificateDate;
|
||||
setCertificateExpiry(
|
||||
formatDateWithTz(date, certificateDateFormat, uiTimezone) ?? "N/A"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
setCertificateExpiry("N/A");
|
||||
logger.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchCertificate();
|
||||
}, [monitorId, certificateDateFormat, uiTimezone, monitor]);
|
||||
return [certificateExpiry, isLoading];
|
||||
};
|
||||
|
||||
export default useCertificateFetch;
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@/Components/v2/monitors";
|
||||
import { TrendingUp, AlertTriangle } from "lucide-react";
|
||||
import { ChecksTable } from "@/Pages/Uptime/Details/Components/ChecksTable";
|
||||
import { MonitorStatBoxes } from "@/Components/v2/monitors";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
@@ -18,7 +19,6 @@ import { useGet } from "@/Hooks/UseApi";
|
||||
import type { MonitorDetailsResponse } from "@/Types/Monitor";
|
||||
import type { ChecksResponse } from "@/Types/Check";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { MonitorStatBoxes } from "@/Components/v2/monitors";
|
||||
import { formatDateWithTz } from "@/Utils/timeUtilsLegacy";
|
||||
import { t } from "i18next";
|
||||
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
// Components
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx";
|
||||
import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx";
|
||||
import ChartBoxes from "./Components/ChartBoxes/index.jsx";
|
||||
import ResponseTimeChart from "./Components/Charts/ResponseTimeChart.jsx";
|
||||
import ResponseTable from "./Components/ResponseTable/index.jsx";
|
||||
import UptimeStatusBoxes from "./Components/UptimeStatusBoxes/index.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
// Utils
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
import { useFetchUptimeMonitorById } from "../../../Hooks/monitorHooks.js";
|
||||
import useCertificateFetch from "./Hooks/useCertificateFetch.jsx";
|
||||
import { useFetchChecksByMonitor } from "../../../Hooks/checkHooks.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// Constants
|
||||
const BREADCRUMBS = [
|
||||
{ name: "uptime", path: "/uptime" },
|
||||
{ name: "details", path: "" },
|
||||
// { name: "details", path: `/uptime/${monitorId}` }, Is this needed? We can't click on this anywy
|
||||
];
|
||||
|
||||
const certificateDateFormat = "MMM D, YYYY h A";
|
||||
|
||||
const UptimeDetails = () => {
|
||||
// Redux state
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
// Local state
|
||||
const [dateRange, setDateRange] = useState("recent");
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [trigger, setTrigger] = useState(false);
|
||||
|
||||
// Utils
|
||||
const dateFormat =
|
||||
dateRange === "day" || dateRange === "recent" ? "MMM D, h A" : "MMM D";
|
||||
const { monitorId } = useParams();
|
||||
const theme = useTheme();
|
||||
const isAdmin = useIsAdmin();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [monitorData, monitorStats, monitorIsLoading, monitorNetworkError] =
|
||||
useFetchUptimeMonitorById({
|
||||
monitorId,
|
||||
dateRange,
|
||||
trigger,
|
||||
});
|
||||
|
||||
const monitor = monitorData?.monitor;
|
||||
|
||||
const [certificateExpiry, certificateIsLoading] = useCertificateFetch({
|
||||
monitor,
|
||||
monitorId,
|
||||
certificateDateFormat,
|
||||
uiTimezone,
|
||||
});
|
||||
|
||||
const monitorType = monitor?.type;
|
||||
|
||||
const [checks, checksCount, checksAreLoading, checksNetworkError] =
|
||||
useFetchChecksByMonitor({
|
||||
monitorId,
|
||||
type: monitorType,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange,
|
||||
filter: null,
|
||||
page,
|
||||
rowsPerPage,
|
||||
});
|
||||
|
||||
// Handlers
|
||||
const triggerUpdate = () => {
|
||||
setTrigger(!trigger);
|
||||
};
|
||||
|
||||
const handlePageChange = (_, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(event.target.value);
|
||||
};
|
||||
|
||||
if (monitorNetworkError || checksNetworkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{t("common.toasts.networkError")}
|
||||
</Typography>
|
||||
<Typography>{t("common.toasts.checkConnection")}</Typography>
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
// Empty view, displayed when loading is complete and there are no checks
|
||||
if (!monitorIsLoading && !checksAreLoading && checksCount === 0) {
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
|
||||
<MonitorDetailsControlHeader
|
||||
path={"uptime"}
|
||||
isAdmin={isAdmin}
|
||||
isLoading={monitorIsLoading}
|
||||
monitor={monitor}
|
||||
triggerUpdate={triggerUpdate}
|
||||
/>
|
||||
<UptimeStatusBoxes
|
||||
isLoading={monitorIsLoading}
|
||||
monitor={monitor}
|
||||
monitorStats={monitorStats}
|
||||
certificateExpiry={certificateExpiry}
|
||||
/>
|
||||
<MonitorTimeFrameHeader
|
||||
isLoading={monitorIsLoading}
|
||||
hasDateRange={true}
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
|
||||
<GenericFallback>
|
||||
<Typography>{t("distributedUptimeDetailsNoMonitorHistory")}</Typography>
|
||||
</GenericFallback>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorDetailsControlHeader
|
||||
path={"uptime"}
|
||||
isAdmin={isAdmin}
|
||||
isLoading={monitorIsLoading}
|
||||
monitor={monitor}
|
||||
triggerUpdate={triggerUpdate}
|
||||
/>
|
||||
<UptimeStatusBoxes
|
||||
isLoading={monitorIsLoading}
|
||||
monitor={monitor}
|
||||
monitorStats={monitorStats}
|
||||
certificateExpiry={certificateExpiry}
|
||||
/>
|
||||
<MonitorTimeFrameHeader
|
||||
isLoading={monitorIsLoading}
|
||||
hasDateRange={true}
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
<ChartBoxes
|
||||
isLoading={monitorIsLoading}
|
||||
monitorData={monitorData}
|
||||
uiTimezone={uiTimezone}
|
||||
dateRange={dateRange}
|
||||
dateFormat={dateFormat}
|
||||
/>
|
||||
<ResponseTimeChart
|
||||
isLoading={monitorIsLoading}
|
||||
groupedChecks={monitorData?.groupedChecks}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
<ResponseTable
|
||||
isLoading={checksAreLoading}
|
||||
checks={checks}
|
||||
uiTimezone={uiTimezone}
|
||||
page={page}
|
||||
setPage={handlePageChange}
|
||||
rowsPerPage={rowsPerPage}
|
||||
setRowsPerPage={handleChangeRowsPerPage}
|
||||
checksCount={checksCount}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default UptimeDetails;
|
||||
@@ -1,162 +0,0 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import FilterHeader from "@/Components/v1/FilterHeader/index.jsx";
|
||||
import { useMemo } from "react";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Filter Component
|
||||
*
|
||||
* A high-level component that provides filtering options for type, status, and state.
|
||||
* It allows users to select multiple options for each filter and reset the filters.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string[]} props.selectedTypes - An array of selected type values.
|
||||
* @param {function} props.setSelectedTypes - A function to set the selected type values.
|
||||
* @param {string[]} props.selectedStatus - An array of selected status values.
|
||||
* @param {function} props.setSelectedStatus - A function to set the selected status values.
|
||||
* @param {string[]} props.selectedState - An array of selected state values.
|
||||
* @param {function} props.setSelectedState - A function to set the selected state values.
|
||||
* @param {function} props.setToFilterStatus - A function to set the filter status based on selected status values.
|
||||
* @param {function} props.setToFilterActive - A function to set the filter active state based on selected state values.
|
||||
* @param {function} props.handleReset - A function to reset all filters.
|
||||
*
|
||||
* @returns {JSX.Element} The rendered Filter component.
|
||||
*/
|
||||
|
||||
const getTypeOptions = () => [
|
||||
{ value: "http", label: "HTTP(S)" },
|
||||
{ value: "ping", label: "Ping" },
|
||||
{ value: "docker", label: "Docker" },
|
||||
{ value: "port", label: "Port" },
|
||||
{ value: "game", label: "Game" },
|
||||
];
|
||||
|
||||
// These functions were moved inline to ensure translations are applied correctly
|
||||
|
||||
const Filter = ({
|
||||
selectedTypes,
|
||||
setSelectedTypes,
|
||||
selectedStatus,
|
||||
setSelectedStatus,
|
||||
selectedState,
|
||||
setSelectedState,
|
||||
setToFilterStatus,
|
||||
setToFilterActive,
|
||||
handleReset,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const typeOptions = getTypeOptions();
|
||||
// Create status options with translations
|
||||
const statusOptions = [
|
||||
{ value: "Up", label: t("monitorStatus.up") },
|
||||
{ value: "Down", label: t("monitorStatus.down") },
|
||||
];
|
||||
// Create state options with translations
|
||||
const stateOptions = [
|
||||
{ value: "Active", label: t("monitorState.active") },
|
||||
{ value: "Paused", label: t("monitorState.paused") },
|
||||
];
|
||||
|
||||
const handleTypeChange = (event) => {
|
||||
const selectedValues = event.target.value;
|
||||
setSelectedTypes(selectedValues.length > 0 ? selectedValues : undefined);
|
||||
};
|
||||
|
||||
const handleStatusChange = (event) => {
|
||||
const selectedValues = event.target.value;
|
||||
setSelectedStatus(selectedValues.length > 0 ? selectedValues : undefined);
|
||||
|
||||
if (selectedValues.length === 0 || selectedValues.length === 2) {
|
||||
setToFilterStatus(undefined);
|
||||
} else {
|
||||
setToFilterStatus(selectedValues[0] === "Up" ? "true" : "false");
|
||||
}
|
||||
};
|
||||
|
||||
const handleStateChange = (event) => {
|
||||
const selectedValues = event.target.value;
|
||||
setSelectedState(selectedValues);
|
||||
|
||||
if (selectedValues.length === 0 || selectedValues.length === 2) {
|
||||
setToFilterActive(undefined);
|
||||
} else {
|
||||
setToFilterActive(selectedValues[0] === "Active" ? "true" : "false");
|
||||
}
|
||||
};
|
||||
|
||||
const isFilterActive = useMemo(() => {
|
||||
return (
|
||||
(selectedTypes?.length ?? 0) > 0 ||
|
||||
(selectedState?.length ?? 0) > 0 ||
|
||||
(selectedStatus?.length ?? 0) > 0
|
||||
);
|
||||
}, [selectedState, selectedTypes, selectedStatus]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
m: theme.spacing(2),
|
||||
ml: theme.spacing(4),
|
||||
gap: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<FilterHeader
|
||||
header={t("type")}
|
||||
options={typeOptions}
|
||||
value={selectedTypes}
|
||||
onChange={handleTypeChange}
|
||||
/>
|
||||
<FilterHeader
|
||||
header={t("status")}
|
||||
options={statusOptions}
|
||||
value={selectedStatus}
|
||||
onChange={handleStatusChange}
|
||||
/>
|
||||
<FilterHeader
|
||||
header={t("state")}
|
||||
options={stateOptions}
|
||||
value={selectedState}
|
||||
onChange={handleStateChange}
|
||||
/>
|
||||
<Button
|
||||
color={theme.palette.primary.contrastText}
|
||||
onClick={handleReset}
|
||||
variant="contained"
|
||||
endIcon={
|
||||
<Icon
|
||||
name="X"
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
sx={{
|
||||
visibility: isFilterActive ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Filter.propTypes = {
|
||||
selectedTypes: PropTypes.arrayOf(PropTypes.string),
|
||||
setSelectedTypes: PropTypes.func.isRequired,
|
||||
selectedStatus: PropTypes.arrayOf(PropTypes.string),
|
||||
setSelectedStatus: PropTypes.func.isRequired,
|
||||
selectedState: PropTypes.arrayOf(PropTypes.string),
|
||||
setSelectedState: PropTypes.func.isRequired,
|
||||
setToFilterStatus: PropTypes.func.isRequired,
|
||||
setToFilterActive: PropTypes.func.isRequired,
|
||||
handleReset: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Filter;
|
||||
@@ -1,46 +0,0 @@
|
||||
import { CircularProgress, Box } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
const LoadingSpinner = ({ shouldRender }) => {
|
||||
const theme = useTheme();
|
||||
if (shouldRender === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="absolute"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
opacity: 0.8,
|
||||
zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
height="100%"
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
sx={{
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 101,
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: theme.palette.accent.main,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
LoadingSpinner.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default LoadingSpinner;
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import Search from "@/Components/v1/Inputs/Search/index.jsx";
|
||||
import { Box } from "@mui/material";
|
||||
import useDebounce from "../../Hooks/useDebounce.jsx";
|
||||
import { useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const SearchComponent = ({ monitors = [], onSearchChange, setIsSearching }) => {
|
||||
const isFirstRender = useRef(true);
|
||||
const [localSearch, setLocalSearch] = useState("");
|
||||
const debouncedSearch = useDebounce(localSearch, 500);
|
||||
useEffect(() => {
|
||||
if (isFirstRender.current === true) {
|
||||
isFirstRender.current = false;
|
||||
return;
|
||||
}
|
||||
onSearchChange(debouncedSearch);
|
||||
setIsSearching(false);
|
||||
}, [debouncedSearch, onSearchChange, setIsSearching]);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
setLocalSearch(value);
|
||||
setIsSearching(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
width="25%"
|
||||
minWidth={150}
|
||||
ml="auto"
|
||||
mt={2}
|
||||
>
|
||||
<Search
|
||||
options={monitors}
|
||||
filteredBy="name"
|
||||
inputValue={localSearch}
|
||||
handleInputChange={handleSearch}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
SearchComponent.propTypes = {
|
||||
monitors: PropTypes.array,
|
||||
onSearchChange: PropTypes.func,
|
||||
setIsSearching: PropTypes.func,
|
||||
};
|
||||
|
||||
export default SearchComponent;
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Skeleton, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={36}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="15%"
|
||||
height={36}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.spacing(12)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
</Stack>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height="100%"
|
||||
flex={1}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -1,42 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Stack } from "@mui/material";
|
||||
import StatusBox from "./statusBox.jsx";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SkeletonLayout from "./skeleton.jsx";
|
||||
|
||||
const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
if (!shouldRender) return <SkeletonLayout shouldRender={shouldRender} />;
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(8)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<StatusBox
|
||||
title={t("monitorStatus.up")}
|
||||
status="up"
|
||||
value={monitorsSummary?.upMonitors ?? 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title={t("monitorStatus.down")}
|
||||
status="down"
|
||||
value={monitorsSummary?.downMonitors ?? 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title={t("monitorStatus.paused")}
|
||||
status="paused"
|
||||
value={monitorsSummary?.pausedMonitors ?? 0}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
StatusBoxes.propTypes = {
|
||||
monitorsSummary: PropTypes.object,
|
||||
shouldRender: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default StatusBoxes;
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Skeleton, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(12)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -1,115 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import Background from "@/assets/Images/background-grid.svg?react";
|
||||
|
||||
const StatusBox = ({ title, value, status }) => {
|
||||
const theme = useTheme();
|
||||
let sharedStyles = {
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
opacity: 0.5,
|
||||
"& svg path": { stroke: theme.palette.primary.contrastTextTertiary },
|
||||
};
|
||||
|
||||
let color;
|
||||
let icon;
|
||||
if (status === "up") {
|
||||
color = theme.palette.success.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(4) }}>
|
||||
<Icon
|
||||
name="ArrowUpRight"
|
||||
size={20}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
} else if (status === "down") {
|
||||
color = theme.palette.error.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: theme.spacing(2) }}>
|
||||
<Icon
|
||||
name="ArrowUpRight"
|
||||
size={20}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
} else if (status === "paused") {
|
||||
color = theme.palette.warning.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<Icon
|
||||
name="Clock"
|
||||
size={20}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
flex={1}
|
||||
border={1}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
p={theme.spacing(8)}
|
||||
overflow="hidden"
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-10%"
|
||||
left="5%"
|
||||
>
|
||||
<Background />
|
||||
</Box>
|
||||
<Stack direction="column">
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Typography
|
||||
variant={"h2"}
|
||||
textTransform="uppercase"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{icon}
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
fontSize={theme.typography.h1.fontSize}
|
||||
fontWeight={600}
|
||||
color={color}
|
||||
gap={theme.spacing(1)}
|
||||
>
|
||||
{value}
|
||||
|
||||
<Typography
|
||||
fontSize={theme.typography.label.fontSize}
|
||||
fontWeight={300}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
sx={{
|
||||
opacity: 0.3,
|
||||
}}
|
||||
>
|
||||
#
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
StatusBox.propTypes = {
|
||||
title: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
status: PropTypes.string,
|
||||
};
|
||||
|
||||
export default StatusBox;
|
||||
@@ -1,241 +0,0 @@
|
||||
// Components
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import Host from "@/Components/v1/Host/index.jsx";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
import BarChart from "@/Components/v1/Charts/BarChart/index.jsx";
|
||||
import ActionsMenu from "@/Components/v1/ActionsMenu/index.jsx";
|
||||
|
||||
import LoadingSpinner from "../LoadingSpinner/index.jsx";
|
||||
import TableSkeleton from "@/Components/v1/Table/skeleton.jsx";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* UptimeDataTable displays a table of uptime monitors with sorting, searching, and action capabilities
|
||||
* @param {Object} props - Component props
|
||||
* @param {boolean} props.isAdmin - Whether the current user has admin privileges
|
||||
* @param {boolean} props.isLoading - Loading state of the table
|
||||
* @param {Array<{
|
||||
* id: string,
|
||||
* url: string,
|
||||
* title: string,
|
||||
* percentage: number,
|
||||
* percentageColor: string,
|
||||
* monitor: {
|
||||
* id: string,
|
||||
* type: string,
|
||||
* checks: Array
|
||||
* }
|
||||
* }>} props.monitors - Array of monitor objects to display
|
||||
* @param {number} props.monitorCount - Total count of monitors
|
||||
* @param {Object} props.sort - Current sort configuration
|
||||
* @param {string} props.sort.field - Field to sort by
|
||||
* @param {'asc'|'desc'} props.sort.order - Sort direction
|
||||
* @param {Function} props.setSort - Callback to update sort configuration
|
||||
* @param {string} props.search - Current search query
|
||||
* @param {Function} props.setSearch - Callback to update search query
|
||||
* @param {boolean} props.isSearching - Whether a search is in progress
|
||||
* @param {Function} props.setIsLoading - Callback to update loading state
|
||||
* @param {Function} props.triggerUpdate - Callback to trigger a data refresh
|
||||
* @returns {JSX.Element} Rendered component
|
||||
*/
|
||||
const UptimeDataTable = ({
|
||||
isAdmin,
|
||||
isSearching,
|
||||
filteredMonitors,
|
||||
sort,
|
||||
setSort,
|
||||
triggerUpdate,
|
||||
monitorsAreLoading,
|
||||
}) => {
|
||||
// Utils
|
||||
const navigate = useNavigate();
|
||||
const { determineState } = useMonitorUtils();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Local state
|
||||
// Handlers
|
||||
const handleSort = (field) => {
|
||||
let order = "";
|
||||
if (sort?.field !== field) {
|
||||
order = "desc";
|
||||
} else {
|
||||
order = sort?.order === "asc" ? "desc" : "asc";
|
||||
}
|
||||
setSort({ field, order });
|
||||
};
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "name",
|
||||
content: (
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
alignItems="center"
|
||||
direction="row"
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
{t("host")}
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
style={{
|
||||
visibility: sort?.field === "name" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort?.order === "asc" ? (
|
||||
<Icon
|
||||
name="ArrowUp"
|
||||
size={18}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
name="ArrowDown"
|
||||
size={18}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
),
|
||||
render: (row) => (
|
||||
<Host
|
||||
key={row.id}
|
||||
url={row.url}
|
||||
title={row.name}
|
||||
percentageColor={row.percentageColor}
|
||||
percentage={row.percentage}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
alignItems="center"
|
||||
display={"inline-flex"}
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
{" "}
|
||||
{t("status")}
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
style={{
|
||||
visibility: sort?.field === "status" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort?.order === "asc" ? (
|
||||
<Icon
|
||||
name="ArrowUp"
|
||||
size={18}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
name="ArrowDown"
|
||||
size={18}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
),
|
||||
render: (row) => {
|
||||
const status = determineState(row.monitor);
|
||||
return (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "responseTime",
|
||||
content: t("responseTime"),
|
||||
render: (row) => (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
>
|
||||
<BarChart checks={row.monitor.checks.slice().reverse()} />
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
content: t("type"),
|
||||
render: (row) => (
|
||||
<span style={{ textTransform: "uppercase" }}>
|
||||
{row.monitor.type === "http" ? "HTTP(s)" : row.monitor.type}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
content: t("actions"),
|
||||
render: (row) => (
|
||||
<ActionsMenu
|
||||
monitor={row.monitor}
|
||||
isAdmin={isAdmin}
|
||||
updateRowCallback={triggerUpdate}
|
||||
pauseCallback={triggerUpdate}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (monitorsAreLoading) {
|
||||
return <TableSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<LoadingSpinner shouldRender={isSearching} />
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={filteredMonitors}
|
||||
config={{
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover td": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
transition: "background-color .3s ease",
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
navigate(`/uptime/${row.id}`);
|
||||
},
|
||||
emptyView: "No monitors found",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
UptimeDataTable.propTypes = {
|
||||
isSearching: PropTypes.bool,
|
||||
setSort: PropTypes.func,
|
||||
setSearch: PropTypes.func,
|
||||
triggerUpdate: PropTypes.func,
|
||||
debouncedSearch: PropTypes.string,
|
||||
onSearchChange: PropTypes.func,
|
||||
isAdmin: PropTypes.bool,
|
||||
monitors: PropTypes.array,
|
||||
filteredMonitors: PropTypes.array,
|
||||
monitorCount: PropTypes.number,
|
||||
monitorsAreLoading: PropTypes.bool,
|
||||
sort: PropTypes.shape({
|
||||
field: PropTypes.string,
|
||||
order: PropTypes.oneOf(["asc", "desc"]),
|
||||
}),
|
||||
};
|
||||
|
||||
export default UptimeDataTable;
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const useDebounce = (value, delay) => {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
return debouncedValue;
|
||||
};
|
||||
|
||||
export default useDebounce;
|
||||
@@ -1,225 +0,0 @@
|
||||
// Required Data
|
||||
// 1. Monitor summary
|
||||
// 2. List of monitors filtered by search term with 25 checks each
|
||||
// 2a.List of monitors must have the total number of monitors that match.
|
||||
|
||||
// Components
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import Greeting from "../../../Utils/greeting.jsx";
|
||||
import StatusBoxes from "./Components/StatusBoxes/index.jsx";
|
||||
import UptimeDataTable from "./Components/UptimeDataTable/index.jsx";
|
||||
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
|
||||
import CreateMonitorHeader from "@/Components/v1/MonitorCreateHeader/index.jsx";
|
||||
import SearchComponent from "./Components/SearchComponent/index.jsx";
|
||||
import Filter from "./Components/Filter/index.jsx";
|
||||
import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
|
||||
|
||||
import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx";
|
||||
|
||||
// MUI Components
|
||||
import { Stack, Box, Button } from "@mui/material";
|
||||
// Utils
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { setRowsPerPage } from "../../../Features/UI/uiSlice.js";
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
useFetchMonitorsWithChecks,
|
||||
useFetchMonitorsByTeamId,
|
||||
} from "@/Hooks/monitorHooks.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const TYPES = ["http", "ping", "docker", "port", "game"];
|
||||
const CreateMonitorButton = ({ shouldRender }) => {
|
||||
// Utils
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
if (shouldRender === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box alignSelf="flex-end">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => {
|
||||
navigate("/uptime/create");
|
||||
}}
|
||||
>
|
||||
{t("createNew")}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
CreateMonitorButton.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
};
|
||||
|
||||
const UptimeMonitors = () => {
|
||||
// Redux state
|
||||
const rowsPerPage = useSelector((state) => state.ui?.monitors?.rowsPerPage ?? 10);
|
||||
|
||||
// Local state
|
||||
const [search, setSearch] = useState(undefined);
|
||||
const [page, setPage] = useState(undefined);
|
||||
const [sort, setSort] = useState(undefined);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false);
|
||||
const [selectedTypes, setSelectedTypes] = useState(undefined);
|
||||
const [selectedState, setSelectedState] = useState(undefined);
|
||||
const [selectedStatus, setSelectedStatus] = useState(undefined);
|
||||
const [toFilterStatus, setToFilterStatus] = useState(undefined);
|
||||
const [toFilterActive, setToFilterActive] = useState(undefined);
|
||||
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const isAdmin = useIsAdmin();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const BREADCRUMBS = [{ name: t("menu.uptime"), path: "/uptime" }];
|
||||
|
||||
// Handlers
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: "monitors",
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const triggerUpdate = useCallback(() => {
|
||||
setMonitorUpdateTrigger((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedState(undefined);
|
||||
setSelectedTypes(undefined);
|
||||
setSelectedStatus(undefined);
|
||||
setToFilterStatus(undefined);
|
||||
setToFilterActive(undefined);
|
||||
};
|
||||
|
||||
const filterLookup = new Map([
|
||||
[toFilterStatus, "status"],
|
||||
[toFilterActive, "isActive"],
|
||||
]);
|
||||
|
||||
const activeFilter = [...filterLookup].find(([key]) => key !== undefined);
|
||||
const field = activeFilter?.[1] || (search ? "name" : sort?.field);
|
||||
const filter = activeFilter?.[0] || search;
|
||||
|
||||
const effectiveTypes = selectedTypes?.length ? selectedTypes : TYPES;
|
||||
|
||||
const [
|
||||
summary,
|
||||
monitorsWithChecks,
|
||||
monitorsWithChecksCount,
|
||||
monitorsWithChecksIsLoading,
|
||||
networkError,
|
||||
] = useFetchMonitorsWithChecks({
|
||||
types: effectiveTypes,
|
||||
limit: 25,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
filter: filter,
|
||||
field: field,
|
||||
order: sort?.order,
|
||||
monitorUpdateTrigger,
|
||||
});
|
||||
|
||||
const [monitors, listIsLoading, listNetworkError] = useFetchMonitorsByTeamId({
|
||||
type: ["http", "ping", "docker", "port", "game"],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isSearching) {
|
||||
setPage(undefined);
|
||||
}
|
||||
}, [isSearching]);
|
||||
|
||||
const isLoading = monitorsWithChecksIsLoading;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageStateWrapper
|
||||
networkError={networkError}
|
||||
isLoading={isLoading}
|
||||
items={monitors}
|
||||
type="uptimeMonitor"
|
||||
fallbackLink="/uptime/create"
|
||||
>
|
||||
<Stack
|
||||
className="monitors"
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<CreateMonitorHeader
|
||||
isAdmin={isAdmin}
|
||||
isLoading={isLoading}
|
||||
path="/uptime/create"
|
||||
bulkPath="/uptime/bulk-import"
|
||||
/>
|
||||
<Greeting type="uptime" />
|
||||
<StatusBoxes
|
||||
monitorsSummary={summary}
|
||||
shouldRender={!monitorsWithChecksIsLoading}
|
||||
/>
|
||||
|
||||
<Stack direction={"row"}>
|
||||
<MonitorCountHeader
|
||||
isLoading={monitorsWithChecksIsLoading}
|
||||
monitorCount={summary?.totalMonitors}
|
||||
/>
|
||||
<Filter
|
||||
selectedTypes={selectedTypes}
|
||||
setSelectedTypes={setSelectedTypes}
|
||||
selectedStatus={selectedStatus}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
selectedState={selectedState}
|
||||
setSelectedState={setSelectedState}
|
||||
setToFilterStatus={setToFilterStatus}
|
||||
setToFilterActive={setToFilterActive}
|
||||
handleReset={handleReset}
|
||||
/>
|
||||
<SearchComponent
|
||||
monitors={monitors}
|
||||
onSearchChange={setSearch}
|
||||
setIsSearching={setIsSearching}
|
||||
/>
|
||||
</Stack>
|
||||
<UptimeDataTable
|
||||
isAdmin={isAdmin}
|
||||
isSearching={isSearching}
|
||||
filteredMonitors={monitorsWithChecks}
|
||||
sort={sort}
|
||||
setSort={setSort}
|
||||
monitorsAreLoading={monitorsWithChecksIsLoading}
|
||||
triggerUpdate={triggerUpdate}
|
||||
/>
|
||||
<Pagination
|
||||
itemCount={monitorsWithChecksCount}
|
||||
paginationLabel="monitors"
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Stack>
|
||||
</PageStateWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UptimeMonitors;
|
||||
@@ -25,9 +25,9 @@ import PageSpeedDetails from "../Pages/PageSpeed/Details/";
|
||||
import PageSpeedCreate from "../Pages/PageSpeed/Create/index.jsx";
|
||||
|
||||
// Infrastructure
|
||||
import Infrastructure from "../Pages/Infrastructure/Monitors/index.jsx";
|
||||
import Infrastructure from "../Pages/Infrastructure/Monitors";
|
||||
import InfrastructureCreate from "../Pages/Infrastructure/Create/index.jsx";
|
||||
import InfrastructureDetails from "../Pages/Infrastructure/Details/index.jsx";
|
||||
import InfrastructureDetails from "../Pages/Infrastructure/Details/index";
|
||||
|
||||
// Server Status
|
||||
import ServerUnreachable from "../Pages/ServerUnreachable.jsx";
|
||||
@@ -43,7 +43,7 @@ import CreateStatus from "../Pages/StatusPage/Create/index.jsx";
|
||||
import StatusPages from "../Pages/StatusPage/StatusPages/index.jsx";
|
||||
import Status from "../Pages/StatusPage/Status/index.jsx";
|
||||
|
||||
import Notifications from "../Pages/Notifications/index.jsx";
|
||||
import Notifications from "../Pages/Notifications";
|
||||
import CreateNotifications from "../Pages/Notifications/create/index.jsx";
|
||||
|
||||
// Settings
|
||||
@@ -147,7 +147,13 @@ const Routes = () => {
|
||||
/>
|
||||
<Route
|
||||
path="infrastructure"
|
||||
element={<Infrastructure />}
|
||||
element={
|
||||
<>
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<Infrastructure />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="infrastructure/create"
|
||||
@@ -159,7 +165,13 @@ const Routes = () => {
|
||||
/>
|
||||
<Route
|
||||
path="infrastructure/:monitorId"
|
||||
element={<InfrastructureDetails />}
|
||||
element={
|
||||
<>
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<InfrastructureDetails />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="checks/:monitorId?"
|
||||
@@ -198,7 +210,13 @@ const Routes = () => {
|
||||
|
||||
<Route
|
||||
path="notifications"
|
||||
element={<Notifications />}
|
||||
element={
|
||||
<>
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<Notifications />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="notifications/create"
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface CheckCpuInfo {
|
||||
physical_core?: number;
|
||||
logical_core?: number;
|
||||
frequency?: number;
|
||||
current_frequency?: number;
|
||||
temperature?: number[];
|
||||
free_percent?: number;
|
||||
usage_percent?: number;
|
||||
@@ -32,6 +33,7 @@ export interface CheckHostInfo {
|
||||
os?: string;
|
||||
platform?: string;
|
||||
kernel_version?: string;
|
||||
pretty_name?: string;
|
||||
}
|
||||
|
||||
export interface CheckCaptureInfo {
|
||||
@@ -42,11 +44,18 @@ export interface CheckCaptureInfo {
|
||||
export interface CheckDiskInfo {
|
||||
device?: string;
|
||||
mountpoint?: string;
|
||||
read_speed_bytes?: number;
|
||||
write_speed_bytes?: number;
|
||||
total_bytes?: number;
|
||||
free_bytes?: number;
|
||||
used_bytes?: number;
|
||||
usage_percent?: number;
|
||||
total_inodes?: number;
|
||||
free_inodes?: number;
|
||||
used_inodes?: number;
|
||||
inodes_usage_percent?: number;
|
||||
read_bytes?: number;
|
||||
write_bytes?: number;
|
||||
read_time?: number;
|
||||
write_time?: number;
|
||||
}
|
||||
|
||||
export interface CheckErrorInfo {
|
||||
@@ -183,7 +192,7 @@ export interface HardwareChecksResult {
|
||||
totalChecks: number;
|
||||
};
|
||||
checks: Array<{
|
||||
_id: string;
|
||||
bucketDate: string;
|
||||
avgCpuUsage: number;
|
||||
avgMemoryUsage: number;
|
||||
avgTemperature: number[];
|
||||
|
||||
@@ -107,3 +107,51 @@ export interface PageSpeedDetailsResponse {
|
||||
monitor: MonitorWithChecks;
|
||||
monitorStats: MonitorStats | null;
|
||||
}
|
||||
|
||||
export interface HardwareDiskStats {
|
||||
name: string;
|
||||
readSpeed: number;
|
||||
writeSpeed: number;
|
||||
totalBytes: number;
|
||||
freeBytes: number;
|
||||
usagePercent: number;
|
||||
}
|
||||
|
||||
export interface HardwareNetStats {
|
||||
name: string;
|
||||
bytesSentPerSecond: number;
|
||||
deltaBytesRecv: number;
|
||||
deltaPacketsSent: number;
|
||||
deltaPacketsRecv: number;
|
||||
deltaErrIn: number;
|
||||
deltaErrOut: number;
|
||||
deltaDropIn: number;
|
||||
deltaDropOut: number;
|
||||
deltaFifoIn: number;
|
||||
deltaFifoOut: number;
|
||||
}
|
||||
|
||||
export interface HardwareCheckStats {
|
||||
bucketDate: string;
|
||||
avgCpuUsage: number;
|
||||
avgMemoryUsage: number;
|
||||
avgTemperature: number[];
|
||||
disks: HardwareDiskStats[];
|
||||
net: HardwareNetStats[];
|
||||
}
|
||||
|
||||
export interface HardwareStats {
|
||||
aggregateData: {
|
||||
totalChecks: number;
|
||||
};
|
||||
upChecks: {
|
||||
totalChecks: number;
|
||||
};
|
||||
checks: HardwareCheckStats[];
|
||||
}
|
||||
|
||||
export interface HardwareDetailsResponse {
|
||||
monitor: Monitor;
|
||||
stats: HardwareStats;
|
||||
monitorStats: MonitorStats | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export const NotificationChannels = [
|
||||
"email",
|
||||
"slack",
|
||||
"discord",
|
||||
"webhook",
|
||||
"pager_duty",
|
||||
"matrix",
|
||||
] as const;
|
||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
userId: string;
|
||||
teamId: string;
|
||||
type: NotificationChannel;
|
||||
notificationName: string;
|
||||
address?: string;
|
||||
phone?: string;
|
||||
homeserverUrl?: string;
|
||||
roomId?: string;
|
||||
accessToken?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { CheckDiskInfo, CheckHostInfo } from "@/Types/Check";
|
||||
|
||||
export const getFrequency = (frequency: number | undefined): string => {
|
||||
if (!frequency) return "N/A";
|
||||
const ghz = (frequency / 1000).toFixed(2);
|
||||
return `${ghz} GHz`;
|
||||
};
|
||||
|
||||
export const getCores = (cores: number | undefined) => {
|
||||
if (!cores) return "N/A";
|
||||
if (cores === 1) return `${cores} core`;
|
||||
return `${cores} cores`;
|
||||
};
|
||||
|
||||
export const getAvgTemp = (temps: number[]): string => {
|
||||
if (!temps || temps.length === 0) return "N/A";
|
||||
const avgTemp = temps.reduce((a, b) => a + b, 0) / temps.length;
|
||||
return `${avgTemp?.toFixed(2)} °C`;
|
||||
};
|
||||
|
||||
export const getGbs = (bytes: number | undefined): string => {
|
||||
if (!bytes) {
|
||||
return "N/A";
|
||||
}
|
||||
if (bytes === 0) {
|
||||
return "0 GB";
|
||||
}
|
||||
|
||||
const GB = bytes / (1024 * 1024 * 1024);
|
||||
const MB = bytes / (1024 * 1024);
|
||||
|
||||
if (GB >= 1) {
|
||||
return GB.toFixed(2) + " GB";
|
||||
} else {
|
||||
return MB.toFixed(2) + " MB";
|
||||
}
|
||||
};
|
||||
|
||||
export const getDiskTotalGbs = (disk?: Partial<CheckDiskInfo>[]): string => {
|
||||
if (!disk) {
|
||||
return getGbs(0);
|
||||
}
|
||||
const totalBytes = disk?.reduce((acc, disk) => acc + (disk.total_bytes || 0), 0) || 0;
|
||||
return getGbs(totalBytes);
|
||||
};
|
||||
|
||||
export const getOsAndPlatform = (hostInfo: CheckHostInfo | undefined): string => {
|
||||
if (!hostInfo) {
|
||||
return "N/A";
|
||||
}
|
||||
const os = hostInfo?.pretty_name || hostInfo?.os || "N/A";
|
||||
const platform = hostInfo?.platform || "N/A";
|
||||
return `${os} (${platform})`;
|
||||
};
|
||||
@@ -276,6 +276,73 @@
|
||||
"weight": "Weight"
|
||||
}
|
||||
}
|
||||
},
|
||||
"infrastructure": {
|
||||
"table": {
|
||||
"headers": {
|
||||
"cpu": "CPU",
|
||||
"disk": "Disk",
|
||||
"memory": "Memory"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"labels": {
|
||||
"overview": "Overview",
|
||||
"network": "Network"
|
||||
}
|
||||
},
|
||||
"statBoxes": {
|
||||
"cpuPhysical": "CPU (Physical)",
|
||||
"cpuLogical": "CPU (Logical)",
|
||||
"cpuFrequency": "CPU Frequency",
|
||||
"avgCpuTemperature": "Average CPU Temperature",
|
||||
"memory": "Memory",
|
||||
"disk": "Disk",
|
||||
"os": "OS"
|
||||
},
|
||||
"gauges": {
|
||||
"cpu": {
|
||||
"lowerLabel": "Max frequency",
|
||||
"title": "CPU usage",
|
||||
"upperLabel": "Current frequency"
|
||||
},
|
||||
"disk": {
|
||||
"lowerLabel": "Free",
|
||||
"title": "Disk {{idx}} usage",
|
||||
"upperLabel": "Used"
|
||||
},
|
||||
"memory": {
|
||||
"lowerLabel": "Free",
|
||||
"title": "Memory usage",
|
||||
"upperLabel": "Used"
|
||||
}
|
||||
},
|
||||
"charts": {
|
||||
"labels": {
|
||||
"memory": "Memory usage",
|
||||
"cpu": "CPU usage",
|
||||
"temp": "Temp",
|
||||
"disk": "Disk usage",
|
||||
"netBytesSent": "{{name}} - Bytes Sent",
|
||||
"netBytesRecv": "{{name}} - Bytes Received"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"fallback": {
|
||||
"actionButton": "Create a channel",
|
||||
"checks": [
|
||||
"Alert teams about downtime or performance issues",
|
||||
"Let engineers know when incidents happen",
|
||||
"Keep administrators informed of system changes"
|
||||
],
|
||||
"title": "Notification channles are used to:"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"destination": "Destination"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"incidentsTableNoIncidents": "No incidents recorded",
|
||||
|
||||
@@ -93,7 +93,7 @@ class MonitorController {
|
||||
const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent";
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorService.getHardwareDetailsById({
|
||||
const data = await this.monitorService.getHardwareDetailsById({
|
||||
teamId,
|
||||
monitorId,
|
||||
dateRange,
|
||||
@@ -102,7 +102,7 @@ class MonitorController {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Hardware details retrieved successfully",
|
||||
data: monitor,
|
||||
data: data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -71,6 +71,7 @@ const cpuSchema = new Schema<CheckCpuInfo>(
|
||||
physical_core: { type: Number, default: 0 },
|
||||
logical_core: { type: Number, default: 0 },
|
||||
frequency: { type: Number, default: 0 },
|
||||
current_frequency: { type: Number, default: 0 },
|
||||
temperature: { type: [Number], default: [] },
|
||||
free_percent: { type: Number, default: 0 },
|
||||
usage_percent: { type: Number, default: 0 },
|
||||
@@ -92,11 +93,18 @@ const diskSchema = new Schema<CheckDiskInfo>(
|
||||
{
|
||||
device: { type: String, default: "" },
|
||||
mountpoint: { type: String, default: "" },
|
||||
read_speed_bytes: { type: Number, default: 0 },
|
||||
write_speed_bytes: { type: Number, default: 0 },
|
||||
total_bytes: { type: Number, default: 0 },
|
||||
free_bytes: { type: Number, default: 0 },
|
||||
used_bytes: { type: Number, default: 0 },
|
||||
usage_percent: { type: Number, default: 0 },
|
||||
total_inodes: { type: Number, default: 0 },
|
||||
free_inodes: { type: Number, default: 0 },
|
||||
used_inodes: { type: Number, default: 0 },
|
||||
inodes_usage_percent: { type: Number, default: 0 },
|
||||
read_bytes: { type: Number, default: 0 },
|
||||
write_bytes: { type: Number, default: 0 },
|
||||
read_time: { type: Number, default: 0 },
|
||||
write_time: { type: Number, default: 0 },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
@@ -106,6 +114,7 @@ const hostSchema = new Schema<CheckHostInfo>(
|
||||
os: { type: String, default: "" },
|
||||
platform: { type: String, default: "" },
|
||||
kernel_version: { type: String, default: "" },
|
||||
pretty_name: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
@@ -117,11 +117,18 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
(disks ?? []).map((disk) => ({
|
||||
device: disk?.device ?? "",
|
||||
mountpoint: disk?.mountpoint ?? "",
|
||||
read_speed_bytes: disk?.read_speed_bytes ?? 0,
|
||||
write_speed_bytes: disk?.write_speed_bytes ?? 0,
|
||||
total_bytes: disk?.total_bytes ?? 0,
|
||||
free_bytes: disk?.free_bytes ?? 0,
|
||||
used_bytes: disk?.used_bytes ?? 0,
|
||||
usage_percent: disk?.usage_percent ?? 0,
|
||||
total_inodes: disk?.total_inodes ?? 0,
|
||||
free_inodes: disk?.free_inodes ?? 0,
|
||||
used_inodes: disk?.used_inodes ?? 0,
|
||||
inodes_usage_percent: disk?.inodes_usage_percent ?? 0,
|
||||
read_bytes: disk?.read_bytes ?? 0,
|
||||
write_bytes: disk?.write_bytes ?? 0,
|
||||
read_time: disk?.read_time ?? 0,
|
||||
write_time: disk?.write_time ?? 0,
|
||||
}));
|
||||
|
||||
const mapErrors = (errors?: CheckErrorInfo[]): CheckErrorInfo[] =>
|
||||
@@ -200,8 +207,30 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
return documents.map((doc) => this.toEntity(doc));
|
||||
};
|
||||
|
||||
private toDocument = (check: Partial<Check>): CheckDocument => {
|
||||
// Map id to _id for MongoDB storage
|
||||
const { id, metadata, ...rest } = check;
|
||||
return {
|
||||
_id: id ? new mongoose.Types.ObjectId(id) : new mongoose.Types.ObjectId(),
|
||||
metadata: metadata
|
||||
? {
|
||||
monitorId: new mongoose.Types.ObjectId(metadata.monitorId),
|
||||
teamId: new mongoose.Types.ObjectId(metadata.teamId),
|
||||
type: metadata.type,
|
||||
}
|
||||
: {
|
||||
monitorId: new mongoose.Types.ObjectId(),
|
||||
teamId: new mongoose.Types.ObjectId(),
|
||||
type: "http",
|
||||
},
|
||||
...rest,
|
||||
} as unknown as CheckDocument;
|
||||
};
|
||||
|
||||
createChecks = async (checks: Check[]) => {
|
||||
return await CheckModel.insertMany(checks);
|
||||
const docs = checks.map((check) => this.toDocument(check));
|
||||
const inserted = await CheckModel.insertMany(docs);
|
||||
return this.mapDocuments(inserted as unknown as CheckDocument[]);
|
||||
};
|
||||
|
||||
findByMonitorId = async (
|
||||
@@ -500,7 +529,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
};
|
||||
|
||||
const checks = (hardwareMetrics ?? []).map((metric) => ({
|
||||
_id: metric._id,
|
||||
bucketDate: metric._id,
|
||||
avgCpuUsage: metric.avgCpuUsage ?? 0,
|
||||
avgMemoryUsage: metric.avgMemoryUsage ?? 0,
|
||||
avgTemperature: metric.avgTemperature ?? [],
|
||||
@@ -606,8 +635,8 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
as: "dIdx",
|
||||
in: {
|
||||
name: { $concat: ["disk", { $toString: "$$dIdx" }] },
|
||||
readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_speed_bytes", "$$dIdx"] } } } },
|
||||
writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_speed_bytes", "$$dIdx"] } } } },
|
||||
readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_bytes", "$$dIdx"] } } } },
|
||||
writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_bytes", "$$dIdx"] } } } },
|
||||
totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } },
|
||||
freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } },
|
||||
usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } },
|
||||
|
||||
@@ -257,9 +257,12 @@ export class MonitorService implements IMonitorService {
|
||||
checks: checksData.checks,
|
||||
};
|
||||
|
||||
const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id);
|
||||
|
||||
return {
|
||||
...monitor,
|
||||
monitor,
|
||||
stats,
|
||||
monitorStats,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface CheckCpuInfo {
|
||||
physical_core?: number;
|
||||
logical_core?: number;
|
||||
frequency?: number;
|
||||
current_frequency?: number;
|
||||
temperature?: number[];
|
||||
free_percent?: number;
|
||||
usage_percent?: number;
|
||||
@@ -28,6 +29,7 @@ export interface CheckHostInfo {
|
||||
os?: string;
|
||||
platform?: string;
|
||||
kernel_version?: string;
|
||||
pretty_name?: string;
|
||||
}
|
||||
|
||||
export interface CheckCaptureInfo {
|
||||
@@ -38,11 +40,18 @@ export interface CheckCaptureInfo {
|
||||
export interface CheckDiskInfo {
|
||||
device?: string;
|
||||
mountpoint?: string;
|
||||
read_speed_bytes?: number;
|
||||
write_speed_bytes?: number;
|
||||
total_bytes?: number;
|
||||
free_bytes?: number;
|
||||
used_bytes?: number;
|
||||
usage_percent?: number;
|
||||
total_inodes?: number;
|
||||
free_inodes?: number;
|
||||
used_inodes?: number;
|
||||
inodes_usage_percent?: number;
|
||||
read_bytes?: number;
|
||||
write_bytes?: number;
|
||||
read_time?: number;
|
||||
write_time?: number;
|
||||
}
|
||||
|
||||
export interface CheckErrorInfo {
|
||||
@@ -127,7 +136,7 @@ export interface HardwareChecksResult {
|
||||
totalChecks: number;
|
||||
};
|
||||
checks: Array<{
|
||||
_id: string;
|
||||
bucketDate: string;
|
||||
avgCpuUsage: number;
|
||||
avgMemoryUsage: number;
|
||||
avgTemperature: number[];
|
||||
|
||||
+45
-35
@@ -75,42 +75,52 @@ export interface UptimeDetailsResult {
|
||||
monitorStats: import("./monitorStats.js").MonitorStats | null;
|
||||
}
|
||||
|
||||
export interface HardwareDetailsResult extends Monitor {
|
||||
stats: {
|
||||
aggregateData: {
|
||||
totalChecks: number;
|
||||
};
|
||||
upChecks: {
|
||||
totalChecks: number;
|
||||
};
|
||||
checks: Array<{
|
||||
_id: string;
|
||||
avgCpuUsage: number;
|
||||
avgMemoryUsage: number;
|
||||
avgTemperature: number[];
|
||||
disks: Array<{
|
||||
name: string;
|
||||
readSpeed: number;
|
||||
writeSpeed: number;
|
||||
totalBytes: number;
|
||||
freeBytes: number;
|
||||
usagePercent: number;
|
||||
}>;
|
||||
net: Array<{
|
||||
name: string;
|
||||
bytesSentPerSecond: number;
|
||||
deltaBytesRecv: number;
|
||||
deltaPacketsSent: number;
|
||||
deltaPacketsRecv: number;
|
||||
deltaErrIn: number;
|
||||
deltaErrOut: number;
|
||||
deltaDropIn: number;
|
||||
deltaDropOut: number;
|
||||
deltaFifoIn: number;
|
||||
deltaFifoOut: number;
|
||||
}>;
|
||||
}>;
|
||||
export interface HardwareDiskStats {
|
||||
name: string;
|
||||
readSpeed: number;
|
||||
writeSpeed: number;
|
||||
totalBytes: number;
|
||||
freeBytes: number;
|
||||
usagePercent: number;
|
||||
}
|
||||
|
||||
export interface HardwareNetStats {
|
||||
name: string;
|
||||
bytesSentPerSecond: number;
|
||||
deltaBytesRecv: number;
|
||||
deltaPacketsSent: number;
|
||||
deltaPacketsRecv: number;
|
||||
deltaErrIn: number;
|
||||
deltaErrOut: number;
|
||||
deltaDropIn: number;
|
||||
deltaDropOut: number;
|
||||
deltaFifoIn: number;
|
||||
deltaFifoOut: number;
|
||||
}
|
||||
|
||||
export interface HardwareCheckStats {
|
||||
bucketDate: string;
|
||||
avgCpuUsage: number;
|
||||
avgMemoryUsage: number;
|
||||
avgTemperature: number[];
|
||||
disks: HardwareDiskStats[];
|
||||
net: HardwareNetStats[];
|
||||
}
|
||||
|
||||
export interface HardwareStats {
|
||||
aggregateData: {
|
||||
totalChecks: number;
|
||||
};
|
||||
upChecks: {
|
||||
totalChecks: number;
|
||||
};
|
||||
checks: HardwareCheckStats[];
|
||||
}
|
||||
|
||||
export interface HardwareDetailsResult {
|
||||
monitor: Monitor;
|
||||
stats: HardwareStats;
|
||||
monitorStats: import("./monitorStats.js").MonitorStats | null;
|
||||
}
|
||||
|
||||
export interface PageSpeedDetailsResult {
|
||||
|
||||
Reference in New Issue
Block a user