Merge pull request #3186 from bluewave-labs/feat/v2-uptime-details
feat/v2 uptime details
This commit is contained in:
67 files changed
+1599
-1038
No files matched your search
+8
-1
@@ -37,7 +37,14 @@ function App() {
|
||||
<AppLayout>
|
||||
<Routes />
|
||||
</AppLayout>
|
||||
<ToastContainer />
|
||||
<ToastContainer
|
||||
newestOnTop={true}
|
||||
theme={mode}
|
||||
style={{
|
||||
"--toastify-color-progress-light": "#7C8BA1",
|
||||
"--toastify-color-progress-dark": "#7C8BA1",
|
||||
}}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
/**
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<any, Monitor>();
|
||||
|
||||
if (!monitor) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Stack
|
||||
spacing={{ xs: theme.spacing(8), md: 0 }}
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
alignItems={"center"}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<MonitorStatus monitor={monitor} />
|
||||
<Stack
|
||||
width={{ xs: "100%", md: "auto" }}
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isPosting}
|
||||
startIcon={<Icon icon={Mail} />}
|
||||
onClick={async () => {
|
||||
await post(`/notifications/test/all`, { monitorId: monitor.id });
|
||||
}}
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{t("common.buttons.testNotifications")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
startIcon={<Icon icon={Bug} />}
|
||||
onClick={() => {
|
||||
navigate(`/incidents/${monitor?.id}`);
|
||||
}}
|
||||
>
|
||||
{t("common.buttons.incidents")}
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isPosting}
|
||||
startIcon={monitor?.isActive ? <Icon icon={Pause} /> : <Icon icon={Play} />}
|
||||
onClick={async () => {
|
||||
await post(`/monitors/pause/${monitor.id}`, {});
|
||||
await refetch();
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive ? t("pause") : t("resume")}
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
startIcon={<Icon icon={Settings} />}
|
||||
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -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<HTMLElement>,
|
||||
newValue: string | null
|
||||
) => {
|
||||
if (newValue !== null) {
|
||||
setDateRange(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
let timeFramePicker = null;
|
||||
|
||||
if (hasDateRange) {
|
||||
timeFramePicker = (
|
||||
<ToggleButtonGroup
|
||||
value={dateRange}
|
||||
exclusive
|
||||
onChange={handleChange}
|
||||
size="small"
|
||||
>
|
||||
<ToggleButton
|
||||
disabled={isLoading}
|
||||
value="recent"
|
||||
>
|
||||
Recent
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
disabled={isLoading}
|
||||
value="day"
|
||||
>
|
||||
Day
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
disabled={isLoading}
|
||||
value="week"
|
||||
>
|
||||
Week
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
disabled={isLoading}
|
||||
value="month"
|
||||
>
|
||||
Month
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
Showing statistics for past{" "}
|
||||
{dateRange === "recent"
|
||||
? "2 hours"
|
||||
: dateRange === "day"
|
||||
? "24 hours"
|
||||
: dateRange === "week"
|
||||
? "7 days"
|
||||
: "30 days"}
|
||||
.
|
||||
</Typography>
|
||||
{timeFramePicker}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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<number | string>;
|
||||
}>;
|
||||
|
||||
export const BaseChart = ({
|
||||
children,
|
||||
icon,
|
||||
title,
|
||||
width = "100%",
|
||||
maxWidth = "100%",
|
||||
padding,
|
||||
}: BaseChartProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BaseBox
|
||||
sx={{
|
||||
padding: padding ?? theme.spacing(8),
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
width: width,
|
||||
maxWidth: maxWidth,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(8)}
|
||||
flex={1}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
{icon && (
|
||||
<BaseBox
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 34,
|
||||
height: 34,
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
"& svg": {
|
||||
width: 20,
|
||||
height: 20,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.secondary,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</BaseBox>
|
||||
)}
|
||||
<Typography variant="h2">{title}</Typography>
|
||||
</Stack>
|
||||
<Box flex={1}>{children}</Box>
|
||||
</Stack>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
@@ -55,7 +55,7 @@ export const BasePage = ({
|
||||
spacing={theme.spacing(10)}
|
||||
{...props}
|
||||
>
|
||||
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
|
||||
<Breadcrumb />
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<MuiBreadcrumbs
|
||||
@@ -38,80 +34,45 @@ export const Breadcrumb = ({ breadcrumbOverride }: { breadcrumbOverride?: string
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to="/uptime"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(2),
|
||||
textDecoration: "none",
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<Home
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
{hasDetailsPage ? (
|
||||
<Link
|
||||
to={`/${basePage}`}
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
color: theme.palette.text.secondary,
|
||||
"&:hover": {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{basePage.charAt(0).toUpperCase() + basePage.slice(1)}
|
||||
</Typography>
|
||||
</Link>
|
||||
) : (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
color: theme.palette.text.secondary,
|
||||
"&:hover": {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
fontWeight: 600,
|
||||
color: theme.palette.primary.main,
|
||||
}}
|
||||
>
|
||||
Home
|
||||
{basePage.charAt(0).toUpperCase() + basePage.slice(1)}
|
||||
</Typography>
|
||||
</Link>
|
||||
{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 ? (
|
||||
<Typography
|
||||
key={to}
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
fontWeight: 600,
|
||||
color: theme.palette.primary.main,
|
||||
}}
|
||||
>
|
||||
{displayName}
|
||||
</Typography>
|
||||
) : (
|
||||
<Link
|
||||
key={to}
|
||||
to={to}
|
||||
style={{
|
||||
textDecoration: "none",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
color: theme.palette.text.secondary,
|
||||
"&:hover": {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{displayName}
|
||||
</Typography>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
)}
|
||||
{hasDetailsPage && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
fontWeight: 600,
|
||||
color: theme.palette.primary.main,
|
||||
}}
|
||||
>
|
||||
Details
|
||||
</Typography>
|
||||
)}
|
||||
</MuiBreadcrumbs>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<span
|
||||
style={{
|
||||
content: '""',
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: color,
|
||||
opacity: 0.8,
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<Icon
|
||||
size={size}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
@@ -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 (
|
||||
<Stack>
|
||||
<Typography
|
||||
fontSize={typographyLevels.xl}
|
||||
fontWeight={500}
|
||||
color={theme.palette.primary.contrastText}
|
||||
overflow={"hidden"}
|
||||
textOverflow={"ellipsis"}
|
||||
whiteSpace={"nowrap"}
|
||||
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<PulseDot color={getStatusColor(monitor.status, theme)} />
|
||||
<Typography
|
||||
color={theme.palette.text.secondary}
|
||||
fontSize={typographyLevels.l}
|
||||
fontWeight={"bolder"}
|
||||
fontFamily={"monospace"}
|
||||
overflow={"hidden"}
|
||||
textOverflow={"ellipsis"}
|
||||
whiteSpace={"nowrap"}
|
||||
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
|
||||
>
|
||||
{formatUrl(monitor?.url)}
|
||||
</Typography>
|
||||
{!isSmall && (
|
||||
<>
|
||||
<Dot />
|
||||
<Typography>
|
||||
Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Box, Stack, useTheme } from "@mui/material";
|
||||
|
||||
interface PulseDotProps {
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const PulseDot = ({ color }: PulseDotProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
width="26px"
|
||||
height="24px"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Box
|
||||
minWidth="18px"
|
||||
minHeight="18px"
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: color,
|
||||
borderRadius: "50%",
|
||||
"&::before": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "inherit",
|
||||
borderRadius: "50%",
|
||||
animation: "ripple 1.8s ease-out infinite",
|
||||
},
|
||||
"&::after": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
width: "7px",
|
||||
height: "7px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<BaseBox
|
||||
sx={{
|
||||
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||
width: isSmall ? `100%` : `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
|
||||
background: bg,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<GradientBox
|
||||
palette={palette}
|
||||
sx={{
|
||||
...(sx as object),
|
||||
...(onClick ? { cursor: "pointer", "&:hover": { opacity: 0.95 } } : {}),
|
||||
}}
|
||||
>
|
||||
<Stack onClick={onClick}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: theme.spacing(2) }}>
|
||||
<Typography color={textColor}>{title}</Typography>
|
||||
{tooltip && (
|
||||
<TooltipWithInfo
|
||||
title={tooltip}
|
||||
iconColor={textColor as string}
|
||||
iconSize={14}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Typography color={textColor}>{subtitle}</Typography>
|
||||
{children}
|
||||
</Stack>
|
||||
</GradientBox>
|
||||
);
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<MuiTooltip
|
||||
title={props.title}
|
||||
placement={placement}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
background:
|
||||
theme.palette.mode === "dark"
|
||||
? "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)"
|
||||
: "linear-gradient(135deg, #2c3e50 0%, #1a252f 100%)",
|
||||
backgroundColor: "transparent",
|
||||
color: "#ffffff",
|
||||
fontSize: "13px",
|
||||
padding: `${theme.spacing(4)} ${theme.spacing(5)}`,
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.4)",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</MuiTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export interface TooltipWithInfoProps extends Omit<StyledTooltipProps, "children"> {
|
||||
iconSize?: number;
|
||||
iconColor?: string;
|
||||
}
|
||||
|
||||
export const TooltipWithInfo = ({
|
||||
iconSize = 14,
|
||||
iconColor,
|
||||
...props
|
||||
}: TooltipWithInfoProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const defaultColor = theme.palette.text.secondary;
|
||||
|
||||
return (
|
||||
<Tooltip {...props}>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
cursor: "help",
|
||||
}}
|
||||
>
|
||||
<Info
|
||||
size={iconSize}
|
||||
strokeWidth={1.5}
|
||||
style={{
|
||||
opacity: 0.7,
|
||||
color: iconColor || defaultColor,
|
||||
transition: "opacity 0.2s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.opacity = "1";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.opacity = "0.7";
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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 (
|
||||
<ToggleButton
|
||||
{...props}
|
||||
sx={{
|
||||
px: 8,
|
||||
textTransform: "none",
|
||||
height: 34,
|
||||
fontWeight: 400,
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
boxShadow: "none",
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToggleButtonGroupInput = ({ sx, ...props }: ToggleButtonGroupProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<ToggleButtonGroup
|
||||
{...props}
|
||||
sx={{
|
||||
"& .MuiToggleButtonGroup-grouped": {
|
||||
borderColor: theme.palette.divider,
|
||||
|
||||
borderRadius: 0,
|
||||
"&:first-of-type": {
|
||||
borderTopLeftRadius: 2,
|
||||
borderBottomLeftRadius: 2,
|
||||
},
|
||||
"&:last-of-type": {
|
||||
borderTopRightRadius: 2,
|
||||
borderBottomRightRadius: 2,
|
||||
},
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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 (
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<StatBox
|
||||
palette={palette}
|
||||
title={t("pages.common.monitors.statBoxes.activeFor")}
|
||||
subtitle={streakTime}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.common.monitors.statBoxes.lastCheck")}
|
||||
subtitle={lastCheckTime}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.common.monitors.statBoxes.lastResponseTime")}
|
||||
subtitle={monitorStats?.lastResponseTime + " ms"}
|
||||
/>
|
||||
|
||||
{monitor?.type === "http" && (
|
||||
<StatBox
|
||||
title={t("pages.common.monitors.statBoxes.certificateExpiry")}
|
||||
subtitle={certificateExpiry || "N/A"}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<Text
|
||||
x={x}
|
||||
y={y + 10}
|
||||
textAnchor="middle"
|
||||
fill={theme.palette.text.secondary}
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatDateWithTz(payload?.value, format, uiTimezone)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
|
||||
<Typography>{formatDateWithTz(String(label), format, uiTimezone)}</Typography>
|
||||
<Typography>Response time: {responseTime} ms</Typography>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
export const HistogramDetails = ({
|
||||
checks,
|
||||
range,
|
||||
}: {
|
||||
checks: GroupedCheck[];
|
||||
range: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||
return (
|
||||
<BaseChart
|
||||
icon={
|
||||
<Clock
|
||||
size={20}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
}
|
||||
title="Response times"
|
||||
>
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={300}
|
||||
>
|
||||
<AreaChart data={checks?.slice()}>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.divider}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="colorUv"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme.palette.primary.main}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme.palette.primary.light}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
dataKey="bucketDate"
|
||||
tick={(props) => (
|
||||
<XTick
|
||||
{...props}
|
||||
range={range}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
content={(props) => (
|
||||
<ResponseTimeToolTip
|
||||
{...props}
|
||||
range={range}
|
||||
theme={theme}
|
||||
uiTimezone={uiTimezone}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="avgResponseTime"
|
||||
stroke={theme.palette.primary.main}
|
||||
fill="url(#colorUv)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</BaseChart>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<>
|
||||
<text
|
||||
x={0}
|
||||
y="100%"
|
||||
dy={-3}
|
||||
textAnchor="start"
|
||||
fontSize={11}
|
||||
fill={theme.palette.text.secondary}
|
||||
>
|
||||
{formatDateWithTz(p1.bucketDate, dateFormat, uiTimezone)}
|
||||
</text>
|
||||
<text
|
||||
x="100%"
|
||||
y="100%"
|
||||
dy={-3}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
fill={theme.palette.text.secondary}
|
||||
>
|
||||
{formatDateWithTz(p2.bucketDate, dateFormat, uiTimezone)}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Stack
|
||||
sx={{
|
||||
p: 1,
|
||||
bgcolor: "background.paper",
|
||||
border: 1,
|
||||
borderColor: "divider",
|
||||
borderRadius: 1,
|
||||
}}
|
||||
>
|
||||
{dateLabel ? (
|
||||
<Typography
|
||||
sx={{ color: "text.secondary" }}
|
||||
variant="caption"
|
||||
>
|
||||
{dateLabel}
|
||||
</Typography>
|
||||
) : null}
|
||||
<Typography variant="caption">{titleText}</Typography>
|
||||
<Typography variant="body2">{Math.floor(avg)} ms</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const totalChecks = checks.reduce((acc, check) => acc + (check.totalChecks || 0), 0);
|
||||
|
||||
return (
|
||||
<BaseChart
|
||||
icon={icon}
|
||||
title={title}
|
||||
>
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Stack
|
||||
position="relative"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack>
|
||||
<Typography>Total checks: {totalChecks}</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={155}
|
||||
>
|
||||
<BarChart data={checks}>
|
||||
<XAxis
|
||||
stroke={theme.palette.divider}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<XLabel
|
||||
p1={checks[0]}
|
||||
p2={checks[checks.length - 1]}
|
||||
range={range}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={false}
|
||||
content={<CustomTooltip />}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="avgResponseTime"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{checks?.map((groupedCheck, index) => {
|
||||
const fillColor = getResponseTimeColor(groupedCheck.avgResponseTime);
|
||||
return (
|
||||
<Cell
|
||||
key={groupedCheck.bucketDate || `check-${index}`}
|
||||
fill={theme.palette[fillColor].main}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Stack>
|
||||
</BaseChart>
|
||||
);
|
||||
};
|
||||
@@ -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<string, string> = {
|
||||
success: "Excellent",
|
||||
warning: "Average",
|
||||
danger: "Poor",
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseChart
|
||||
icon={
|
||||
<Gauge
|
||||
size={20}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
}
|
||||
title={t("common.charts.labels.averageResponseTime")}
|
||||
>
|
||||
<Stack
|
||||
height="100%"
|
||||
position={"relative"}
|
||||
justifyContent={"flex-end"}
|
||||
>
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={155}
|
||||
>
|
||||
<RadialBarChart
|
||||
cy="100%"
|
||||
data={chartData}
|
||||
startAngle={180}
|
||||
endAngle={0}
|
||||
innerRadius={"120%"}
|
||||
outerRadius={"200%"}
|
||||
>
|
||||
<RadialBar
|
||||
dataKey="value"
|
||||
background={{ fill: theme.palette[palette].light }}
|
||||
>
|
||||
<Cell visibility={"hidden"} />
|
||||
<Cell fill={theme.palette[palette].main} />
|
||||
</RadialBar>
|
||||
</RadialBarChart>
|
||||
</ResponsiveContainer>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<Typography variant="body2">{t("common.charts.labels.low")}</Typography>
|
||||
<Typography variant="body2">{t("common.charts.labels.high")}</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
position="absolute"
|
||||
top={"50%"}
|
||||
right={"50%"}
|
||||
sx={{
|
||||
transform: "translate(50%, 0%)",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
textAlign={"center"}
|
||||
>
|
||||
{msg[palette]}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
textAlign={"center"}
|
||||
>{`${avg?.toFixed()}ms`}</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</BaseChart>
|
||||
);
|
||||
};
|
||||
@@ -1 +1,5 @@
|
||||
export * from "./ControlsFilter";
|
||||
export * from "./MonitorStatBoxes";
|
||||
export * from "./charts/HistogramStatus";
|
||||
export * from "./charts/RadialAvgResponse";
|
||||
export * from "./charts/HistogramDetails";
|
||||
@@ -1,4 +1,5 @@
|
||||
import { toast, type ToastOptions } from "react-toastify";
|
||||
|
||||
export const useToast = () => {
|
||||
const showToast = (message: string, options?: ToastOptions) => {
|
||||
toast.dismiss();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getHumanReadableDuration } from "@/Utils/timeUtils.js";
|
||||
import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js";
|
||||
const useGetIncidentsDuration = (incident, isActive) => {
|
||||
if (!incident?.startTime) {
|
||||
return "-";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 <SkeletonLayout />;
|
||||
}
|
||||
|
||||
const noIncidentsMessage = "Great. No Incidents, yet!";
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
flexWrap="wrap"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<ChartBox
|
||||
icon={
|
||||
<Icon
|
||||
name="Activity"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
header="Uptime"
|
||||
isEmpty={monitorData?.groupedUpChecks?.length === 0}
|
||||
>
|
||||
<Stack
|
||||
width={"100%"}
|
||||
justifyContent="space-between"
|
||||
direction="row"
|
||||
>
|
||||
<Box position="relative">
|
||||
<Typography>Total Checks</Typography>
|
||||
<Typography component="span">
|
||||
{hoveredUptimeData !== null
|
||||
? hoveredUptimeData.totalChecks
|
||||
: (monitorData?.groupedUpChecks?.reduce((count, checkGroup) => {
|
||||
return count + checkGroup.totalChecks;
|
||||
}, 0) ?? 0)}
|
||||
</Typography>
|
||||
{hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
|
||||
<Typography
|
||||
component="h5"
|
||||
position="absolute"
|
||||
top="100%"
|
||||
fontSize={11}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{formatDateWithTz(hoveredUptimeData._id, dateFormat, uiTimezone)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>
|
||||
{hoveredUptimeData !== null ? "Avg Response Time" : "Uptime Percentage"}
|
||||
</Typography>
|
||||
<Typography component="span">
|
||||
{hoveredUptimeData !== null
|
||||
? Math.floor(hoveredUptimeData?.originalAvgResponseTime ?? 0)
|
||||
: Math.floor((monitorData?.groupedUptimePercentage ?? 0) * 100)}
|
||||
<Typography component="span">
|
||||
{hoveredUptimeData !== null ? " ms" : " %"}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<UpBarChart
|
||||
groupedUpChecks={monitorData?.groupedUpChecks}
|
||||
type={dateRange}
|
||||
onBarHover={setHoveredUptimeData}
|
||||
/>
|
||||
</ChartBox>
|
||||
<ChartBox
|
||||
icon={
|
||||
<Icon
|
||||
name="AlertTriangle"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
header="Incidents"
|
||||
noDataMessage={noIncidentsMessage}
|
||||
isEmpty={monitorData?.groupedDownChecks?.length === 0}
|
||||
>
|
||||
<Stack width={"100%"}>
|
||||
<Box position="relative">
|
||||
<Typography component="span">
|
||||
{hoveredIncidentsData !== null
|
||||
? hoveredIncidentsData.totalChecks
|
||||
: (monitorData?.groupedDownChecks?.reduce((count, checkGroup) => {
|
||||
return count + checkGroup.totalChecks;
|
||||
}, 0) ?? 0)}
|
||||
</Typography>
|
||||
{hoveredIncidentsData !== null && hoveredIncidentsData.time !== null && (
|
||||
<Typography
|
||||
component="h5"
|
||||
position="absolute"
|
||||
top="100%"
|
||||
fontSize={11}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{formatDateWithTz(hoveredIncidentsData._id, dateFormat, uiTimezone)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
<DownBarChart
|
||||
groupedDownChecks={monitorData?.groupedDownChecks}
|
||||
type={dateRange}
|
||||
onBarHover={setHoveredIncidentsData}
|
||||
/>
|
||||
</ChartBox>
|
||||
<ChartBox
|
||||
icon={
|
||||
<Icon
|
||||
name="BarChart3"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
header="Average Response Time"
|
||||
>
|
||||
<ResponseGaugeChart avgResponseTime={monitorData?.groupedAvgResponseTime ?? 0} />
|
||||
</ChartBox>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Skeleton, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={300}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={300}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={300}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -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 (
|
||||
<>
|
||||
<text
|
||||
x={x}
|
||||
y={height}
|
||||
dy={-3}
|
||||
textAnchor="start"
|
||||
fontSize={11}
|
||||
>
|
||||
{formatDateWithTz(firstDataPoint._id, dateFormat, uiTimezone)}
|
||||
</text>
|
||||
<text
|
||||
x={width}
|
||||
y={height}
|
||||
dy={-3}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
>
|
||||
{formatDateWithTz(lastDataPoint._id, dateFormat, uiTimezone)}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -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 (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={250}
|
||||
height={155}
|
||||
>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={groupedDownChecks}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalChecks: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={groupedDownChecks?.[0] ?? {}}
|
||||
lastDataPoint={groupedDownChecks?.[groupedDownChecks?.length - 1] ?? {}}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="avgResponseTime"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{groupedDownChecks?.map((entry, index) => {
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={
|
||||
hoveredBarIndex === index
|
||||
? theme.palette.error.main
|
||||
: chartHovered
|
||||
? theme.palette.error.light // CAIO_REVIEW
|
||||
: theme.palette.error.main
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({ time: null, totalChecks: 0 });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
|
||||
DownBarChart.displayName = "DownBarChart";
|
||||
DownBarChart.propTypes = {
|
||||
monitor: PropTypes.shape({
|
||||
groupedDownChecks: PropTypes.arrayOf(PropTypes.object),
|
||||
}),
|
||||
type: PropTypes.string,
|
||||
onBarHover: PropTypes.func,
|
||||
};
|
||||
export default DownBarChart;
|
||||
@@ -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 (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={210}
|
||||
height={155}
|
||||
>
|
||||
<RadialBarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
cy="89%"
|
||||
data={data}
|
||||
startAngle={180}
|
||||
endAngle={0}
|
||||
innerRadius={100}
|
||||
outerRadius={150}
|
||||
>
|
||||
<text
|
||||
x={0}
|
||||
y="100%"
|
||||
dx="5%"
|
||||
dy={-2}
|
||||
textAnchor="start"
|
||||
fontSize={11}
|
||||
>
|
||||
{t("low")}
|
||||
</text>
|
||||
<text
|
||||
x="100%"
|
||||
y="100%"
|
||||
dx="-3%"
|
||||
dy={-2}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
>
|
||||
{t("high")}
|
||||
</text>
|
||||
<text
|
||||
x="50%"
|
||||
y="45%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fontSize={18}
|
||||
fontWeight={400}
|
||||
>
|
||||
{responseProps.category}
|
||||
</text>
|
||||
<text
|
||||
x="50%"
|
||||
y="70%"
|
||||
textAnchor="middle"
|
||||
alignmentBaseline="hanging"
|
||||
fontSize={25}
|
||||
>
|
||||
<tspan fontWeight={600}>{responseTime}</tspan> <tspan opacity={0.8}>ms</tspan>
|
||||
</text>
|
||||
<RadialBar
|
||||
background={{ fill: responseProps.bg }}
|
||||
clockWise
|
||||
dataKey="response"
|
||||
stroke="none"
|
||||
>
|
||||
<Cell
|
||||
fill="transparent"
|
||||
background={false}
|
||||
barSize={0}
|
||||
/>
|
||||
<Cell fill={responseProps.main} />
|
||||
</RadialBar>
|
||||
</RadialBarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
ResponseGaugeChart.propTypes = {
|
||||
avgResponseTime: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default ResponseGaugeChart;
|
||||
@@ -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 <SkeletonLayout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartBox
|
||||
icon={
|
||||
<Icon
|
||||
name="TrendingUp"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
header="Response Times"
|
||||
>
|
||||
<MonitorDetailsAreaChart
|
||||
checks={groupedChecks}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
</ChartBox>
|
||||
);
|
||||
};
|
||||
|
||||
ResponseTImeChart.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
groupedChecks: PropTypes.array,
|
||||
dateRange: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ResponseTImeChart;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Skeleton } from "@mui/material";
|
||||
const ResponseTimeChartSkeleton = () => {
|
||||
return (
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={300}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponseTimeChartSkeleton;
|
||||
@@ -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 (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={210}
|
||||
height={155}
|
||||
>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={groupedUpChecks}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalChecks: 0, originalAvgResponseTime: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={groupedUpChecks?.[0]}
|
||||
lastDataPoint={groupedUpChecks?.[groupedUpChecks?.length - 1]}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="avgResponseTime"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{groupedUpChecks?.map((entry, index) => {
|
||||
const themeColor = getThemeColor(entry.originalAvgResponseTime);
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={
|
||||
hoveredBarIndex === index
|
||||
? theme.palette[themeColor].main
|
||||
: chartHovered
|
||||
? theme.palette[themeColor].light
|
||||
: theme.palette[themeColor].main
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({
|
||||
time: null,
|
||||
totalChecks: 0,
|
||||
groupUptimePercentage: 0,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
|
||||
// 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;
|
||||
@@ -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<Check>[] = [
|
||||
{
|
||||
id: "status",
|
||||
content: t("common.table.headers.status"),
|
||||
render: (row) => {
|
||||
return <StatusLabel status={row.status} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
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<HTMLButtonElement> | null,
|
||||
newPage: number
|
||||
) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleRowsPerPageChange = (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
) => {
|
||||
const value = Number(e.target.value);
|
||||
setPage(0);
|
||||
setRowsPerPage(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Table
|
||||
headers={headers}
|
||||
data={checks}
|
||||
onRowClick={(row) => {
|
||||
navigate(`/checks/${row.id}`);
|
||||
}}
|
||||
/>
|
||||
<Pagination
|
||||
component="div"
|
||||
count={count}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onPageChange={handlePageChange}
|
||||
onRowsPerPageChange={handleRowsPerPageChange}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<Stack
|
||||
sx={{
|
||||
py: theme.spacing(2),
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
marginBottom: theme.spacing(1),
|
||||
color: theme.palette.primary.contrastText,
|
||||
}}
|
||||
>
|
||||
{t("responseTimeBreakdown")}
|
||||
</Typography>
|
||||
{timingDetails.map((detail, index, array) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing(4),
|
||||
marginBottom: index < array.length - 1 ? theme.spacing(0.5) : 0,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: theme.palette.primary.contrastText }}
|
||||
>
|
||||
{detail.label}:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: index === array.length - 1 ? 600 : 400,
|
||||
color: theme.palette.primary.contrastText,
|
||||
}}
|
||||
>
|
||||
{Math.round(detail.value)} ms
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const ResponseTable = ({
|
||||
isLoading = false,
|
||||
checks = [],
|
||||
checksCount,
|
||||
uiTimezone,
|
||||
page,
|
||||
setPage,
|
||||
rowsPerPage,
|
||||
setRowsPerPage,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
if (isLoading) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "status",
|
||||
content: t("status"),
|
||||
render: (row) => {
|
||||
const status = row.status === true ? "up" : "down";
|
||||
|
||||
return (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<Tooltip
|
||||
title={GetTooltip(row.timings, theme, t)}
|
||||
placement="top"
|
||||
arrow
|
||||
enterDelay={300}
|
||||
enterNextDelay={300}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
backgroundColor: lighten(theme.palette.primary.main, 0.1),
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
"& .MuiTooltip-arrow": {
|
||||
color: lighten(theme.palette.primary.main, 0.1),
|
||||
"&::before": {
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
cursor: "help",
|
||||
display: "inline-block",
|
||||
}}
|
||||
>
|
||||
{responseTimeDisplay}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ChartBox
|
||||
icon={
|
||||
<Icon
|
||||
name="History"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
header="Response Times"
|
||||
height="100%"
|
||||
>
|
||||
<Table
|
||||
headers={headers}
|
||||
data={checks}
|
||||
/>
|
||||
<TablePagination
|
||||
page={page}
|
||||
handleChangePage={setPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangeRowsPerPage={setRowsPerPage}
|
||||
itemCount={checksCount}
|
||||
/>
|
||||
</ChartBox>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Skeleton } from "@mui/material";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
return (
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={300}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -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 (
|
||||
<StatusBoxes shouldRender={!isLoading}>
|
||||
<StatBox
|
||||
gradient={true}
|
||||
status={determineState(monitor)}
|
||||
heading={"active for"}
|
||||
subHeading={streakTime}
|
||||
/>
|
||||
<StatBox
|
||||
heading="last check"
|
||||
subHeading={
|
||||
<>
|
||||
{lastCheckTime}
|
||||
<Typography component="span">{"ago"}</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StatBox
|
||||
heading="last response time"
|
||||
subHeading={
|
||||
<>
|
||||
{monitorStats?.lastResponseTime}
|
||||
<Typography component="span">{"ms"}</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{monitor?.type === "http" && (
|
||||
<StatBox
|
||||
heading="certificate expiry"
|
||||
subHeading={
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize={13}
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{certificateExpiry}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StatusBoxes>
|
||||
);
|
||||
};
|
||||
|
||||
UptimeStatusBoxes.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
monitor: PropTypes.object,
|
||||
monitorStats: PropTypes.object,
|
||||
certificateExpiry: PropTypes.string,
|
||||
};
|
||||
|
||||
export default UptimeStatusBoxes;
|
||||
@@ -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,
|
||||
|
||||
@@ -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<number>(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState<number>(5);
|
||||
const [dateRange, setDateRange] = useState<string>("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<MonitorDetailsResponse>(
|
||||
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<CertificateResponse>(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<ChecksResponse>(
|
||||
checksUrl,
|
||||
{},
|
||||
{ keepPreviousData: true }
|
||||
);
|
||||
|
||||
const checks = checksData?.checks ?? [];
|
||||
const checksCount = checksData?.checksCount ?? 0;
|
||||
|
||||
return (
|
||||
<BasePage>
|
||||
<HeaderMonitorControls
|
||||
path="uptime"
|
||||
monitor={monitor}
|
||||
isAdmin={isAdmin}
|
||||
refetch={refetchMonitor}
|
||||
/>
|
||||
<MonitorStatBoxes
|
||||
monitor={monitor}
|
||||
monitorStats={monitorStats}
|
||||
certificateExpiry={certificateExpiry}
|
||||
/>
|
||||
<HeaderTimeRange
|
||||
isLoading={monitorIsLoading || checksIsLoading}
|
||||
hasDateRange={true}
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<HistogramStatus
|
||||
title={t("common.charts.labels.uptime")}
|
||||
icon={<TrendingUp />}
|
||||
checks={monitorData?.groupedUpChecks ?? []}
|
||||
range={dateRange}
|
||||
/>
|
||||
<HistogramStatus
|
||||
title={t("common.charts.labels.downtime")}
|
||||
icon={<AlertTriangle />}
|
||||
checks={monitorData?.groupedDownChecks ?? []}
|
||||
range={dateRange}
|
||||
/>
|
||||
<RadialAvgResponse
|
||||
avg={monitorStats?.avgResponseTime || 0}
|
||||
max={500}
|
||||
/>
|
||||
</Stack>
|
||||
<HistogramDetails
|
||||
checks={monitorData?.groupedChecks || []}
|
||||
range={dateRange}
|
||||
/>
|
||||
<ChecksTable
|
||||
checks={checks}
|
||||
count={checksCount}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
setRowsPerPage={setRowsPerPage}
|
||||
/>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export default UptimeDetailsPage;
|
||||
File renamed without changes.
@@ -88,7 +88,11 @@ const UptimeMonitorsPage = () => {
|
||||
data: monitors,
|
||||
isLoading: monitorsLoading,
|
||||
error,
|
||||
} = useGet<Monitor[]>("/monitors/team?type=http&type=ping&type=port&type=docker");
|
||||
} = useGet<Monitor[]>(
|
||||
"/monitors/team?type=http&type=ping&type=port&type=docker",
|
||||
{},
|
||||
{ keepPreviousData: true }
|
||||
);
|
||||
|
||||
const {
|
||||
data: monitorsWithChecksData,
|
||||
@@ -98,7 +102,7 @@ const UptimeMonitorsPage = () => {
|
||||
} = useGet<MonitorsWithChecksResponse>(
|
||||
monitorsWithChecksUrl,
|
||||
{},
|
||||
{ refreshInterval: 5000 }
|
||||
{ refreshInterval: 5000, keepPreviousData: true }
|
||||
);
|
||||
|
||||
const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {};
|
||||
|
||||
@@ -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 = () => {
|
||||
/>
|
||||
<Route
|
||||
path="/uptime/:monitorId/"
|
||||
element={<UptimeDetails />}
|
||||
element={
|
||||
<>
|
||||
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
|
||||
<UptimeDetails />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/uptime/configure/:monitorId/"
|
||||
|
||||
@@ -128,7 +128,7 @@ export interface Check {
|
||||
}
|
||||
|
||||
export interface GroupedCheck {
|
||||
_id: string;
|
||||
bucketDate: string;
|
||||
avgResponseTime: number;
|
||||
totalChecks: number;
|
||||
}
|
||||
@@ -139,3 +139,8 @@ export interface LatestCheck {
|
||||
checkedAt: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ChecksResponse {
|
||||
checks: Check[];
|
||||
checksCount: number;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Check } from "@/Types/Check";
|
||||
import type { Check, GroupedCheck } from "@/Types/Check";
|
||||
export type MonitorStatus = boolean | undefined;
|
||||
|
||||
export const MonitorTypes = [
|
||||
@@ -47,3 +47,32 @@ export interface MonitorsWithChecksResponse {
|
||||
monitors: MonitorWithChecks[];
|
||||
summary: MonitorsSummary;
|
||||
}
|
||||
|
||||
export interface MonitorStats {
|
||||
id: string;
|
||||
monitorId: string;
|
||||
avgResponseTime: number;
|
||||
totalChecks: number;
|
||||
totalUpChecks: number;
|
||||
totalDownChecks: number;
|
||||
uptimePercentage: number;
|
||||
lastCheckTimestamp: number;
|
||||
lastResponseTime: number;
|
||||
timeOfLastFailure?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface MonitorData {
|
||||
monitor: Monitor;
|
||||
groupedChecks: GroupedCheck[];
|
||||
groupedUpChecks: GroupedCheck[];
|
||||
groupedDownChecks: GroupedCheck[];
|
||||
groupedAvgResponseTime: number;
|
||||
groupedUptimePercentage: number;
|
||||
}
|
||||
|
||||
export interface MonitorDetailsResponse {
|
||||
monitorData: MonitorData;
|
||||
monitorStats: MonitorStats | null;
|
||||
}
|
||||
@@ -25,10 +25,10 @@ export const formatDateWithTz = (timestamp: string, format: string, timezone: st
|
||||
|
||||
export const tickDateFormatLookup = (range: string) => {
|
||||
const tickFormatLookup: Record<string, string> = {
|
||||
"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<string, string> = {
|
||||
"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) {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 } },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -157,7 +157,7 @@ export interface HardwareChecksResult {
|
||||
}
|
||||
|
||||
export interface GroupedCheck {
|
||||
_id: string;
|
||||
bucketDate: string;
|
||||
avgResponseTime: number;
|
||||
totalChecks: number;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user