implemet stats

This commit is contained in:
Alex Holliday committed 2025-07-11 11:23:50 -07:00
1 parent b1b4fd3f64
commit 5b395b96cd
11 files changed
+294 -77

No files matched your search

@@ -1,8 +1,10 @@
import { useTheme } from "@emotion/react";
import { useEffect, useState, useMemo } from "react";
import Stack from "@mui/material/Stack";
import { Box, Typography } from "@mui/material";
import PropTypes from "prop-types";
import "./index.css";
import CircularProgress from "@mui/material/CircularProgress";
const MINIMUM_VALUE = 0;
const MAXIMUM_VALUE = 100;
@@ -16,6 +18,8 @@ const MAXIMUM_VALUE = 100;
* @param {number} [props.radius=60] - Radius of the gauge circle
* @param {number} [props.strokeWidth=15] - Width of the gauge stroke
* @param {number} [props.threshold=50] - Threshold for color change
* @param {number} [props.precision=1] - Precision of the progress percentage
* @param {string} [props.unit="%"] - Unit of progress
*
* @example
* <CustomGauge
@@ -27,7 +31,15 @@ const MAXIMUM_VALUE = 100;
*
* @returns {React.ReactElement} Rendered CustomGauge component
*/
const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = 50 }) => {
const CustomGauge = ({
isLoading = false,
progress = 0,
radius = 70,
strokeWidth = 15,
threshold = 50,
precision = 1,
unit = "%",
}) => {
const theme = useTheme();
// Calculate the length of the stroke for the circle
const { circumference, totalSize, strokeLength } = useMemo(
@@ -57,6 +69,20 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
? theme.palette.error.lowContrast
: theme.palette.accent.main;
if (isLoading) {
return (
<Stack
className="radial-chart"
width={radius}
height={radius}
alignItems="center"
justifyContent="center"
>
<CircularProgress color="accent" />
</Stack>
);
}
return (
<Box
className="radial-chart"
@@ -71,7 +97,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
>
<circle
className="radial-chart-base"
stroke={theme.palette.secondary.light} // CAIO_REVIEW
stroke={theme.palette.secondary.light}
strokeWidth={strokeWidth}
fill="none"
cx={totalSize / 2} // Center the circle
@@ -103,7 +129,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
fill: theme.typography.h2.color,
}}
>
{`${progressWithinRange.toFixed(1)}%`}
{`${progressWithinRange.toFixed(precision)}${unit}`}
</Typography>
</Box>
);
@@ -112,8 +138,11 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
export default CustomGauge;
CustomGauge.propTypes = {
isLoading: PropTypes.bool,
progress: PropTypes.number,
radius: PropTypes.number,
strokeWidth: PropTypes.number,
threshold: PropTypes.number,
precision: PropTypes.number,
unit: PropTypes.string,
};
@@ -1,12 +1,12 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Gauge from "../../../../../Components/Charts/CustomGauge";
import Typography from "@mui/material/Typography";
// Utils
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import { getPercentage } from "./utils";
import { getPercentage } from "../../utils/utils";
import { useTranslation } from "react-i18next";
const GaugeBox = ({ title, subtitle, children }) => {
const theme = useTheme();
@@ -14,6 +14,8 @@ const GaugeBox = ({ title, subtitle, children }) => {
<Stack
alignItems="center"
p={theme.spacing(2)}
maxWidth={150}
width={150}
>
{children}
@@ -23,52 +25,85 @@ const GaugeBox = ({ title, subtitle, children }) => {
);
};
const Gauges = ({ diagnostics }) => {
GaugeBox.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};
const Gauges = ({ diagnostics, isLoading }) => {
const heapTotalSize = getPercentage(
diagnostics?.v8HeapStats?.totalHeapSizeMb,
diagnostics?.v8HeapStats?.heapSizeLimitMb
diagnostics?.v8HeapStats?.totalHeapSizeBytes,
diagnostics?.v8HeapStats?.heapSizeLimitBytes
);
const heapUsedSize = getPercentage(
diagnostics?.v8HeapStats?.usedHeapSizeMb,
diagnostics?.v8HeapStats?.heapSizeLimitMb
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
diagnostics?.v8HeapStats?.heapSizeLimitBytes
);
const actualHeapUsed = getPercentage(
diagnostics?.v8HeapStats?.usedHeapSizeMb,
diagnostics?.v8HeapStats?.totalHeapSizeMb
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
diagnostics?.v8HeapStats?.totalHeapSizeBytes
);
const theme = useTheme();
const { t } = useTranslation();
return (
<Stack
direction="row"
spacing={theme.spacing(4)}
flexWrap="wrap"
>
<GaugeBox
title="Heap Allocation"
subtitle="% of available memory"
title={t("diagnosticsPage.gauges.heapAllocationTitle")}
subtitle={t("diagnosticsPage.gauges.heapAllocationSubtitle")}
>
<Gauge progress={heapTotalSize} />
<Gauge
isLoading={isLoading}
radius={100}
progress={heapTotalSize}
/>
</GaugeBox>
<GaugeBox
title="Heap Usage"
subtitle="% of available memory"
title={t("diagnosticsPage.gauges.heapUsageTitle")}
subtitle={t("diagnosticsPage.gauges.heapUsageSubtitle")}
>
<Gauge progress={heapUsedSize} />
<Gauge
isLoading={isLoading}
radius={100}
progress={heapUsedSize}
/>
</GaugeBox>
<GaugeBox
title="Heap Utilization"
subtitle="% of Allocated"
title={t("diagnosticsPage.gauges.heapUtilizationTitle")}
subtitle={t("diagnosticsPage.gauges.heapUtilizationSubtitle")}
>
<Gauge progress={actualHeapUsed} />
<Gauge
isLoading={isLoading}
radius={100}
progress={actualHeapUsed}
/>
</GaugeBox>
<GaugeBox
title={t("diagnosticsPage.gauges.instantCpuUsageTitle")}
subtitle={t("diagnosticsPage.gauges.instantCpuUsageSubtitle")}
>
<Gauge
isLoading={isLoading}
radius={100}
progress={diagnostics?.cpuUsage?.usagePercentage}
precision={2}
/>
</GaugeBox>
</Stack>
);
};
Gauges.propTypes = {
diagnostics: PropTypes.object.isRequired,
diagnostics: PropTypes.object,
isLoading: PropTypes.bool,
};
export default Gauges;
@@ -1,4 +0,0 @@
export const getPercentage = (value, total) => {
if (!value || !total) return 0;
return (value / total) * 100;
};
@@ -0,0 +1,97 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import CircularProgress from "@mui/material/CircularProgress";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
import { formatBytes } from "../../utils/utils";
import { useTranslation } from "react-i18next";
const StatsCard = ({ title, value, unit = "", isLoading }) => {
const theme = useTheme();
return (
<Card sx={{ width: 150, maxWidth: 150, height: 80, maxHeight: 80 }}>
{isLoading ? (
<Stack
alignItems="center"
justifyContent="center"
height={80}
maxHeight={80}
>
<CircularProgress color="accent" />
</Stack>
) : (
<CardContent>
<Typography
variant="body1"
color={theme.palette.primary.contrastText}
>
{title}
</Typography>
<Typography variant="body1">
{value} {unit}
</Typography>
</CardContent>
)}
</Card>
);
};
StatsCard.propTypes = {
title: PropTypes.string,
value: PropTypes.string,
unit: PropTypes.string,
isLoading: PropTypes.bool,
};
const Stats = ({ diagnostics, isLoading }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Stack
direction="row"
gap={theme.spacing(4)}
flexWrap="wrap"
>
<StatsCard
title={t("diagnosticsPage.stats.eventLoopDelayTitle")}
value={getHumanReadableDuration(diagnostics?.eventLoopDelayMs)}
isLoading={isLoading}
/>
<StatsCard
title={t("diagnosticsPage.stats.uptimeTitle")}
value={getHumanReadableDuration(diagnostics?.uptimeMs)}
isLoading={isLoading}
/>
<StatsCard
title={t("diagnosticsPage.stats.usedHeapSizeTitle")}
value={formatBytes(diagnostics?.v8HeapStats?.usedHeapSizeBytes)}
isLoading={isLoading}
/>
<StatsCard
title={t("diagnosticsPage.stats.totalHeapSizeTitle")}
value={formatBytes(diagnostics?.v8HeapStats?.totalHeapSizeBytes)}
isLoading={isLoading}
/>
<StatsCard
title={t("diagnosticsPage.stats.osMemoryLimitTitle")}
value={formatBytes(diagnostics?.osStats?.totalMemoryBytes)}
isLoading={isLoading}
/>
</Stack>
);
};
Stats.propTypes = {
diagnostics: PropTypes.object,
isLoading: PropTypes.bool,
};
export default Stats;
+17 -3
View File
@@ -2,6 +2,8 @@ import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Gauges from "./components/gauges";
import Stats from "./components/stats";
import Divider from "@mui/material/Divider";
import { useTheme } from "@emotion/react";
import { useState } from "react";
@@ -15,14 +17,26 @@ const Diagnostics = () => {
const theme = useTheme();
const { t } = useTranslation();
const [diagnostics, isLoading, error] = useFetchDiagnostics();
console.log(diagnostics);
// Setup
return (
<Stack gap={theme.spacing(4)}>
<Box>
<Typography variant="h2">{t("diagnosticsPage.description")}</Typography>
<Typography variant="h2">{t("diagnosticsPage.diagnosticDescription")}</Typography>
</Box>
<Gauges diagnostics={diagnostics} />
<Divider color={theme.palette.accent.main} />
<Stack
gap={theme.spacing(20)}
mt={theme.spacing(10)}
>
<Gauges
diagnostics={diagnostics}
isLoading={isLoading}
/>
<Stats
diagnostics={diagnostics}
isLoading={isLoading}
/>
</Stack>
</Stack>
);
};
@@ -0,0 +1,16 @@
export const getPercentage = (value, total) => {
if (!value || !total) return 0;
return (value / total) * 100;
};
export const formatBytes = (bytes) => {
if (!bytes) return "N/A";
if (bytes === 0) return "0 Bytes";
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
};
+4
View File
@@ -2,6 +2,7 @@ import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Select from "../../../Components/Inputs/Select";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
import { useFetchLogs } from "../../../Hooks/logHooks";
import { useTheme } from "@emotion/react";
@@ -53,10 +54,13 @@ const Logs = () => {
<Box>
<Typography variant="h2">{t("logsPage.description")}</Typography>
</Box>
<Divider color={theme.palette.accent.main} />
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
mt={theme.spacing(10)}
>
<Typography>{t("logsPage.logLevelSelect.title")}</Typography>
<Select
@@ -1,8 +1,6 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import StatBox from "../../../../../Components/StatBox";
import StatusBoxes from "../../../../../Components/StatusBoxes";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
@@ -24,20 +22,17 @@ const Metrics = ({ metrics = {} }) => {
});
return (
<Stack gap={theme.spacing(2)}>
<Typography variant="h2">{t("queuePage.metricsTable.title")}</Typography>
<StatusBoxes flexWrap="wrap">
{data.map((metric) => {
return (
<StatBox
key={metric.key}
heading={metric.title}
subHeading={metric.value}
/>
);
})}
</StatusBoxes>
</Stack>
<StatusBoxes flexWrap="wrap">
{data.map((metric) => {
return (
<StatBox
key={metric.key}
heading={metric.title}
subHeading={metric.value}
/>
);
})}
</StatusBoxes>
);
};
export default Metrics;
+39 -30
View File
@@ -5,6 +5,8 @@ import Metrics from "./components/Metrics";
import FailedJobTable from "./components/FailedJobTable";
import ButtonGroup from "@mui/material/ButtonGroup";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
// Utils
import { useState } from "react";
@@ -26,39 +28,46 @@ const QueueDetails = () => {
if (error || flushError) return <div>Error: {error.message}</div>;
return (
<Stack gap={theme.spacing(20)}>
<Metrics metrics={metrics} />
<JobTable jobs={jobs} />
<FailedJobTable metrics={metrics} />
<ButtonGroup
variant="contained"
color="accent"
sx={{
position: "sticky",
bottom: 0,
zIndex: 1000,
backgroundColor: theme.palette.primary.main,
p: theme.spacing(4),
border: `1px solid ${theme.palette.primary.lowContrast}`,
borderRadius: theme.spacing(2),
}}
<Stack gap={theme.spacing(4)}>
<Typography variant="h2">{t("queuePage.metricsTable.title")}</Typography>
<Divider color={theme.palette.accent.main} />
<Stack
gap={theme.spacing(20)}
mt={theme.spacing(10)}
>
<Button
onClick={() => {
setTrigger(!trigger);
<Metrics metrics={metrics} />
<JobTable jobs={jobs} />
<FailedJobTable metrics={metrics} />
<ButtonGroup
variant="contained"
color="accent"
sx={{
position: "sticky",
bottom: 0,
zIndex: 1000,
backgroundColor: theme.palette.primary.main,
p: theme.spacing(4),
border: `1px solid ${theme.palette.primary.lowContrast}`,
borderRadius: theme.spacing(2),
}}
loading={isLoading}
>
{t("queuePage.refreshButton")}
</Button>
<Button
onClick={() => flushQueue(trigger, setTrigger)}
loading={isFlushing}
>
{t("queuePage.flushButton")}
</Button>
</ButtonGroup>
<Button
onClick={() => {
setTrigger(!trigger);
}}
loading={isLoading}
>
{t("queuePage.refreshButton")}
</Button>
<Button
onClick={() => flushQueue(trigger, setTrigger)}
loading={isFlushing}
>
{t("queuePage.flushButton")}
</Button>
</ButtonGroup>
</Stack>
</Stack>
);
};
+4
View File
@@ -85,6 +85,7 @@ export const getHumanReadableDuration = (ms) => {
hours: durationObj.hours(),
minutes: durationObj.minutes(),
seconds: durationObj.seconds(),
milliseconds: durationObj.milliseconds(),
};
const result = [];
@@ -101,6 +102,9 @@ export const getHumanReadableDuration = (ms) => {
if (result.length < 2 && parts.seconds > 0) {
result.push(`${parts.seconds}s`);
}
if (result.length < 2 && parts.milliseconds > 0 && parts.seconds < 1) {
result.push(`${parts.milliseconds.toFixed(2)}ms`);
}
if (result.length === 0) {
// fallback for durations < 1s
+20 -2
View File
@@ -438,7 +438,7 @@
"inviteNoTokenFound": "No invite token found",
"loginHere": "Login here",
"logsPage": {
"description": "This page shows the latest 1000 lines of logs from the Checkmate server",
"description": "System logs - last 1000 lines",
"logLevelSelect": {
"title": "Log level",
"values": {
@@ -460,7 +460,25 @@
}
},
"diagnosticsPage": {
"description": "System diagnostics"
"diagnosticDescription": "System diagnostics",
"statsDescription": "System statistics",
"gauges": {
"heapAllocationTitle": "Heap allocation",
"heapAllocationSubtitle": "% of available memory",
"heapUsageTitle": "Heap usage",
"heapUsageSubtitle": "% of available memory",
"heapUtilizationTitle": "Heap utilization",
"heapUtilizationSubtitle": "% of allocated",
"instantCpuUsageTitle": "Instant CPU usage",
"instantCpuUsageSubtitle": "% of 1s used by CPU"
},
"stats": {
"eventLoopDelayTitle": "Event loop delay",
"uptimeTitle": "Uptime",
"usedHeapSizeTitle": "Used heap size",
"totalHeapSizeTitle": "Total heap size",
"osMemoryLimitTitle": "OS Memory Limit"
}
},
"low": "low",
"maintenance": "maintenance",