implemet stats
This commit is contained in:
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;
|
||||
@@ -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`;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user