diff --git a/README.md b/README.md index 129953fb9..30b406684 100644 --- a/README.md +++ b/README.md @@ -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! diff --git a/client/src/Components/v2/design-elements/Gauge.tsx b/client/src/Components/v2/design-elements/Gauge.tsx new file mode 100644 index 000000000..34c57487d --- /dev/null +++ b/client/src/Components/v2/design-elements/Gauge.tsx @@ -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 ( + + + + + + + + {`${progressWithinRange.toFixed(precision)}${unit}`} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/Tabs.tsx b/client/src/Components/v2/design-elements/Tabs.tsx new file mode 100644 index 000000000..149c0bd93 --- /dev/null +++ b/client/src/Components/v2/design-elements/Tabs.tsx @@ -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 ( + + {props.children} + + ); +}; + +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 ( + + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index 6ebffc624..33248a345 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -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"; diff --git a/client/src/Components/v2/monitors/ControlsFilter.tsx b/client/src/Components/v2/monitors/ControlsFilter.tsx index d7eecefb5..34eedd190 100644 --- a/client/src/Components/v2/monitors/ControlsFilter.tsx +++ b/client/src/Components/v2/monitors/ControlsFilter.tsx @@ -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>; + showTypes?: boolean; + selectedTypes?: MonitorType[]; + setSelectedTypes?: React.Dispatch>; selectedStatus: string; setSelectedStatus: React.Dispatch>; 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 ( - + {showTypes && setSelectedTypes && ( + + )} setSelectedMonitor(e.target.value)} - items={monitorNames} - sx={{ - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastTextSecondary, - }} - maxWidth={250} - /> - - - - {t("incidentsOptionsHeaderFilterBy")} - - setSelectedInterface(e.target.value)} - items={availableInterfaces.map((interfaceName) => ({ - _id: interfaceName, - name: interfaceName, - }))} - sx={{ minWidth: 200 }} - /> - )} - - - - - ); -}; - -Network.propTypes = { - net: PropTypes.array, - checks: PropTypes.array, - isLoading: PropTypes.bool.isRequired, - dateRange: PropTypes.string.isRequired, - setDateRange: PropTypes.func.isRequired, -}; - -export default Network; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx deleted file mode 100644 index 52fcfad87..000000000 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import { - Card, - CardContent, - Skeleton, - Table, - TableHead, - TableRow, - TableCell, - TableBody, -} from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - - - - Name - Bytes Sent - Bytes Received - Packets Sent - Packets Received - Errors In - Errors Out - Drops In - Drops Out - - - - {Array.from({ length: 5 }).map((_, idx) => ( - - {Array.from({ length: 9 }).map((__, colIdx) => ( - - - - ))} - - ))} - -
-
-
- ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes.tsx b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes.tsx new file mode 100644 index 000000000..7b1d80c40 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes.tsx @@ -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 ( + + + + + + + + + + ); +}; diff --git a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx deleted file mode 100644 index d411886c7..000000000 --- a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx +++ /dev/null @@ -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 ( - - - - {physicalCores} - - {physicalCores === 1 ? "core" : "cores"} - - - } - /> - - {logicalCores} - - {logicalCores === 1 ? "core" : "cores"} - - - } - /> - - {(cpuFrequency / 1000).toFixed(2)} - Ghz - - } - /> - - {cpuTemperature.toFixed(2)} - °C - - } - /> - - - {/* - {(uptimePercentage * 100).toFixed(2)} - % - - } - /> */} - - - ); -}; - -export default InfraStatBoxes; diff --git a/client/src/Pages/Infrastructure/Details/Components/TabNetwork.tsx b/client/src/Pages/Infrastructure/Details/Components/TabNetwork.tsx new file mode 100644 index 000000000..612040cb1 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/TabNetwork.tsx @@ -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 ( + + + + ); +}; diff --git a/client/src/Pages/Infrastructure/Details/Components/TabOverview.tsx b/client/src/Pages/Infrastructure/Details/Components/TabOverview.tsx new file mode 100644 index 000000000..447a2cce6 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/TabOverview.tsx @@ -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 ( + + + + + + ); +}; diff --git a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx deleted file mode 100644 index 8938847a4..000000000 --- a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx +++ /dev/null @@ -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 ? " " : ""} - {t("gb")} - - ); - if (typeof bytes !== "number") - return ( - <> - {0} - {space ? " " : ""} - {t("gb")} - - ); - if (bytes === 0) - return ( - <> - {0} - {space ? " " : ""} - {t("gb")} - - ); - - const GB = bytes / (1024 * 1024 * 1024); - const MB = bytes / (1024 * 1024); - - if (GB >= 1) { - return ( - <> - {Number(GB.toFixed(2))} - {space ? " " : ""} - {t("gb")} - - ); - } else { - return ( - <> - {Number(MB.toFixed(2))} - {space ? " " : ""} - {t("mb")} - - ); - } - }; - - 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 ( - - - {deviceStr} - - - ); - }; - - const formatMountpoint = (mountpoint) => { - const mountpointStr = String(mountpoint || ""); - - if (!mountpointStr) { - return ( - - - N/A - - - ); - } - - // Show full mountpoint path - return ( - - - {mountpointStr} - - - ); - }; - - /** - * 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 }; diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx deleted file mode 100644 index 6ae630822..000000000 --- a/client/src/Pages/Infrastructure/Details/index.jsx +++ /dev/null @@ -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 ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - if (!isLoading && monitor?.stats?.checks?.length === 0) { - return ( - - - - - {t("distributedUptimeDetailsNoMonitorHistory")} - - - ); - } - - return ( - - - - - setTab(v)} - > - - - - {tab === "details" && ( - <> - - - - - - )} - {tab === "network" && ( - - )} - - - ); -}; - -export default InfrastructureDetails; diff --git a/client/src/Pages/Infrastructure/Details/index.tsx b/client/src/Pages/Infrastructure/Details/index.tsx new file mode 100644 index 000000000..42be78a63 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/index.tsx @@ -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("recent"); + const [selectedTab, setSelectedTab] = useState(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( + monitorDetailsUrl, + {}, + { refreshInterval: 10000, keepPreviousData: true } + ); + + const monitor = monitorDetailsData?.monitor; + const monitorStats = monitorDetailsData?.monitorStats ?? null; + const stats = monitorDetailsData?.stats; + + return ( + + + + + { + setSelectedTab(value); + }} + > + + + + {selectedTab === 0 && ( + + )} + {selectedTab === 1 && ( + + )} + + ); +}; + +export default InfrastructureDetails; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx deleted file mode 100644 index 01e56c68a..000000000 --- a/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx +++ /dev/null @@ -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 ( - - - - - ); -}; - -Filter.propTypes = { - selectedStatus: PropTypes.arrayOf(PropTypes.string), - setSelectedStatus: PropTypes.func.isRequired, - setToFilterStatus: PropTypes.func.isRequired, - handleReset: PropTypes.func.isRequired, -}; - -export default Filter; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx new file mode 100644 index 000000000..2644f969e --- /dev/null +++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx @@ -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(); + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + const value = Number(e.target.value); + setPage(0); + setRowsPerPage(value); + }; + + const handleSort = (e: any, field: string) => { + e.preventDefault(); + e.stopPropagation(); + if (sortField === field) { + const newOrder = sortOrder === "asc" ? "desc" : "asc"; + setSortOrder(newOrder); + } else { + setSortField(field); + setSortOrder("asc"); + } + refetch(); + }; + + const getActions = (monitor: Monitor): ActionMenuItem[] => { + return [ + { + id: 1, + label: t("pages.common.monitors.actions.openSite"), + action: () => { + window.open(monitor.url, "_blank", "noreferrer"); + }, + closeMenu: true, + }, + { + id: 2, + label: t("pages.common.monitors.actions.details"), + action: () => { + navigate(`${monitor.id}`); + }, + }, + { + id: 3, + label: t("pages.common.monitors.actions.incidents"), + action: () => { + navigate(`/incidents?monitorId=${monitor.id}`); + }, + }, + { + id: 4, + label: t("pages.common.monitors.actions.configure"), + action: () => { + navigate(`/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: ( + + {t("common.buttons.delete")} + + ), + action: () => { + setSelectedMonitor(monitor); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const renderSortIcon = (isActive: boolean) => ( + + {isActive ? ( + sortOrder === "asc" ? ( + + ) : ( + + ) + ) : null} + + ); + const headers: Header[] = [ + { + id: "name", + content: ( + handleSort(e, "name")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.name")} + {renderSortIcon(sortField === "name")} + + ), + render: (row) => { + return row.name; + }, + }, + { + id: "status", + content: ( + handleSort(e, "status")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.status")} + {renderSortIcon(sortField === "status")} + + ), + render: (row) => { + return ( + + ); + }, + }, + { + id: "cpu", + content: t("pages.infrastructure.table.headers.cpu"), + render: (row) => { + const check = row.recentChecks?.[0]; + const cpuUsage = (check?.cpu?.usage_percent || 0) * 100; + return ; + }, + }, + { + 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 ; + }, + }, + { + 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 ; + }, + }, + + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + let headers = getHeaders(); + + if (isSmall) { + headers = headers.filter((h) => h.id !== "histogram"); + } + return ( + + { + navigate(`/infrastructure/${row.id}`); + }} + /> + + + ); +}; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx deleted file mode 100644 index d12ef1d24..000000000 --- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx +++ /dev/null @@ -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) => ( - - ), - }, - { - id: "status", - content: t("incidentsTableStatus"), - render: (row) => ( - - ), - }, - { - id: "frequency", - content: t("frequency"), - render: (row) => ( - - - {row.processor} - - ), - }, - { id: "cpu", content: t("cpu"), render: (row) => }, - { - id: "memory", - content: t("memory"), - render: (row) => , - }, - { - id: "disk", - content: t("disk"), - render: (row) => , - }, - { - id: "actions", - content: t("actions"), - render: (row) => ( - - ), - }, - ]; - - 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 ( - - - openDetails(row.id), - emptyView: "No monitors found", - }} - /> - - ); -}; - -MonitorsTable.propTypes = { - isLoading: PropTypes.bool, - monitors: PropTypes.array, - isAdmin: PropTypes.bool, - handleActionMenuDelete: PropTypes.func, - isSearching: PropTypes.bool, -}; - -export default MonitorsTable; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx deleted file mode 100644 index 50af0aaa2..000000000 --- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx +++ /dev/null @@ -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 ( - <> - - - - - - { - e.stopPropagation(); - openDetails(monitor.id); - closeMenu(e); - }} - > - {t("monitorActions.details")} - - {isAdmin && ( - { - e.stopPropagation(); - openConfigure(monitor.id); - closeMenu(e); - }} - > - {t("configure")} - - )} - {isAdmin && ( - { - e.stopPropagation(); - await handlePause(); - closeMenu(e); - }} - > - {!monitor.isActive ? t("resume") : t("pause")} - - )} - {isAdmin && ( - - {t("remove")} - - )} - - - - ); -}; - -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 }; diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx deleted file mode 100644 index e4add415c..000000000 --- a/client/src/Pages/Infrastructure/Monitors/index.jsx +++ /dev/null @@ -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 ( - <> - - - - - - - - - - - - - - - - ); -}; - -export default InfrastructureMonitors; diff --git a/client/src/Pages/Infrastructure/Monitors/index.tsx b/client/src/Pages/Infrastructure/Monitors/index.tsx new file mode 100644 index 000000000..af7b3e899 --- /dev/null +++ b/client/src/Pages/Infrastructure/Monitors/index.tsx @@ -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(""); + const [selectedState, setSelectedState] = useState(""); + const [search, setSearch] = useState(""); + const [page, setPage] = useState(0); + const [sortField, setSortField] = useState(""); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); + const [selectedMonitor, setSelectedMonitor] = useState(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([ + [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("/monitors/team?type=hardware", {}, { keepPreviousData: true }); + + const { + data: monitorsWithChecksData, + isLoading: monitorsWithChecksLoading, + error: monitorsWithChecksError, + refetch, + } = useGet( + 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 ( + + + + + + + + + + { + setSearch(event.target.value); + }} + /> + + { + dispatch( + setRowsPerPage({ + value, + table: "infrastructure", + }) + ); + setPage(0); + }} + /> + + + ); +}; + +export default InfrastructureMonitors; diff --git a/client/src/Pages/Notifications/components/ActionMenu.jsx b/client/src/Pages/Notifications/components/ActionMenu.jsx deleted file mode 100644 index a3ccc4bbb..000000000 --- a/client/src/Pages/Notifications/components/ActionMenu.jsx +++ /dev/null @@ -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 ( - <> - e.stopPropagation()} - > - - - - e.stopPropagation()} - onMouseDown={(e) => e.stopPropagation()} - > - {t("configure")} - - {t("delete")} - - - - ); -}; - -ActionMenu.propTypes = { - notification: PropTypes.object, - onDelete: PropTypes.func, -}; - -export default ActionMenu; diff --git a/client/src/Pages/Notifications/components/NotificationsTable.tsx b/client/src/Pages/Notifications/components/NotificationsTable.tsx new file mode 100644 index 000000000..306d8efa8 --- /dev/null +++ b/client/src/Pages/Notifications/components/NotificationsTable.tsx @@ -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: ( + + {t("pages.common.monitors.actions.delete")} + + ), + action: async () => { + setSelectedChannel(channel); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: t("common.table.headers.name"), + render: (row) => { + return {row?.notificationName}; + }, + }, + + { + id: "type", + content: t("common.table.headers.type"), + render: (row) => { + return {row?.type}; + }, + }, + { + id: "destination", + content: t("pages.notifications.table.headers.destination"), + render: (row) => { + return {row?.address}; + }, + }, + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(); + + return ( +
{ + navigate(`/notifications/${row.id}`); + }} + /> + ); +}; diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/Notifications/index.jsx deleted file mode 100644 index 70ed3ef96..000000000 --- a/client/src/Pages/Notifications/index.jsx +++ /dev/null @@ -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 ( - - ); - }, - }, - ]; - - return ( - <> - - - - - - - {t("notifications.createTitle")} - navigate(`/notifications/${row.id}`), - rowSX: { - cursor: "pointer", - "&:hover td": { - backgroundColor: theme.palette.tertiary.main, - transition: "background-color .3s ease", - }, - }, - }} - headers={headers} - data={notifications} - /> - - - - ); -}; - -export default Notifications; diff --git a/client/src/Pages/Notifications/index.tsx b/client/src/Pages/Notifications/index.tsx new file mode 100644 index 000000000..6e7dcc456 --- /dev/null +++ b/client/src/Pages/Notifications/index.tsx @@ -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(null); + const isDialogOpen = Boolean(selectedChannel); + + const { + data: notifications, + isLoading, + isValidating, + error, + refetch, + } = useGet("/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 ( + + + + + ); +}; + +export default NotificationsPage; diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx deleted file mode 100644 index d8c118237..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx +++ /dev/null @@ -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 ( - - - {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)} - - {Object.keys(config) - .reverse() - .map((key) => { - const { color } = config[key]; - const dotColor = theme.palette[color].main; - - return ( - - - - {config[key].text} - {" "} - {payload[0].payload[key]} - - ); - })} - - ); - } - 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 ( - - {formatDateWithTz(payload?.value, "h:mm a", uiTimezone)} - - ); -}; -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 ( - - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - - } - axisLine={false} - tickLine={false} - height={18} - minTickGap={0} - interval="equidistantPreserveStart" - /> - } - /> - - {Object.values(filteredConfig).map(({ id, color }) => { - /* TODO not working? */ - const startColor = theme.palette[color].main; - const endColor = theme.palette[color].lowContrast; - - return ( - - - - - ); - })} - - {Object.keys(filteredConfig).map((key) => { - const { color } = filteredConfig[key]; - const strokeColor = theme.palette[color].main; - const bgColor = theme.palette.primary.main; - - return ( - - ); - })} - - - ); -}; - -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; diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx deleted file mode 100644 index 525e4d0d2..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx +++ /dev/null @@ -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 ( - - } - header="Metrics" - > - - - {t("shown")} - - - - handleMetrics("accessibility")} - /> - - handleMetrics("bestPractices")} - /> - - handleMetrics("performance")} - /> - - handleMetrics("seo")} - /> - - - ); -}; - -export default AreaChartLegend; diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx deleted file mode 100644 index eec075684..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx +++ /dev/null @@ -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 ( - setExpand(true)} - > - - - {value} - - - ); -}; - -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 ( - - - +{value} - - - ); -}; - -// 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} 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 ( - setExpand(false)} - sx={{ - display: "flex", - justifyContent: "center", - alignItems: "center", - width: "100%", - }} - > - {expand ? ( - - - {pieData?.map((pie) => ( - - ))} - - ) : ( - - - - )} - - ); -}; - -PieChart.propTypes = { - audits: PropTypes.object, -}; - -export default PieChart; diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx deleted file mode 100644 index 5cfdad608..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx +++ /dev/null @@ -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 ( - - } - 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 ( - - - - {audit.title} - - - {value} - - {unit} - - - - - - ); - })} - - ); -}; - -PieChartLegend.propTypes = { - audits: PropTypes.object, -}; - -export default PieChartLegend; diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx deleted file mode 100644 index 9020ddd6d..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx +++ /dev/null @@ -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 ; - } - - const data = monitor?.checks ? [...monitor.checks].reverse() : []; - - return ( - - - } - header="Score history" - height="100%" - borderRadiusRight={16} - Legend={ - - } - > - - - - ); -}; - -PageSpeedAreaChart.propTypes = { - shouldRender: PropTypes.bool, - monitor: PropTypes.object, - metrics: PropTypes.object, - handleMetrics: PropTypes.func, -}; - -export default PageSpeedAreaChart; diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx deleted file mode 100644 index b793908d4..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Box, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx deleted file mode 100644 index 6ff26c739..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx +++ /dev/null @@ -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 ( - - - {streakTime} - {t("ago")} - - } - /> - - {time} - {t("ago")} - - } - /> - - ); -}; - -export default PageSpeedStatusBoxes; diff --git a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx deleted file mode 100644 index a2e1b5f96..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx +++ /dev/null @@ -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 ; - } - - return ( - - } - header="Performance report" - Legend={} - borderRadiusRight={16} - > - - - {t("pageSpeedDetailsPerformanceReport")}{" "} - - {t("pageSpeedDetailsPerformanceReportCalculator")} - - - - ); -}; - -export default PerformanceReport; diff --git a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx b/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx deleted file mode 100644 index b793908d4..000000000 --- a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Box, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/PageSpeed/Details/old.jsx b/client/src/Pages/PageSpeed/Details/old.jsx deleted file mode 100644 index 3190787b3..000000000 --- a/client/src/Pages/PageSpeed/Details/old.jsx +++ /dev/null @@ -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 ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - // Empty view, displayed when loading is complete and there are no checks - if (!isLoading && monitor?.checks?.length === 0) { - return ( - - - - - {t("distributedUptimeDetailsNoMonitorHistory")} - - - ); - } - - return ( - - - - - - - - - - ); -}; - -export default PageSpeedDetails; diff --git a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx deleted file mode 100644 index 4a85863c4..000000000 --- a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx +++ /dev/null @@ -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 ( - - - {formatDateWithTz( - payload[0]?.payload.createdAt, - "ddd, MMMM D, YYYY, h:mm A", - uiTimezone - )} - - - - - - {payload[0]?.name} - {" "} - {payload[0]?.payload.score} - - - ); -}; - -CustomToolTip.propTypes = { - payload: PropTypes.array, -}; - -/* TODO separate utils in folder*/ -/** - * Processes the raw data to include a score for each entry. - * @param {Array} data - The raw data array. - * @returns {Array} - 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} 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 ( - - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - - } - /> - - - - - - - - - - ); -}; - -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 ( - - 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", - }, - }} - > - - - - - {monitor.name} - - - - {monitor.url} - - - - - - - {t("checkingEvery")}{" "} - {(() => { - const { time, format } = formatDurationSplit(monitor?.interval); - return ( - <> - - {time}{" "} - - {format} - - ); - })()} - - - - - ); -}; - -Card.propTypes = { - monitor: PropTypes.object.isRequired, -}; - -export default Card; diff --git a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx deleted file mode 100644 index 9df5f7577..000000000 --- a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx +++ /dev/null @@ -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 ( - - {monitors?.map((monitor) => ( - - ))} - - ); -}; - -export default MonitorGrid; diff --git a/client/src/Pages/PageSpeed/Monitors/old.jsx b/client/src/Pages/PageSpeed/Monitors/old.jsx deleted file mode 100644 index f1bfcd0dd..000000000 --- a/client/src/Pages/PageSpeed/Monitors/old.jsx +++ /dev/null @@ -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 ( - <> - - ) - } - > - - - - - - - - - ); -}; - -export default PageSpeed; diff --git a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx b/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx deleted file mode 100644 index 8912438d8..000000000 --- a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx +++ /dev/null @@ -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; diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index 1a6177ddc..f32ae1989 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -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"; diff --git a/client/src/Pages/Uptime/Details/old.jsx b/client/src/Pages/Uptime/Details/old.jsx deleted file mode 100644 index 144ceb4dc..000000000 --- a/client/src/Pages/Uptime/Details/old.jsx +++ /dev/null @@ -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 ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - // Empty view, displayed when loading is complete and there are no checks - if (!monitorIsLoading && !checksAreLoading && checksCount === 0) { - return ( - - - - - - - - - {t("distributedUptimeDetailsNoMonitorHistory")} - - - ); - } - - return ( - - - - - - - - - - ); -}; - -export default UptimeDetails; diff --git a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx deleted file mode 100644 index 62ba0be13..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx +++ /dev/null @@ -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 ( - - - - - - - ); -}; - -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; diff --git a/client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx b/client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx deleted file mode 100644 index a649473af..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx +++ /dev/null @@ -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 ( - <> - - - - - - ); -}; - -LoadingSpinner.propTypes = { - shouldRender: PropTypes.bool, -}; - -export default LoadingSpinner; diff --git a/client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx b/client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx deleted file mode 100644 index 64c546a8a..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx +++ /dev/null @@ -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 ( - - - - ); -}; - -SearchComponent.propTypes = { - monitors: PropTypes.array, - onSearchChange: PropTypes.func, - setIsSearching: PropTypes.func, -}; - -export default SearchComponent; diff --git a/client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx deleted file mode 100644 index 4e55be536..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx +++ /dev/null @@ -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 ( - <> - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx deleted file mode 100644 index 5cba9e3b1..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx +++ /dev/null @@ -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 ; - return ( - - - - - - ); -}; - -StatusBoxes.propTypes = { - monitorsSummary: PropTypes.object, - shouldRender: PropTypes.bool, -}; - -export default StatusBoxes; diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx deleted file mode 100644 index df6e08ccd..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -const SkeletonLayout = () => { - const theme = useTheme(); - return ( - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx deleted file mode 100644 index 136c3575d..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx +++ /dev/null @@ -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 = ( - - - - ); - } else if (status === "down") { - color = theme.palette.error.lowContrast; - icon = ( - - - - ); - } else if (status === "paused") { - color = theme.palette.warning.lowContrast; - icon = ( - - - - ); - } - - return ( - - - - - - - - {title} - - {icon} - - - {value} - - - # - - - - - ); -}; - -StatusBox.propTypes = { - title: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - status: PropTypes.string, -}; - -export default StatusBox; diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx deleted file mode 100644 index 8c3239dd3..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx +++ /dev/null @@ -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: ( - handleSort("name")} - > - {t("host")} - - {sort?.order === "asc" ? ( - - ) : ( - - )} - - - ), - render: (row) => ( - - ), - }, - { - id: "status", - content: ( - handleSort("status")} - > - {" "} - {t("status")} - - {sort?.order === "asc" ? ( - - ) : ( - - )} - - - ), - render: (row) => { - const status = determineState(row.monitor); - return ( - - ); - }, - }, - { - id: "responseTime", - content: t("responseTime"), - render: (row) => ( - - - - ), - }, - { - id: "type", - content: t("type"), - render: (row) => ( - - {row.monitor.type === "http" ? "HTTP(s)" : row.monitor.type} - - ), - }, - { - id: "actions", - content: t("actions"), - render: (row) => ( - - ), - }, - ]; - - if (monitorsAreLoading) { - return ; - } - - return ( - - - { - navigate(`/uptime/${row.id}`); - }, - emptyView: "No monitors found", - }} - /> - - ); -}; - -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; diff --git a/client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx b/client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx deleted file mode 100644 index 11d639f14..000000000 --- a/client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx +++ /dev/null @@ -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; diff --git a/client/src/Pages/Uptime/Monitors/old.jsx b/client/src/Pages/Uptime/Monitors/old.jsx deleted file mode 100644 index 67019362f..000000000 --- a/client/src/Pages/Uptime/Monitors/old.jsx +++ /dev/null @@ -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 ( - - - - ); -}; - -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 ( - <> - - - - - - - - - - - - - - - - - - ); -}; - -export default UptimeMonitors; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 5ee16a49e..e0725e8e2 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -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 = () => { /> } + element={ + <> + + + + + } /> { /> } + element={ + <> + + + + + } /> { } + element={ + <> + + + + + } /> { + 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[]): 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})`; +}; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 2b1005c0a..960397021 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -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", diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 721565f59..df1945fcc 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -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); diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index 4d9c499de..7bd1cec6f 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -71,6 +71,7 @@ const cpuSchema = new Schema( 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( { 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( os: { type: String, default: "" }, platform: { type: String, default: "" }, kernel_version: { type: String, default: "" }, + pretty_name: { type: String, default: "" }, }, { _id: false } ); diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 7916e2c06..a36788e2c 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -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): 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"] } } } }, diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index b950c9a32..b824908b5 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -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, }; }; diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 8e223d7c8..c02b03099 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -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[]; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index b2bf360dc..62ced091d 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -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 {