diff --git a/client/src/App.jsx b/client/src/App.jsx index af2f6c11c..da83f9061 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -37,7 +37,14 @@ function App() { - + ); } diff --git a/client/src/Components/v1/Charts/BarChart/index.jsx b/client/src/Components/v1/Charts/BarChart/index.jsx index 9f0ed8aa6..77b836cae 100644 --- a/client/src/Components/v1/Charts/BarChart/index.jsx +++ b/client/src/Components/v1/Charts/BarChart/index.jsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { useEffect, useState } from "react"; import "./index.css"; import { useSelector } from "react-redux"; diff --git a/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx index b37990e46..8b6c2b165 100644 --- a/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx +++ b/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx @@ -5,7 +5,7 @@ import { Stack, Box, Tooltip, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useState, useEffect } from "react"; import PropTypes from "prop-types"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { useSelector } from "react-redux"; const PlaceholderCheck = ({ daysToShow }) => { diff --git a/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx index a9ef04a53..dafca7b40 100644 --- a/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx +++ b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx @@ -12,7 +12,7 @@ import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useMemo, useState } from "react"; import { useSelector } from "react-redux"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { tooltipDateFormatLookup, tickDateFormatLookup, diff --git a/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx index 309201b84..2dc5e61f0 100644 --- a/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx +++ b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { useEffect, useState, forwardRef } from "react"; import { useSelector } from "react-redux"; import PropTypes from "prop-types"; diff --git a/client/src/Components/v1/Charts/Utils/chartUtils.jsx b/client/src/Components/v1/Charts/Utils/chartUtils.jsx index 1bab80595..7797ba462 100644 --- a/client/src/Components/v1/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/v1/Charts/Utils/chartUtils.jsx @@ -2,7 +2,7 @@ import PropTypes from "prop-types"; import { useSelector } from "react-redux"; import { useTheme } from "@mui/material"; import { Text } from "recharts"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { Box, Stack, Typography } from "@mui/material"; import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions.js"; /** diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx index febf449d4..c6a3cfcfa 100644 --- a/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx +++ b/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx @@ -4,7 +4,7 @@ import PulseDot from "../Animated/PulseDot.jsx"; import Typography from "@mui/material/Typography"; import Dot from "../Dot/index.jsx"; // Utils -import { formatDurationRounded } from "../../../Utils/timeUtils.js"; +import { formatDurationRounded } from "../../../Utils/timeUtilsLegacy.js"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; diff --git a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx new file mode 100644 index 000000000..17576a8b2 --- /dev/null +++ b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx @@ -0,0 +1,102 @@ +import Stack from "@mui/material/Stack"; +import { Icon, MonitorStatus } from "@/Components/v2/design-elements"; +import { Button } from "@/Components/v2/inputs"; +import { Settings, Pause, Play, Mail, Bug } from "lucide-react"; + +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { useTheme } from "@mui/material/styles"; +import { usePost } from "@/Hooks/UseApi"; + +import type { Monitor } from "@/Types/Monitor.js"; + +interface HeaderMonitorControlsProps { + path: string; + monitor?: Monitor; + isAdmin: boolean; + refetch: Function; +} + +export const HeaderMonitorControls = ({ + path, + monitor, + isAdmin, + refetch, +}: HeaderMonitorControlsProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const theme = useTheme(); + const { + post, + loading: isPosting, + // error: postError, + } = usePost(); + + if (!monitor) { + return null; + } + return ( + + + + + + {isAdmin && ( + + )} + {isAdmin && ( + + )} + + + ); +}; diff --git a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx new file mode 100644 index 000000000..57b61473a --- /dev/null +++ b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx @@ -0,0 +1,89 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { ToggleButtonGroup, ToggleButton } from "@/Components/v2/inputs"; +import { useTheme } from "@mui/material/styles"; + +interface MonitorTimeFrameHeaderProps { + isLoading?: boolean; + hasDateRange?: boolean; + dateRange: string; + setDateRange: (dateRange: string) => void; +} + +export const HeaderTimeRange = ({ + isLoading = false, + hasDateRange = true, + dateRange, + setDateRange, +}: MonitorTimeFrameHeaderProps) => { + const theme = useTheme(); + + const handleChange = ( + _event: React.MouseEvent, + newValue: string | null + ) => { + if (newValue !== null) { + setDateRange(newValue); + } + }; + + let timeFramePicker = null; + + if (hasDateRange) { + timeFramePicker = ( + + + Recent + + + Day + + + Week + + + Month + + + ); + } + + return ( + + + Showing statistics for past{" "} + {dateRange === "recent" + ? "2 hours" + : dateRange === "day" + ? "24 hours" + : dateRange === "week" + ? "7 days" + : "30 days"} + . + + {timeFramePicker} + + ); +}; diff --git a/client/src/Components/v2/common/index.ts b/client/src/Components/v2/common/index.ts index e354983eb..d0d1262b7 100644 --- a/client/src/Components/v2/common/index.ts +++ b/client/src/Components/v2/common/index.ts @@ -2,3 +2,5 @@ export * from "../common/charts/HistogramResponseTime"; export * from "../common/charts/HeatmapResponseTime"; export * from "../common/charts/HeatmapResponseTimeTooltip"; export * from "../common/controls/HeaderCreate"; +export * from "../common/controls/HeaderMonitorControls"; +export * from "../common/controls/HeaderTimeRange"; diff --git a/client/src/Components/v2/design-elements/BaseChart.tsx b/client/src/Components/v2/design-elements/BaseChart.tsx new file mode 100644 index 000000000..fd9251915 --- /dev/null +++ b/client/src/Components/v2/design-elements/BaseChart.tsx @@ -0,0 +1,72 @@ +import { BaseBox } from "../design-elements"; +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import type { ResponsiveStyleValue } from "@mui/system"; + +import { useTheme } from "@mui/material/styles"; + +type BaseChartProps = React.PropsWithChildren<{ + icon: React.ReactNode; + title: string; + width?: number | string; + maxWidth?: number | string; + padding?: number | string | ResponsiveStyleValue; +}>; + +export const BaseChart = ({ + children, + icon, + title, + width = "100%", + maxWidth = "100%", + padding, +}: BaseChartProps) => { + const theme = useTheme(); + return ( + + + + {icon && ( + + {icon} + + )} + {title} + + {children} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx index c853a8d30..c8618fa67 100644 --- a/client/src/Components/v2/design-elements/BasePage.tsx +++ b/client/src/Components/v2/design-elements/BasePage.tsx @@ -55,7 +55,7 @@ export const BasePage = ({ spacing={theme.spacing(10)} {...props} > - + {children} ); diff --git a/client/src/Components/v2/design-elements/Breadcrumb.tsx b/client/src/Components/v2/design-elements/Breadcrumb.tsx index 4489f992d..492ed0ec9 100644 --- a/client/src/Components/v2/design-elements/Breadcrumb.tsx +++ b/client/src/Components/v2/design-elements/Breadcrumb.tsx @@ -2,25 +2,21 @@ import MuiBreadcrumbs from "@mui/material/Breadcrumbs"; import Typography from "@mui/material/Typography"; import { Link, useLocation } from "react-router-dom"; import { useTheme } from "@mui/material/styles"; -import { ChevronRight, Home } from "lucide-react"; -import { useTranslation } from "react-i18next"; +import { ChevronRight } from "lucide-react"; -export const Breadcrumb = ({ breadcrumbOverride }: { breadcrumbOverride?: string[] }) => { - const { t } = useTranslation(); +const isId = (segment: string): boolean => { + return segment.length === 24 || /^[a-f0-9-]{36}$/.test(segment); +}; + +export const Breadcrumb = () => { const theme = useTheme(); const location = useLocation(); - // Don't show breadcrumb on dashboard - if (location.pathname === "/" || location.pathname === "/uptime") { - return null; - } + const segments = location.pathname.split("/").filter((x) => x); - const pathnames = breadcrumbOverride || location.pathname.split("/").filter((x) => x); - - // Don't show if only one level (e.g., /uptime) - if (pathnames.length === 0) { - return null; - } + // Build simplified breadcrumb: "uptime" or "uptime / details" + const basePage = segments[0] || "home"; + const hasDetailsPage = segments.length > 1 && isId(segments[1]); return ( - - + {hasDetailsPage ? ( + + + {basePage.charAt(0).toUpperCase() + basePage.slice(1)} + + + ) : ( - Home + {basePage.charAt(0).toUpperCase() + basePage.slice(1)} - - {pathnames.map((value, index) => { - const last = index === pathnames.length - 1; - const to = `/${pathnames.slice(0, index + 1).join("/")}`; - - let displayName = ""; - - // Paths that are IDs should be detail pages - if (value.length === 24 || value.match(/^[a-f0-9-]{36}$/)) { - displayName = "Details"; - } else { - displayName = - t(`common.breadcrumbs.${value}`) || - value.charAt(0).toUpperCase() + value.slice(1); - } - - return last ? ( - - {displayName} - - ) : ( - - - {displayName} - - - ); - })} + )} + {hasDetailsPage && ( + + Details + + )} ); }; diff --git a/client/src/Components/v2/design-elements/Dot.tsx b/client/src/Components/v2/design-elements/Dot.tsx new file mode 100644 index 000000000..541e90812 --- /dev/null +++ b/client/src/Components/v2/design-elements/Dot.tsx @@ -0,0 +1,23 @@ +import type { CSSProperties } from "react"; + +interface DotProps { + color?: string; + size?: string; + style?: CSSProperties; +} + +export const Dot = ({ color = "gray", size = "4px", style }: DotProps) => { + return ( + + ); +}; diff --git a/client/src/Components/v2/design-elements/Icon.tsx b/client/src/Components/v2/design-elements/Icon.tsx new file mode 100644 index 000000000..b32cd84b2 --- /dev/null +++ b/client/src/Components/v2/design-elements/Icon.tsx @@ -0,0 +1,18 @@ +import type { LucideIcon } from "lucide-react"; + +interface IconProps { + icon: LucideIcon; + size?: number; + strokeWidth?: number; +} + +const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => { + return ( + + ); +}; + +export default Icon; diff --git a/client/src/Components/v2/design-elements/MonitorStatus.tsx b/client/src/Components/v2/design-elements/MonitorStatus.tsx new file mode 100644 index 000000000..0ecb77df8 --- /dev/null +++ b/client/src/Components/v2/design-elements/MonitorStatus.tsx @@ -0,0 +1,59 @@ +import type { Monitor } from "@/Types/Monitor"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { PulseDot, Dot } from "@/Components/v2/design-elements"; +import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils"; +import { useTheme } from "@mui/material/styles"; +import prettyMilliseconds from "pretty-ms"; +import { typographyLevels } from "@/Utils/Theme/v2Palette"; +import useMediaQuery from "@mui/material/useMediaQuery"; +export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + if (!monitor) { + return null; + } + return ( + + + {monitor.name} + + + + + {formatUrl(monitor?.url)} + + {!isSmall && ( + <> + + + Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })} + + + )} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/PulseDot.tsx b/client/src/Components/v2/design-elements/PulseDot.tsx new file mode 100644 index 000000000..ab792b628 --- /dev/null +++ b/client/src/Components/v2/design-elements/PulseDot.tsx @@ -0,0 +1,48 @@ +import { Box, Stack, useTheme } from "@mui/material"; + +interface PulseDotProps { + color: string; +} + +export const PulseDot = ({ color }: PulseDotProps) => { + const theme = useTheme(); + + return ( + + + + ); +}; diff --git a/client/src/Components/v2/design-elements/StatBox.tsx b/client/src/Components/v2/design-elements/StatBox.tsx new file mode 100644 index 000000000..b78a003dc --- /dev/null +++ b/client/src/Components/v2/design-elements/StatBox.tsx @@ -0,0 +1,86 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +import { lighten } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import type { PaletteKey } from "@/Utils/Theme/v2Theme"; +import { BaseBox, TooltipWithInfo } from "@/Components/v2/design-elements"; +import type { SxProps } from "@mui/material"; + +type GradientBox = React.PropsWithChildren<{ + palette?: PaletteKey; + sx?: SxProps; +}>; + +export const GradientBox = ({ children, palette, sx }: GradientBox) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const isLight = theme.palette.mode === "light"; + const paper = theme.palette.background.paper; + const paperStart = lighten(paper, isLight ? 0.06 : 0.08); + const paperEnd = paper; + const bg = palette + ? `linear-gradient(135deg, ${theme.palette[palette].light} 0%, ${theme.palette[palette].main} 100%)` + : `linear-gradient(135deg, ${paperStart} 0%, ${paperEnd} 100%)`; + + return ( + + {children} + + ); +}; + +type StatBoxProps = React.PropsWithChildren<{ + title: string; + subtitle: string; + palette?: PaletteKey; + sx?: SxProps; + tooltip?: string; + onClick?: () => void; +}>; + +export const StatBox = ({ + title, + subtitle, + palette, + children, + sx, + tooltip, + onClick, +}: StatBoxProps) => { + const theme = useTheme(); + const textColor = palette ? theme.palette[palette].contrastText : "inherit"; + + return ( + + + + {title} + {tooltip && ( + + )} + + {subtitle} + {children} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/StatusLabel.tsx b/client/src/Components/v2/design-elements/StatusLabel.tsx index 30adf5917..c9991f0d4 100644 --- a/client/src/Components/v2/design-elements/StatusLabel.tsx +++ b/client/src/Components/v2/design-elements/StatusLabel.tsx @@ -17,14 +17,17 @@ export const StatusLabel = ({ sx, }: { status: MonitorStatus; - isActive: boolean; + isActive?: boolean; sx?: SxProps; }) => { const { t } = useTranslation(); const theme = useTheme(); const palette = getStatusPalette(status); - const determineStatus = (isActive: boolean, status: MonitorStatus): string => { + const determineStatus = ( + isActive: boolean | undefined, + status: MonitorStatus + ): string => { if (isActive === false) { return t("pages.common.monitors.status.paused"); } diff --git a/client/src/Components/v2/design-elements/Tooltip.tsx b/client/src/Components/v2/design-elements/Tooltip.tsx new file mode 100644 index 000000000..f83b1331c --- /dev/null +++ b/client/src/Components/v2/design-elements/Tooltip.tsx @@ -0,0 +1,79 @@ +import MuiTooltip from "@mui/material/Tooltip"; +import { useTheme } from "@mui/material/styles"; +import { Info } from "lucide-react"; + +import type { TooltipProps } from "@mui/material/Tooltip"; + +type StyledTooltipProps = TooltipProps; + +export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) => { + const theme = useTheme(); + + return ( + + {props.children} + + ); +}; + +export interface TooltipWithInfoProps extends Omit { + iconSize?: number; + iconColor?: string; +} + +export const TooltipWithInfo = ({ + iconSize = 14, + iconColor, + ...props +}: TooltipWithInfoProps) => { + const theme = useTheme(); + + const defaultColor = theme.palette.text.secondary; + + return ( + + + { + e.currentTarget.style.opacity = "1"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.opacity = "0.7"; + }} + /> + + + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index 374dabef7..6ebffc624 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -8,3 +8,10 @@ export * from "./BaseBox"; export * from "./Table"; export { DataTable as Table } from "./Table"; export * from "./StatusLabel"; +export * from "./PulseDot"; +export * from "./Dot"; +export * from "./MonitorStatus"; +export { default as Icon } from "./Icon"; +export * from "./Tooltip"; +export * from "./StatBox"; +export * from "./BaseChart"; diff --git a/client/src/Components/v2/inputs/ToggleButton.tsx b/client/src/Components/v2/inputs/ToggleButton.tsx new file mode 100644 index 000000000..40b4309b8 --- /dev/null +++ b/client/src/Components/v2/inputs/ToggleButton.tsx @@ -0,0 +1,52 @@ +import ToggleButton from "@mui/material/ToggleButton"; +import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; +import type { ToggleButtonProps } from "@mui/material/ToggleButton"; +import type { ToggleButtonGroupProps } from "@mui/material/ToggleButtonGroup"; +import { useTheme } from "@mui/material/styles"; + +export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => { + return ( + + ); +}; + +export const ToggleButtonGroupInput = ({ sx, ...props }: ToggleButtonGroupProps) => { + const theme = useTheme(); + return ( + + ); +}; diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index 9a76f6f7f..87698a4f0 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -4,3 +4,7 @@ export { SelectInput as Select } from "./Select"; export { CheckboxInput as Checkbox } from "./Checkbox"; export { AutoCompleteInput as Autocomplete } from "./AutoComplete"; export { TextInput as TextField } from "./TextInput"; +export { + ToggleButtonInput as ToggleButton, + ToggleButtonGroupInput as ToggleButtonGroup, +} from "./ToggleButton"; diff --git a/client/src/Components/v2/monitors/MonitorStatBoxes.tsx b/client/src/Components/v2/monitors/MonitorStatBoxes.tsx new file mode 100644 index 000000000..65be06f3b --- /dev/null +++ b/client/src/Components/v2/monitors/MonitorStatBoxes.tsx @@ -0,0 +1,69 @@ +import Stack from "@mui/material/Stack"; +import { StatBox } from "@/Components/v2/design-elements"; + +import { useTheme } from "@mui/material/styles"; +import type { MonitorStats, Monitor } from "@/Types/Monitor"; +import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js"; +import { getStatusPalette } from "@/Utils/MonitorUtils"; +import { useTranslation } from "react-i18next"; + +interface MonitorStatBoxesProps { + monitor?: Monitor; + monitorStats: MonitorStats | null; + certificateExpiry?: string; +} + +export const MonitorStatBoxes = ({ + monitor, + monitorStats, + certificateExpiry, +}: MonitorStatBoxesProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + if (!monitorStats || !monitor) { + return null; + } + + const timeOfLastFailure = monitorStats?.timeOfLastFailure || 0; + const timeSinceLastFailure = + timeOfLastFailure > 0 + ? Date.now() - timeOfLastFailure + : Date.now() - new Date(monitorStats?.createdAt || 0).getTime(); + + // Determine time since last check + const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0; + const timeSinceLastCheck = Date.now() - timeOfLastCheck; + + const streakTime = getHumanReadableDuration(timeSinceLastFailure); + + const lastCheckTime = getHumanReadableDuration(timeSinceLastCheck); + const palette = getStatusPalette(monitor?.status); + + return ( + + + + + + {monitor?.type === "http" && ( + + )} + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/HistogramDetails.tsx b/client/src/Components/v2/monitors/charts/HistogramDetails.tsx new file mode 100644 index 000000000..8858a9784 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramDetails.tsx @@ -0,0 +1,163 @@ +import { BaseChart, BaseBox } from "@/Components/v2/design-elements"; +import { Clock } from "lucide-react"; +import { + AreaChart, + Area, + XAxis, + Tooltip, + CartesianGrid, + ResponsiveContainer, + Text, +} from "recharts"; +import Typography from "@mui/material/Typography"; + +import { useSelector } from "react-redux"; +import { + formatDateWithTz, + tickDateFormatLookup, + tooltipDateFormatLookup, +} from "@/Utils/TimeUtils"; +import { useTheme } from "@mui/material/styles"; +import type { GroupedCheck } from "@/Types/Check"; +import type { RootState } from "@/Types/state"; + +type XTickProps = { + x: number; + y: number; + payload: { value: any }; + range: string; +}; + +export const XTick = ({ x, y, payload, range }: XTickProps) => { + const format = tickDateFormatLookup(range); + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + return ( + + {formatDateWithTz(payload?.value, format, uiTimezone)} + + ); +}; + +type ResponseTimeToolTipProps = { + active?: boolean | undefined; + payload?: any[]; + label?: string | number; + range: string; + theme: any; + uiTimezone: string; +}; + +const ResponseTimeToolTip = ({ + active, + payload, + label, + range, + theme, + uiTimezone, +}: ResponseTimeToolTipProps) => { + if (!label) return null; + if (!payload) return null; + if (!active) return null; + + const format = tooltipDateFormatLookup(range); + const responseTime = Math.floor(payload?.[0]?.payload?.avgResponseTime || 0); + return ( + + {formatDateWithTz(String(label), format, uiTimezone)} + Response time: {responseTime} ms + + ); +}; + +export const HistogramDetails = ({ + checks, + range, +}: { + checks: GroupedCheck[]; + range: string; +}) => { + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + return ( + + } + title="Response times" + > + + + + + + + + + + ( + + )} + /> + + ( + + )} + /> + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx new file mode 100644 index 000000000..1c25c339e --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx @@ -0,0 +1,166 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { BaseChart } from "@/Components/v2/design-elements"; +import { useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; +import { formatDateWithTz } from "@/Utils/timeUtilsLegacy"; +import { useTranslation } from "react-i18next"; +import { ResponsiveContainer, BarChart, XAxis, Bar, Cell, Tooltip } from "recharts"; +import { getResponseTimeColor } from "@/Utils/MonitorUtils"; +import type { GroupedCheck } from "@/Types/Check"; +import type { RootState } from "@/Types/state"; + +interface XLabelProps { + p1: GroupedCheck; + p2: GroupedCheck; + range: string; +} + +const XLabel = ({ p1, p2, range }: XLabelProps) => { + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + const dateFormat = range === "day" ? "MMM D, h:mm A" : "MMM D"; + return ( + <> + + {formatDateWithTz(p1.bucketDate, dateFormat, uiTimezone)} + + + {formatDateWithTz(p2.bucketDate, dateFormat, uiTimezone)} + + + ); +}; + +interface HistogramStatusProps { + title: string; + icon: React.ReactNode; + checks?: GroupedCheck[]; + range: string; +} + +export const HistogramStatus = ({ + title, + icon, + checks = [], + range, +}: HistogramStatusProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const CustomTooltip = ({ + active, + payload, + }: { + active?: boolean; + payload?: Array<{ payload: GroupedCheck }>; + }) => { + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + if (!active || !payload?.length) return null; + const d = payload[0]?.payload; + const avg = d?.avgResponseTime ?? 0; + const titleText = t("common.charts.labels.averageResponseTime"); + const fmt = range === "30d" ? "MMM D, YYYY" : "ddd, MMM D, YYYY, h:mm A"; + let dateLabel = ""; + if (d?.bucketDate) { + const base = new Date(d.bucketDate); + const midBucket = + range === "30d" ? new Date(base.getTime() + 12 * 60 * 60 * 1000) : base; + dateLabel = formatDateWithTz(midBucket.toISOString(), fmt, uiTimezone); + } + return ( + + {dateLabel ? ( + + {dateLabel} + + ) : null} + {titleText} + {Math.floor(avg)} ms + + ); + }; + + const totalChecks = checks.reduce((acc, check) => acc + (check.totalChecks || 0), 0); + + return ( + + + + + Total checks: {totalChecks} + + + + + + } + /> + } + /> + + {checks?.map((groupedCheck, index) => { + const fillColor = getResponseTimeColor(groupedCheck.avgResponseTime); + return ( + + ); + })} + + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/RadialAvgResponse.tsx b/client/src/Components/v2/monitors/charts/RadialAvgResponse.tsx new file mode 100644 index 000000000..b54bfc764 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/RadialAvgResponse.tsx @@ -0,0 +1,91 @@ +import { BaseChart } from "@/Components/v2/design-elements"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Gauge } from "lucide-react"; +import { Cell, RadialBarChart, RadialBar, ResponsiveContainer } from "recharts"; + +import { useTranslation } from "react-i18next"; +import { getResponseTimeColor } from "@/Utils/MonitorUtils"; +import { useTheme } from "@mui/material/styles"; + +export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const chartData = [ + { name: "max", value: max - avg, color: "transparent" }, + { name: "avg", value: avg, color: "red" }, + ]; + + const palette = getResponseTimeColor(avg); + const msg: Record = { + success: "Excellent", + warning: "Average", + danger: "Poor", + }; + + return ( + + } + title={t("common.charts.labels.averageResponseTime")} + > + + + + + + + + + + + {t("common.charts.labels.low")} + {t("common.charts.labels.high")} + + + + {msg[palette]} + + {`${avg?.toFixed()}ms`} + + + + ); +}; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx index a7cfa34fd..de38d2cab 100644 --- a/client/src/Components/v2/monitors/index.tsx +++ b/client/src/Components/v2/monitors/index.tsx @@ -1 +1,5 @@ export * from "./ControlsFilter"; +export * from "./MonitorStatBoxes"; +export * from "./charts/HistogramStatus"; +export * from "./charts/RadialAvgResponse"; +export * from "./charts/HistogramDetails"; diff --git a/client/src/Hooks/UseToast.ts b/client/src/Hooks/UseToast.ts index 2ee275253..8823aa675 100644 --- a/client/src/Hooks/UseToast.ts +++ b/client/src/Hooks/UseToast.ts @@ -1,4 +1,5 @@ import { toast, type ToastOptions } from "react-toastify"; + export const useToast = () => { const showToast = (message: string, options?: ToastOptions) => { toast.dismiss(); diff --git a/client/src/Pages/Checks/Components/IncidentTable/index.jsx b/client/src/Pages/Checks/Components/IncidentTable/index.jsx index 31475f8b1..857f31bff 100644 --- a/client/src/Pages/Checks/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Checks/Components/IncidentTable/index.jsx @@ -13,7 +13,7 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; //Utils -import { formatDateWithTz } from "@/Utils/timeUtils.js"; +import { formatDateWithTz } from "@/Utils/timeUtilsLegacy.js"; import { useSelector } from "react-redux"; import { useState } from "react"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx index d68b0b85e..bfca9e93f 100644 --- a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx @@ -9,7 +9,7 @@ import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; import { useEffect, useState } from "react"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { formatDateWithTz } from "@/Utils/timeUtils"; +import { formatDateWithTz } from "@/Utils/timeUtilsLegacy.js"; import { useSelector } from "react-redux"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx index 05242c1f3..2a03a8822 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx @@ -9,7 +9,7 @@ import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; import IncidentActionsMenu from "./IncidentActionsMenu.jsx"; //Utils -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { useSelector } from "react-redux"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx index 7c6fb611a..ca5bb640f 100644 --- a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx +++ b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx @@ -2,7 +2,7 @@ import PropTypes from "prop-types"; import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { getHumanReadableDuration } from "@/Utils/timeUtils.js"; +import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js"; import Icon from "@/Components/v1/Icon"; import { useTranslation } from "react-i18next"; import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration"; diff --git a/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js b/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js index e9b1da6d1..a4f434a80 100644 --- a/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js +++ b/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js @@ -1,4 +1,4 @@ -import { getHumanReadableDuration } from "@/Utils/timeUtils.js"; +import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js"; const useGetIncidentsDuration = (incident, isActive) => { if (!incident?.startTime) { return "-"; diff --git a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx index b169a6a85..13ed63618 100644 --- a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx @@ -7,7 +7,7 @@ import CircularProgress from "@mui/material/CircularProgress"; import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js"; +import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js"; import { formatBytes } from "../../utils/utils.js"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx index 07b504c77..194e13821 100644 --- a/client/src/Pages/Logs/Diagnostics/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/index.jsx @@ -7,7 +7,7 @@ import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import { useFetchDiagnostics } from "../../../Hooks/logHooks.js"; -import { getHumanReadableDuration } from "../../../Utils/timeUtils.js"; +import { getHumanReadableDuration } from "../../../Utils/timeUtilsLegacy.js"; import { formatBytes, getPercentage } from "./utils/utils.js"; const Diagnostics = () => { diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx index 22a61084d..a5e853160 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx @@ -6,7 +6,7 @@ import { MS_PER_HOUR, MS_PER_DAY, MS_PER_WEEK, -} from "../../../../Utils/timeUtils.js"; +} from "../../../../Utils/timeUtilsLegacy.js"; const useMaintenanceActions = () => { const MS_LOOKUP = { diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx index 3414334a7..4ab56bef9 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx @@ -6,7 +6,7 @@ import { MS_PER_HOUR, MS_PER_DAY, MS_PER_WEEK, -} from "../../../../Utils/timeUtils.js"; +} from "../../../../Utils/timeUtilsLegacy.js"; const useMaintenanceData = () => { const REVERSE_REPEAT_LOOKUP = { 0: "none", diff --git a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx index 765858050..11875bb6a 100644 --- a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx +++ b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx @@ -6,7 +6,7 @@ import Icon from "@/Components/v1/Icon"; import ActionsMenu from "./ActionsMenu/index.jsx"; import { memo } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { formatDurationRounded } from "../../../Utils/timeUtils.js"; +import { formatDurationRounded } from "../../../Utils/timeUtilsLegacy.js"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx index 06b92c933..d8c118237 100644 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx +++ b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx @@ -11,7 +11,7 @@ import { import { useTheme } from "@emotion/react"; import { useMemo, useState } from "react"; import { Box, Stack, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../../Utils/timeUtilsLegacy.js"; import { useSelector } from "react-redux"; const config = { diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx index 872b1e293..6ff26c739 100644 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx +++ b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx @@ -1,7 +1,7 @@ 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/timeUtils.js"; +import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js"; import { useTranslation } from "react-i18next"; const PageSpeedStatusBoxes = ({ shouldRender, monitorStats }) => { diff --git a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx index 4642e2336..4a85863c4 100644 --- a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx +++ b/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx @@ -6,7 +6,7 @@ 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/timeUtils.js"; +import { formatDateWithTz, formatDurationSplit } from "@/Utils/timeUtilsLegacy.js"; import { useMonitorUtils } from "@/Hooks/useMonitorUtils.js"; import { useState } from "react"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx deleted file mode 100644 index b8496f5ea..000000000 --- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx +++ /dev/null @@ -1,160 +0,0 @@ -// Components -import { Stack, Typography, Box } from "@mui/material"; -import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import UpBarChart from "../Charts/UpBarChart.jsx"; -import DownBarChart from "../Charts/DownBarChart.jsx"; -import ResponseGaugeChart from "../Charts/ResponseGaugeChart.jsx"; -import SkeletonLayout from "./skeleton.jsx"; -// Utils -import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { useState } from "react"; - -const ChartBoxes = ({ - isLoading = false, - monitorData, - dateRange, - uiTimezone, - dateFormat, -}) => { - // Local state - const [hoveredUptimeData, setHoveredUptimeData] = useState(null); - const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null); - const theme = useTheme(); - if (isLoading) { - return ; - } - - const noIncidentsMessage = "Great. No Incidents, yet!"; - - return ( - - - } - header="Uptime" - isEmpty={monitorData?.groupedUpChecks?.length === 0} - > - - - Total Checks - - {hoveredUptimeData !== null - ? hoveredUptimeData.totalChecks - : (monitorData?.groupedUpChecks?.reduce((count, checkGroup) => { - return count + checkGroup.totalChecks; - }, 0) ?? 0)} - - {hoveredUptimeData !== null && hoveredUptimeData.time !== null && ( - - {formatDateWithTz(hoveredUptimeData._id, dateFormat, uiTimezone)} - - )} - - - - {hoveredUptimeData !== null ? "Avg Response Time" : "Uptime Percentage"} - - - {hoveredUptimeData !== null - ? Math.floor(hoveredUptimeData?.originalAvgResponseTime ?? 0) - : Math.floor((monitorData?.groupedUptimePercentage ?? 0) * 100)} - - {hoveredUptimeData !== null ? " ms" : " %"} - - - - - - - - } - header="Incidents" - noDataMessage={noIncidentsMessage} - isEmpty={monitorData?.groupedDownChecks?.length === 0} - > - - - - {hoveredIncidentsData !== null - ? hoveredIncidentsData.totalChecks - : (monitorData?.groupedDownChecks?.reduce((count, checkGroup) => { - return count + checkGroup.totalChecks; - }, 0) ?? 0)} - - {hoveredIncidentsData !== null && hoveredIncidentsData.time !== null && ( - - {formatDateWithTz(hoveredIncidentsData._id, dateFormat, uiTimezone)} - - )} - - - - - - } - header="Average Response Time" - > - - - - ); -}; - -export default ChartBoxes; - -ChartBoxes.propTypes = { - isLoading: PropTypes.bool, - monitorData: PropTypes.object, - dateRange: PropTypes.string.isRequired, - uiTimezone: PropTypes.string.isRequired, - dateFormat: PropTypes.string.isRequired, - hoveredUptimeData: PropTypes.object, - setHoveredUptimeData: PropTypes.func, - hoveredIncidentsData: PropTypes.object, - setHoveredIncidentsData: PropTypes.func, -}; diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx deleted file mode 100644 index 750b3b20a..000000000 --- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx +++ /dev/null @@ -1,30 +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/Details/Components/Charts/CustomLabels.jsx b/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx deleted file mode 100644 index 472c8d058..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import PropTypes from "prop-types"; -import { useSelector } from "react-redux"; -import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; - -const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => { - const uiTimezone = useSelector((state) => state.ui.timezone); - const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D"; - - return ( - <> - - {formatDateWithTz(firstDataPoint._id, dateFormat, uiTimezone)} - - - {formatDateWithTz(lastDataPoint._id, dateFormat, uiTimezone)} - - - ); -}; - -CustomLabels.propTypes = { - x: PropTypes.number.isRequired, - width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - firstDataPoint: PropTypes.object, - lastDataPoint: PropTypes.object, - type: PropTypes.string.isRequired, -}; - -export default CustomLabels; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx deleted file mode 100644 index c5e08ecca..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import { memo, useState } from "react"; -import { useTheme } from "@mui/material"; -import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; -import PropTypes from "prop-types"; -import CustomLabels from "./CustomLabels.jsx"; - -const DownBarChart = memo(({ groupedDownChecks = [], type, onBarHover }) => { - const theme = useTheme(); - - const [chartHovered, setChartHovered] = useState(false); - const [hoveredBarIndex, setHoveredBarIndex] = useState(null); - - return ( - - { - setChartHovered(true); - onBarHover({ time: null, totalChecks: 0 }); - }} - onMouseLeave={() => { - setChartHovered(false); - setHoveredBarIndex(null); - onBarHover(null); - }} - > - - } - /> - - {groupedDownChecks?.map((entry, index) => { - return ( - { - setHoveredBarIndex(index); - onBarHover(entry); - }} - onMouseLeave={() => { - setHoveredBarIndex(null); - onBarHover({ time: null, totalChecks: 0 }); - }} - /> - ); - })} - - - - ); -}); - -DownBarChart.displayName = "DownBarChart"; -DownBarChart.propTypes = { - monitor: PropTypes.shape({ - groupedDownChecks: PropTypes.arrayOf(PropTypes.object), - }), - type: PropTypes.string, - onBarHover: PropTypes.func, -}; -export default DownBarChart; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx deleted file mode 100644 index 084a711c3..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx +++ /dev/null @@ -1,118 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@mui/material"; -import { ResponsiveContainer, RadialBarChart, RadialBar, Cell } from "recharts"; -import { useTranslation } from "react-i18next"; - -const ResponseGaugeChart = ({ avgResponseTime }) => { - const theme = useTheme(); - const { t } = useTranslation(); - let max = 1000; // max ms - - const data = [ - { response: max, fill: "transparent", background: false }, - { response: avgResponseTime, background: true }, - ]; - let responseTime = Math.floor(avgResponseTime); - let responseProps = - responseTime <= 200 - ? { - category: "Excellent", - main: theme.palette.success.main, - bg: theme.palette.success.lowContrast, - } - : responseTime <= 500 - ? { - category: "Fair", - main: theme.palette.success.main, - bg: theme.palette.success.lowContrast, - } - : responseTime <= 600 - ? { - category: "Acceptable", - main: theme.palette.warning.main, - bg: theme.palette.warning.lowContrast, - } - : { - category: "Poor", - main: theme.palette.error.main, - bg: theme.palette.error.lowContrast, - }; - - return ( - - - - {t("low")} - - - {t("high")} - - - {responseProps.category} - - - {responseTime} ms - - - - - - - - ); -}; - -ResponseGaugeChart.propTypes = { - avgResponseTime: PropTypes.number.isRequired, -}; - -export default ResponseGaugeChart; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx deleted file mode 100644 index 7e7a940e0..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import MonitorDetailsAreaChart from "@/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import SkeletonLayout from "./ResponseTimeChartSkeleton.jsx"; -import PropTypes from "prop-types"; - -const ResponseTImeChart = ({ isLoading = false, groupedChecks = [], dateRange }) => { - if (isLoading) { - return ; - } - - return ( - - } - header="Response Times" - > - - - ); -}; - -ResponseTImeChart.propTypes = { - isLoading: PropTypes.bool, - groupedChecks: PropTypes.array, - dateRange: PropTypes.string, -}; - -export default ResponseTImeChart; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx deleted file mode 100644 index a70757e36..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Skeleton } from "@mui/material"; -const ResponseTimeChartSkeleton = () => { - return ( - - ); -}; - -export default ResponseTimeChartSkeleton; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx deleted file mode 100644 index 8f1af8912..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import { memo, useState } from "react"; -import { useTheme } from "@mui/material"; -import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; -import PropTypes from "prop-types"; -import CustomLabels from "./CustomLabels.jsx"; - -const getThemeColor = (responseTime) => { - if (responseTime < 200) { - return "success"; - } else if (responseTime < 300) { - return "warning"; - } else { - return "error"; - } -}; - -const UpBarChart = memo(({ groupedUpChecks = [], type, onBarHover }) => { - const theme = useTheme(); - const [chartHovered, setChartHovered] = useState(false); - const [hoveredBarIndex, setHoveredBarIndex] = useState(null); - - return ( - - { - setChartHovered(true); - onBarHover({ time: null, totalChecks: 0, originalAvgResponseTime: 0 }); - }} - onMouseLeave={() => { - setChartHovered(false); - setHoveredBarIndex(null); - onBarHover(null); - }} - > - - } - /> - - {groupedUpChecks?.map((entry, index) => { - const themeColor = getThemeColor(entry.originalAvgResponseTime); - return ( - { - setHoveredBarIndex(index); - onBarHover(entry); - }} - onMouseLeave={() => { - setHoveredBarIndex(null); - onBarHover({ - time: null, - totalChecks: 0, - groupUptimePercentage: 0, - }); - }} - /> - ); - })} - - - - ); -}); - -// Add display name for the component -UpBarChart.displayName = "UpBarChart"; - -// Validate props using PropTypes -UpBarChart.propTypes = { - monitor: PropTypes.shape({ - groupedUpChecks: PropTypes.array, - }), - type: PropTypes.string, - onBarHover: PropTypes.func, -}; -export default UpBarChart; diff --git a/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx new file mode 100644 index 000000000..2cfdc1f15 --- /dev/null +++ b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx @@ -0,0 +1,100 @@ +import { Table, Pagination, StatusLabel } from "@/Components/v2/design-elements"; +import Box from "@mui/material/Box"; +import type { Header } from "@/Components/v2/design-elements/"; +import type { Check } from "@/Types/Check"; + +import { useNavigate } from "react-router"; +import { useTranslation } from "react-i18next"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; +import type { RootState } from "@/Types/state"; +import { useSelector } from "react-redux"; + +const getHeaders = (t: Function, uiTimezone: string) => { + const headers: Header[] = [ + { + id: "status", + content: t("common.table.headers.status"), + render: (row) => { + return ; + }, + }, + { + id: "date", + content: t("pages.checks.table.headers.dateTime"), + render: (row) => { + return formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone); + }, + }, + { + id: "message", + content: t("common.table.headers.message"), + render: (row) => { + return row.message || "N/A"; + }, + }, + { + id: "statusCode", + content: t("pages.checks.table.headers.statusCode"), + render: (row) => { + return row.statusCode || "N/A"; + }, + }, + ]; + return headers; +}; + +export const ChecksTable = ({ + checks, + count, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}: { + checks: Check[]; + count: number; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; +}) => { + const navigate = useNavigate(); + const { t } = useTranslation(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + const headers = getHeaders(t, uiTimezone); + + 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); + }; + + return ( + + { + navigate(`/checks/${row.id}`); + }} + /> + + + ); +}; diff --git a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx b/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx deleted file mode 100644 index 2d7991324..000000000 --- a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx +++ /dev/null @@ -1,222 +0,0 @@ -import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import PropTypes from "prop-types"; -import Icon from "@/Components/v1/Icon"; -import Table from "@/Components/v1/Table/index.jsx"; -import TablePagination from "@/Components/v1/Table/TablePagination/index.jsx"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { useTranslation } from "react-i18next"; -import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; -import SkeletonLayout from "./skeleton.jsx"; -import Tooltip from "@mui/material/Tooltip"; -import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; -import Stack from "@mui/material/Stack"; -import { lighten, useTheme } from "@mui/material"; - -/** - * Creates tooltip content with detailed timing breakdown - * Following the pattern from IncidentTable's GetTooltip function - * @param {Object} timings - Timing object (guaranteed to have phases by caller) - * @param {Object} theme - MUI theme object - * @param {Function} t - Translation function - * @returns {JSX.Element} Tooltip content - */ -const GetTooltip = (timings, theme, t) => { - const phases = timings.phases; - const timingDetails = [ - { label: t("dnsLookup"), value: phases.dns }, - { label: t("tcpConnection"), value: phases.tcp }, - { label: t("tlsHandshake"), value: phases.tls }, - { label: t("waitTime"), value: phases.wait }, - { label: t("timeToFirstByte"), value: phases.firstByte }, - { label: t("download"), value: phases.download }, - { label: t("total"), value: phases.total }, - ].filter((item) => item.value > 0); - - return ( - - - {t("responseTimeBreakdown")} - - {timingDetails.map((detail, index, array) => ( - - - {detail.label}: - - - {Math.round(detail.value)} ms - - - ))} - - ); -}; - -const ResponseTable = ({ - isLoading = false, - checks = [], - checksCount, - uiTimezone, - page, - setPage, - rowsPerPage, - setRowsPerPage, -}) => { - const { t } = useTranslation(); - const theme = useTheme(); - - if (isLoading) { - return ; - } - - const headers = [ - { - id: "status", - content: t("status"), - render: (row) => { - const status = row.status === true ? "up" : "down"; - - return ( - - ); - }, - }, - { - id: "date", - content: t("date&Time"), - render: (row) => - formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone), - }, - { - id: "statusCode", - content: t("statusCode"), - render: (row) => (row.statusCode ? row.statusCode : "N/A"), - }, - { - id: "message", - content: t("message"), - render: (row) => row.message, - }, - { - id: "responseTime", - content: t("responseTime"), - render: (row) => { - const hasTimings = row.timings && row.timings.phases; - const responseTime = row.responseTime; - const responseTimeDisplay = - responseTime !== null && responseTime !== undefined - ? `${Math.round(responseTime)} ms` - : "N/A"; - - if (!hasTimings) { - return responseTimeDisplay; - } - - return ( - - - {responseTimeDisplay} - - - ); - }, - }, - ]; - - return ( - - } - header="Response Times" - height="100%" - > -
- - - ); -}; - -ResponseTable.propTypes = { - isLoading: PropTypes.bool, - checks: PropTypes.array, - checksCount: PropTypes.number, - uiTimezone: PropTypes.string.isRequired, - page: PropTypes.number.isRequired, - setPage: PropTypes.func.isRequired, - rowsPerPage: PropTypes.number.isRequired, - setRowsPerPage: PropTypes.func.isRequired, -}; - -export default ResponseTable; diff --git a/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx b/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx deleted file mode 100644 index 2665ffaf1..000000000 --- a/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx deleted file mode 100644 index 252873cf9..000000000 --- a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; -import StatBox from "@/Components/v1/StatBox/index.jsx"; - -import PropTypes from "prop-types"; -import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js"; -import { useTheme } from "@mui/material/styles"; -import { Typography } from "@mui/material"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; - -const UptimeStatusBoxes = ({ - isLoading = false, - monitor, - monitorStats, - certificateExpiry, -}) => { - const theme = useTheme(); - const { determineState } = useMonitorUtils(); - - // Determine time since last failure - const timeOfLastFailure = monitorStats?.timeOfLastFailure; - const timeSinceLastFailure = - timeOfLastFailure > 0 - ? Date.now() - timeOfLastFailure - : Date.now() - new Date(monitorStats?.createdAt); - - // Determine time since last check - const timeOfLastCheck = monitorStats?.lastCheckTimestamp; - const timeSinceLastCheck = Date.now() - timeOfLastCheck; - - const streakTime = getHumanReadableDuration(timeSinceLastFailure); - - const lastCheckTime = getHumanReadableDuration(timeSinceLastCheck); - return ( - - - - {lastCheckTime} - {"ago"} - - } - /> - - {monitorStats?.lastResponseTime} - {"ms"} - - } - /> - {monitor?.type === "http" && ( - - {certificateExpiry} - - } - /> - )} - - ); -}; - -UptimeStatusBoxes.propTypes = { - isLoading: PropTypes.bool, - monitor: PropTypes.object, - monitorStats: PropTypes.object, - certificateExpiry: PropTypes.string, -}; - -export default UptimeStatusBoxes; diff --git a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx b/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx index e74a99d82..8912438d8 100644 --- a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx +++ b/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx @@ -1,7 +1,7 @@ import { logger } from "../../../../Utils/Logger.js"; import { useEffect, useState } from "react"; import { networkService } from "../../../../main.jsx"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; const useCertificateFetch = ({ monitor, diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx new file mode 100644 index 000000000..1a6177ddc --- /dev/null +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -0,0 +1,166 @@ +import { BasePage } from "@/Components/v2/design-elements"; +import { HeaderMonitorControls, HeaderTimeRange } from "@/Components/v2/common"; +import Stack from "@mui/material/Stack"; +import { + HistogramStatus, + RadialAvgResponse, + HistogramDetails, +} from "@/Components/v2/monitors"; +import { TrendingUp, AlertTriangle } from "lucide-react"; +import { ChecksTable } from "@/Pages/Uptime/Details/Components/ChecksTable"; + +import { useTheme } from "@mui/material/styles"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; +import { useState, useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { useSelector } from "react-redux"; +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"; + +const certificateDateFormat = "MMM D, YYYY h A"; + +interface CertificateResponse { + certificateDate: string; +} + +const UptimeDetailsPage = () => { + const theme = useTheme(); + const isAdmin = useIsAdmin(); + const { monitorId } = useParams<{ monitorId: string }>(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + const [dateRange, setDateRange] = useState("recent"); + + const monitorDetailsUrl = useMemo(() => { + if (!monitorId) { + return null; + } + const params = new URLSearchParams(); + params.append("dateRange", dateRange); + params.append("normalize", "true"); + return `/monitors/uptime/details/${monitorId}?${params.toString()}`; + }, [monitorId, dateRange]); + + const { + data: monitorDetailsData, + isLoading: monitorIsLoading, + refetch: refetchMonitor, + } = useGet( + monitorDetailsUrl, + {}, + { refreshInterval: 10000, keepPreviousData: true } + ); + + const monitorData = monitorDetailsData?.monitorData; + const monitor = monitorData?.monitor; + const monitorStats = monitorDetailsData?.monitorStats ?? null; + + // Certificate fetch - only for HTTP monitors + const certificateUrl = useMemo(() => { + if (!monitorId || monitor?.type !== "http") { + return null; + } + return `/monitors/certificate/${monitorId}`; + }, [monitorId, monitor?.type]); + + const { data: certificateData } = useGet(certificateUrl); + + const certificateExpiry = useMemo(() => { + if (!certificateData?.certificateDate) { + return undefined; + } + return ( + formatDateWithTz( + certificateData.certificateDate, + certificateDateFormat, + uiTimezone + ) ?? "N/A" + ); + }, [certificateData, uiTimezone]); + + const checksUrl = useMemo(() => { + if (!monitorId || !monitor?.type) { + return null; + } + const params = new URLSearchParams(); + params.append("type", monitor.type); + params.append("sortOrder", "desc"); + params.append("dateRange", dateRange); + params.append("page", String(page)); + params.append("rowsPerPage", String(rowsPerPage)); + return `/checks/${monitorId}?${params.toString()}`; + }, [monitorId, monitor?.type, dateRange, page, rowsPerPage]); + + const { data: checksData, isLoading: checksIsLoading } = useGet( + checksUrl, + {}, + { keepPreviousData: true } + ); + + const checks = checksData?.checks ?? []; + const checksCount = checksData?.checksCount ?? 0; + + return ( + + + + + + } + checks={monitorData?.groupedUpChecks ?? []} + range={dateRange} + /> + } + checks={monitorData?.groupedDownChecks ?? []} + range={dateRange} + /> + + + + + + ); +}; + +export default UptimeDetailsPage; diff --git a/client/src/Pages/Uptime/Details/index.jsx b/client/src/Pages/Uptime/Details/old.jsx similarity index 100% rename from client/src/Pages/Uptime/Details/index.jsx rename to client/src/Pages/Uptime/Details/old.jsx diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 5b05817bb..e3e90429a 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -88,7 +88,11 @@ const UptimeMonitorsPage = () => { data: monitors, isLoading: monitorsLoading, error, - } = useGet("/monitors/team?type=http&type=ping&type=port&type=docker"); + } = useGet( + "/monitors/team?type=http&type=ping&type=port&type=docker", + {}, + { keepPreviousData: true } + ); const { data: monitorsWithChecksData, @@ -98,7 +102,7 @@ const UptimeMonitorsPage = () => { } = useGet( monitorsWithChecksUrl, {}, - { refreshInterval: 5000 } + { refreshInterval: 5000, keepPreviousData: true } ); const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {}; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index e37bae063..715d4374e 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -16,7 +16,7 @@ import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed.jsx"; // Uptime import Uptime from "../Pages/Uptime/Monitors"; -import UptimeDetails from "../Pages/Uptime/Details/index.jsx"; +import UptimeDetails from "../Pages/Uptime/Details"; import UptimeCreate from "../Pages/Uptime/Create/index.jsx"; // PageSpeed @@ -103,7 +103,13 @@ const Routes = () => { /> } + element={ + <> + + + + + } /> { const tickFormatLookup: Record = { - "1h": "h:mm A", - "24h": "h:mm A", - "7d": "MM/D, h:mm A", - "30d": "ddd. M/D", + recent: "h:mm A", + day: "h:mm A", + week: "MM/D, h:mm A", + month: "ddd. M/D", }; const format = tickFormatLookup[range]; if (format === undefined) { @@ -39,10 +39,10 @@ export const tickDateFormatLookup = (range: string) => { export const tooltipDateFormatLookup = (range: string) => { const dateFormatLookup: Record = { - "1h": "ddd. MMMM D, YYYY, hh:mm A", - "24h": "ddd. MMMM D, YYYY, hh:mm A", - "7d": "ddd. MMMM D, YYYY, hh:mm A", - "30d": "ddd. MMMM D, YYYY", + recent: "ddd. MMMM D, YYYY, hh:mm A", + day: "ddd. MMMM D, YYYY, hh:mm A", + week: "ddd. MMMM D, YYYY, hh:mm A", + month: "ddd. MMMM D, YYYY", }; const format = dateFormatLookup[range]; if (format === undefined) { diff --git a/client/src/Utils/timeUtils.js b/client/src/Utils/timeUtilsLegacy.js similarity index 87% rename from client/src/Utils/timeUtils.js rename to client/src/Utils/timeUtilsLegacy.js index f6e2dcec0..365572f6b 100644 --- a/client/src/Utils/timeUtils.js +++ b/client/src/Utils/timeUtilsLegacy.js @@ -135,3 +135,33 @@ export const formatDateWithTz = (timestamp, format, timezone) => { const formattedDate = dayjs(timestamp).tz(timezone).format(format); return formattedDate; }; + +export const tickDateFormatLookup = (range) => { + switch (range) { + case "recent": + return "h:mm A"; + case "day": + return "h A"; + case "week": + return "MMM D"; + case "month": + return "MMM D"; + default: + return "MMM D, h A"; + } +}; + +export const tooltipDateFormatLookup = (range) => { + switch (range) { + case "recent": + return "MMM D, h:mm A"; + case "day": + return "MMM D, h:mm A"; + case "week": + return "ddd, MMM D"; + case "month": + return "ddd, MMM D"; + default: + return "MMM D, h:mm A"; + } +}; diff --git a/client/src/index.css b/client/src/index.css index 8b2a1cfce..48737d4bb 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -50,7 +50,8 @@ html { cursor: not-allowed; } -.Toastify__toast-container { +/* .Toastify__toast-container { + min-width: 300px; width: auto; } .Toastify__toast-body .alert { @@ -68,7 +69,7 @@ html { } .Toastify [class*="Toastify__toast-theme"] { background-color: transparent; -} +} */ .MuiTouchRipple-root { display: none; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index de02815b1..d098e54de 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -4,7 +4,9 @@ "monitoringAgentName": "Capture", "buttons": { "toggleTheme": "Toggles light & dark", - "create": "Create" + "testNotifications": "Test notifications", + "create": "Create", + "incidents": "Incidents" }, "toasts": { "networkError": "Network error", @@ -22,6 +24,15 @@ "type": "Type" }, "empty": "Nothing h" + }, + "charts": { + "labels": { + "uptime": "Uptime", + "downtime": "Downtime", + "averageResponseTime": "Average response time", + "low": "Low", + "high": "High" + } } }, "submit": "Submit", @@ -167,6 +178,12 @@ "pause": "Pause", "resume": "Resume", "delete": "Delete" + }, + "statBoxes": { + "activeFor": "Active for", + "lastCheck": "Last check", + "lastResponseTime": "Last response time", + "certificateExpiry": "Certificate expiry" } } }, @@ -191,6 +208,14 @@ "placeholder": "Search monitors..." } } + }, + "checks": { + "table": { + "headers": { + "dateTime": "Date & time", + "statusCode": "Status code" + } + } } }, "distributedUptimeStatusCreateStatusPage": "status page", diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index b9cf2041f..0783c0040 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -483,6 +483,7 @@ class MongoChecksRepository implements IChecksRepository { }, }, { $sort: { _id: 1 } }, + { $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, ], groupedUpChecks: [ { $match: { status: true } }, @@ -496,6 +497,7 @@ class MongoChecksRepository implements IChecksRepository { }, }, { $sort: { _id: 1 } }, + { $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, ], groupedDownChecks: [ { $match: { status: false } }, @@ -509,6 +511,7 @@ class MongoChecksRepository implements IChecksRepository { }, }, { $sort: { _id: 1 } }, + { $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, ], }, }, diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 3195cc06a..ee40ff6dc 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -157,7 +157,7 @@ export interface HardwareChecksResult { } export interface GroupedCheck { - _id: string; + bucketDate: string; avgResponseTime: number; totalChecks: number; }