34 files changed
+894
-1021
No files matched your search
Generated
+13
-1
@@ -26,6 +26,7 @@
|
||||
"joi": "17.13.3",
|
||||
"lucide-react": "^0.562.0",
|
||||
"mui-color-input": "^7.0.0",
|
||||
"pretty-bytes": "^7.1.0",
|
||||
"pretty-ms": "^9.3.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -5265,6 +5266,18 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-bytes": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.1.0.tgz",
|
||||
"integrity": "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-ms": {
|
||||
"version": "9.3.0",
|
||||
"license": "MIT",
|
||||
@@ -6737,7 +6750,6 @@
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"joi": "17.13.3",
|
||||
"lucide-react": "^0.562.0",
|
||||
"mui-color-input": "^7.0.0",
|
||||
"pretty-bytes": "^7.1.0",
|
||||
"pretty-ms": "^9.3.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { BaseChart } from "@/Components/v2/design-elements";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
@@ -104,3 +106,50 @@ export const Gauge = ({
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const DetailGauge = ({
|
||||
title,
|
||||
progress,
|
||||
upperLabel,
|
||||
upperValue,
|
||||
lowerLabel,
|
||||
lowerValue,
|
||||
}: {
|
||||
title: string;
|
||||
progress: number;
|
||||
upperLabel?: string;
|
||||
upperValue?: string | number;
|
||||
lowerLabel?: string;
|
||||
lowerValue?: string | number;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BaseChart
|
||||
icon={null}
|
||||
title={title}
|
||||
maxWidth={225}
|
||||
>
|
||||
<Stack
|
||||
alignItems={"center"}
|
||||
mb={theme.spacing(4)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Gauge progress={progress} />
|
||||
</Stack>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<Typography>{upperLabel}</Typography>
|
||||
<Typography>{upperValue}</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<Typography>{lowerLabel}</Typography>
|
||||
<Typography>{lowerValue}</Typography>
|
||||
</Stack>
|
||||
</BaseChart>
|
||||
);
|
||||
};
|
||||
@@ -29,6 +29,7 @@ import type { TablePaginationOwnProps } from "@mui/material/TablePagination";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState, Fragment, useEffect } from "react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { SxProps, Theme } from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
|
||||
export type Header<T> = {
|
||||
@@ -47,6 +48,7 @@ type DataTableProps<T extends { id?: string | number; _id?: string | number }> =
|
||||
renderExpandedContent?: (row: T) => React.ReactNode;
|
||||
emptyViewText?: string;
|
||||
emptyViewPositive?: boolean;
|
||||
getRowSx?: (row: T) => SxProps<Theme>;
|
||||
};
|
||||
|
||||
export function DataTable<
|
||||
@@ -64,6 +66,7 @@ export function DataTable<
|
||||
renderExpandedContent,
|
||||
emptyViewText,
|
||||
emptyViewPositive,
|
||||
getRowSx,
|
||||
}: DataTableProps<T>) {
|
||||
const theme = useTheme();
|
||||
const [expanded, setExpanded] = useState<(string | number) | null>(null);
|
||||
@@ -204,6 +207,7 @@ export function DataTable<
|
||||
<TableRow
|
||||
sx={{
|
||||
cursor: onRowClick ? "pointer" : "default",
|
||||
...(getRowSx?.(row) as object),
|
||||
}}
|
||||
onClick={() => {
|
||||
if (expandableRows) handleExpand(row);
|
||||
|
||||
@@ -19,6 +19,9 @@ const initialState = {
|
||||
infrastructure: {
|
||||
rowsPerPage: 5,
|
||||
},
|
||||
logs: {
|
||||
rowsPerPage: 15,
|
||||
},
|
||||
sidebar: {
|
||||
collapsed: false,
|
||||
},
|
||||
@@ -41,9 +44,10 @@ const uiSlice = createSlice({
|
||||
},
|
||||
setRowsPerPage: (state, action) => {
|
||||
const { table, value } = action.payload;
|
||||
if (state[table]) {
|
||||
state[table].rowsPerPage = value;
|
||||
if (!state[table]) {
|
||||
state[table] = {};
|
||||
}
|
||||
state[table].rowsPerPage = value;
|
||||
},
|
||||
toggleSidebar: (state) => {
|
||||
state.sidebar.collapsed = !state.sidebar.collapsed;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { BaseChart, Gauge } from "@/Components/v2/design-elements";
|
||||
import { DetailGauge } from "@/Components/v2/design-elements";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getGbs, getFrequency } from "@/Utils/InfraUtils";
|
||||
@@ -8,53 +7,6 @@ import { useTheme } from "@mui/material";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import type { CheckSnapshot } from "@/Types/Check";
|
||||
|
||||
const InfraDetailGauge = ({
|
||||
title,
|
||||
progress,
|
||||
upperLabel,
|
||||
upperValue,
|
||||
lowerLabel,
|
||||
lowerValue,
|
||||
}: {
|
||||
title: string;
|
||||
progress: number;
|
||||
upperLabel?: string;
|
||||
upperValue?: string | number;
|
||||
lowerLabel?: string;
|
||||
lowerValue?: string | number;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BaseChart
|
||||
icon={null}
|
||||
title={title}
|
||||
maxWidth={225}
|
||||
>
|
||||
<Stack
|
||||
alignItems={"center"}
|
||||
mb={theme.spacing(4)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Gauge progress={progress} />
|
||||
</Stack>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<Typography>{upperLabel}</Typography>
|
||||
<Typography>{upperValue}</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<Typography>{lowerLabel}</Typography>
|
||||
<Typography>{lowerValue}</Typography>
|
||||
</Stack>
|
||||
</BaseChart>
|
||||
);
|
||||
};
|
||||
|
||||
export const InfraDetailsGauges = ({
|
||||
snapshot,
|
||||
}: {
|
||||
@@ -74,7 +26,7 @@ export const InfraDetailsGauges = ({
|
||||
spacing={theme.spacing(8)}
|
||||
alignItems={"center"}
|
||||
>
|
||||
<InfraDetailGauge
|
||||
<DetailGauge
|
||||
title={t("pages.infrastructure.gauges.memory.title")}
|
||||
progress={(snapshot?.memory?.usage_percent || 0) * 100}
|
||||
upperLabel={t("pages.infrastructure.gauges.memory.upperLabel")}
|
||||
@@ -82,7 +34,7 @@ export const InfraDetailsGauges = ({
|
||||
lowerLabel={t("pages.infrastructure.gauges.memory.lowerLabel")}
|
||||
lowerValue={getGbs(snapshot?.memory?.total_bytes || 0)}
|
||||
/>
|
||||
<InfraDetailGauge
|
||||
<DetailGauge
|
||||
title={t("pages.infrastructure.gauges.cpu.title")}
|
||||
progress={(snapshot?.cpu?.usage_percent || 0) * 100}
|
||||
upperLabel={t("pages.infrastructure.gauges.cpu.upperLabel")}
|
||||
@@ -92,7 +44,7 @@ export const InfraDetailsGauges = ({
|
||||
/>
|
||||
{snapshot?.disk?.map((disk, idx) => {
|
||||
return (
|
||||
<InfraDetailGauge
|
||||
<DetailGauge
|
||||
key={disk?.device || 0 + idx}
|
||||
// title={`Disk ${idx} usage`}
|
||||
title={t("pages.infrastructure.gauges.disk.title", { idx })}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import { getPercentage, formatBytes } from "../../utils/utils.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
const BaseContainer = ({children}) => {
|
||||
const theme = useTheme()
|
||||
return(
|
||||
<Box
|
||||
sx={{
|
||||
padding: theme.spacing(3),
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
minWidth: 200,
|
||||
width: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
|
||||
[theme.breakpoints.down("md")]: {
|
||||
width: `calc(50% - (1 * ${theme.spacing(8)} / 2))`,
|
||||
},
|
||||
}}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const InfrastructureStyleGauge = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const MetricRow = ({ label, value }) => (
|
||||
<Stack
|
||||
justifyContent="space-between"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Typography>{label}</Typography>
|
||||
<Typography sx={{
|
||||
borderRadius: theme.spacing(2),
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
width: "40%",
|
||||
mb: theme.spacing(2),
|
||||
mt: theme.spacing(2),
|
||||
pr: theme.spacing(2),
|
||||
textAlign: "right",
|
||||
}}>
|
||||
{value}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return(
|
||||
<BaseContainer>
|
||||
<Stack direction="column" gap={theme.spacing(2)} alignItems="center">
|
||||
<Box
|
||||
sx = {{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<CustomGauge progress={value} radius={100}/>
|
||||
<Typography component="h2" sx={{fontWeight: 600}}>
|
||||
{heading}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ width:"100%", borderTop:`1px solid ${theme.palette.divider}`}}>
|
||||
<MetricRow label={metricOne} value={valueOne} />
|
||||
{metricTwo && valueTwo && (
|
||||
<MetricRow label={metricTwo} value={valueTwo} />
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</BaseContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const Gauges = ({ diagnostics, isLoading }) => {
|
||||
const heapTotalSize = getPercentage(
|
||||
diagnostics?.v8HeapStats?.totalHeapSizeBytes,
|
||||
diagnostics?.v8HeapStats?.heapSizeLimitBytes
|
||||
);
|
||||
|
||||
const heapUsedSize = getPercentage(
|
||||
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
|
||||
diagnostics?.v8HeapStats?.heapSizeLimitBytes
|
||||
);
|
||||
|
||||
const actualHeapUsed = getPercentage(
|
||||
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
|
||||
diagnostics?.v8HeapStats?.totalHeapSizeBytes
|
||||
);
|
||||
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<InfrastructureStyleGauge
|
||||
value={heapTotalSize}
|
||||
heading={t("diagnosticsPage.gauges.heapAllocationTitle")}
|
||||
metricOne={t("diagnosticsPage.gauges.heapAllocationSubtitle")}
|
||||
valueOne={`${heapTotalSize?.toFixed(1) || 0}%`}
|
||||
metricTwo={t("total")}
|
||||
valueTwo={formatBytes(diagnostics?.v8HeapStats?.heapSizeLimitBytes)}
|
||||
/>
|
||||
<InfrastructureStyleGauge
|
||||
value={heapUsedSize}
|
||||
heading={t("diagnosticsPage.gauges.heapUsageTitle")}
|
||||
metricOne={t("diagnosticsPage.gauges.heapUsageSubtitle")}
|
||||
valueOne={`${heapUsedSize?.toFixed(1) || 0}%`}
|
||||
metricTwo={t("used")}
|
||||
valueTwo={formatBytes(diagnostics?.v8HeapStats?.usedHeapSizeBytes)}
|
||||
/>
|
||||
<InfrastructureStyleGauge
|
||||
value={actualHeapUsed}
|
||||
heading={t("diagnosticsPage.gauges.heapUtilizationTitle")}
|
||||
metricOne={t("diagnosticsPage.gauges.heapUtilizationSubtitle")}
|
||||
valueOne={`${actualHeapUsed?.toFixed(1) || 0}%`}
|
||||
metricTwo={t("total")}
|
||||
valueTwo={formatBytes(diagnostics?.v8HeapStats?.totalHeapSizeBytes)}
|
||||
/>
|
||||
<InfrastructureStyleGauge
|
||||
value={diagnostics?.cpuUsage?.usagePercentage}
|
||||
heading={t("diagnosticsPage.gauges.instantCpuUsageTitle")}
|
||||
metricOne={t("diagnosticsPage.gauges.instantCpuUsageSubtitle")}
|
||||
valueOne={`${diagnostics?.cpuUsage?.usagePercentage?.toFixed(1) || 0}%`}
|
||||
metricTwo=""
|
||||
valueTwo=""
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Gauges.propTypes = {
|
||||
diagnostics: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
InfrastructureStyleGauge.propTypes = {
|
||||
value: PropTypes.number,
|
||||
heading: PropTypes.string,
|
||||
metricOne: PropTypes.string,
|
||||
valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
metricTwo: PropTypes.string,
|
||||
valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
};
|
||||
|
||||
BaseContainer.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default Gauges;
|
||||
@@ -1,97 +0,0 @@
|
||||
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/timeUtilsLegacy.js";
|
||||
import { formatBytes } from "../../utils/utils.js";
|
||||
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;
|
||||
@@ -1,77 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Gauges from "./components/gauges/index.jsx";
|
||||
import Button from "@mui/material/Button";
|
||||
import StatBox from "@/Components/v1/StatBox/index.jsx";
|
||||
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/timeUtilsLegacy.js";
|
||||
import { formatBytes, getPercentage } from "./utils/utils.js";
|
||||
|
||||
const Diagnostics = () => {
|
||||
// Local state
|
||||
|
||||
// Hooks
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [diagnostics, fetchDiagnostics, isLoading, error] = useFetchDiagnostics();
|
||||
// Setup
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<StatusBoxes flexWrap="wrap">
|
||||
<StatBox
|
||||
gradient={true}
|
||||
status="up"
|
||||
heading={t("status")}
|
||||
subHeading={
|
||||
error
|
||||
? t("logsPage.logLevelSelect.values.error")
|
||||
: isLoading
|
||||
? t("commonSaving")
|
||||
: diagnostics
|
||||
? t("diagnosticsPage.diagnosticDescription")
|
||||
: t("general.noOptionsFound", { unit: "data" })
|
||||
}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("diagnosticsPage.stats.eventLoopDelayTitle")}
|
||||
subHeading={getHumanReadableDuration(diagnostics?.eventLoopDelayMs)}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("diagnosticsPage.stats.uptimeTitle")}
|
||||
subHeading={getHumanReadableDuration(diagnostics?.uptimeMs)}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("diagnosticsPage.stats.usedHeapSizeTitle")}
|
||||
subHeading={formatBytes(diagnostics?.v8HeapStats?.usedHeapSizeBytes)}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("diagnosticsPage.stats.totalHeapSizeTitle")}
|
||||
subHeading={formatBytes(diagnostics?.v8HeapStats?.totalHeapSizeBytes)}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("diagnosticsPage.stats.osMemoryLimitTitle")}
|
||||
subHeading={formatBytes(diagnostics?.osStats?.totalMemoryBytes)}
|
||||
/>
|
||||
</StatusBoxes>
|
||||
<Gauges
|
||||
diagnostics={diagnostics}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={fetchDiagnostics}
|
||||
loading={isLoading}
|
||||
>
|
||||
{t("queuePage.refreshButton")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Diagnostics;
|
||||
@@ -1,16 +0,0 @@
|
||||
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`;
|
||||
};
|
||||
@@ -1,176 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Select from "@/Components/v1/Inputs/Select/index.jsx";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
|
||||
|
||||
import { useFetchLogs } from "../../../Hooks/logHooks.js";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const LevelBadge = ({ level }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const levelColors = {
|
||||
info: theme.palette.success.main,
|
||||
warn: theme.palette.warning.main,
|
||||
error: theme.palette.error.main,
|
||||
debug: theme.palette.accent.main,
|
||||
};
|
||||
|
||||
const color = levelColors[level] || theme.palette.primary.contrastText;
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
color: color,
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{level}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const formatTimestamp = (timestamp) => {
|
||||
if (!timestamp) return "-";
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const Logs = () => {
|
||||
const [logLevel, setLogLevel] = useState("all");
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(15);
|
||||
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [logs, isLoading, error] = useFetchLogs();
|
||||
|
||||
const LOG_LEVELS = [
|
||||
{ _id: "all", name: t("logsPage.logLevelSelect.values.all") },
|
||||
{ _id: "info", name: t("logsPage.logLevelSelect.values.info") },
|
||||
{ _id: "warn", name: t("logsPage.logLevelSelect.values.warn") },
|
||||
{ _id: "error", name: t("logsPage.logLevelSelect.values.error") },
|
||||
{ _id: "debug", name: t("logsPage.logLevelSelect.values.debug") },
|
||||
];
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "timestamp",
|
||||
content: t("logsPage.table.timestamp"),
|
||||
render: (row) => (
|
||||
<Typography sx={{ fontSize: 13, fontFamily: "monospace" }}>
|
||||
{formatTimestamp(row.timestamp)}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "level",
|
||||
content: t("logsPage.table.level"),
|
||||
render: (row) => <LevelBadge level={row.level} />,
|
||||
},
|
||||
{
|
||||
id: "service",
|
||||
content: t("logsPage.table.service"),
|
||||
render: (row) => (
|
||||
<Typography sx={{ fontSize: 13 }}>{row.service || "-"}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "method",
|
||||
content: t("logsPage.table.method"),
|
||||
render: (row) => (
|
||||
<Typography sx={{ fontSize: 13, fontFamily: "monospace" }}>{row.method || "-"}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "message",
|
||||
content: t("logsPage.table.message"),
|
||||
render: (row) => (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
maxWidth: 400,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{row.message || "-"}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const filteredLogs = logs
|
||||
?.filter((log) => {
|
||||
if (logLevel === "all") return true;
|
||||
return log.level === logLevel;
|
||||
})
|
||||
.reverse()
|
||||
.map((log, idx) => ({ ...log, id: idx }));
|
||||
|
||||
const paginatedLogs = filteredLogs?.slice(
|
||||
page * rowsPerPage,
|
||||
page * rowsPerPage + rowsPerPage
|
||||
);
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const handleLogLevelChange = (e) => {
|
||||
setLogLevel(e.target.value);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Typography>{t("logsPage.logLevelSelect.title")}</Typography>
|
||||
<Select
|
||||
items={LOG_LEVELS}
|
||||
value={logLevel}
|
||||
onChange={handleLogLevelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<DataTable
|
||||
shouldRender={!isLoading}
|
||||
headers={headers}
|
||||
data={paginatedLogs || []}
|
||||
config={{
|
||||
emptyView: t("logsPage.noLogs"),
|
||||
}}
|
||||
/>
|
||||
|
||||
{filteredLogs?.length > 0 && (
|
||||
<Pagination
|
||||
paginationLabel={t("logsPage.table.logs")}
|
||||
itemCount={filteredLogs?.length || 0}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logs;
|
||||
@@ -1,82 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { TypeToPathMap } from "../../../../../Utils/monitorUtilsLegacy.js";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const FailedJobTable = ({ metrics = {} }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const jobsWithFailures = metrics?.jobsWithFailures;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "monitorId",
|
||||
content: t("queuePage.failedJobTable.monitorIdHeader"),
|
||||
render: (row) => {
|
||||
return row.monitorId;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "monitorUrl",
|
||||
content: t("queuePage.failedJobTable.monitorUrlHeader"),
|
||||
render: (row) => {
|
||||
return row.monitorUrl;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "failCount",
|
||||
content: t("queuePage.failedJobTable.failCountHeader"),
|
||||
render: (row) => {
|
||||
return row.failCount;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "failedAt",
|
||||
content: t("queuePage.failedJobTable.failedAtHeader"),
|
||||
render: (row) => {
|
||||
return row.failedAt;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "failReason",
|
||||
content: t("queuePage.failedJobTable.failReasonHeader"),
|
||||
render: (row) => {
|
||||
return row.failReason;
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
<Typography variant="h2">{t("queuePage.failedJobTable.title")}</Typography>
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={jobsWithFailures}
|
||||
config={{
|
||||
onRowClick: (row) => {
|
||||
const path = TypeToPathMap[row.monitorType];
|
||||
navigate(`/${path}/${row.monitorId}`);
|
||||
},
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover td": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
transition: "background-color .3s ease",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
FailedJobTable.propTypes = {
|
||||
metrics: PropTypes.object,
|
||||
};
|
||||
|
||||
export default FailedJobTable;
|
||||
@@ -1,130 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import Typography from "@mui/material/Typography";
|
||||
// Utils
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { TypeToPathMap } from "../../../../../Utils/monitorUtilsLegacy.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createHeaderFactory } from "@/Components/v1/Table/TableUtils.js";
|
||||
|
||||
const JobTable = ({ jobs = [] }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const buildSx = (row) => {
|
||||
if (row.lockedAt) {
|
||||
return {
|
||||
color: `${theme.palette.success.main} !important`,
|
||||
};
|
||||
}
|
||||
if (!row.active) {
|
||||
return {
|
||||
color: `${theme.palette.warning.main} !important`,
|
||||
};
|
||||
}
|
||||
|
||||
if (row.failCount > 0 && row.lastFailedAt >= row.lastFinishedAt) {
|
||||
return {
|
||||
color: `${theme.palette.error.main} !important`,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const createHeader = createHeaderFactory(buildSx);
|
||||
const headersData = [
|
||||
{
|
||||
id: "id",
|
||||
content: t("queuePage.jobTable.idHeader"),
|
||||
render: (row) => row.monitorId,
|
||||
},
|
||||
{
|
||||
id: "url",
|
||||
content: t("queuePage.jobTable.urlHeader"),
|
||||
render: (row) => row.monitorUrl,
|
||||
},
|
||||
{
|
||||
id: "interval",
|
||||
content: t("queuePage.jobTable.intervalHeader"),
|
||||
render: (row) => `${row.monitorInterval} ms`,
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
content: t("queuePage.jobTable.typeHeader"),
|
||||
render: (row) => row.monitorType,
|
||||
},
|
||||
{
|
||||
id: "active",
|
||||
content: t("queuePage.jobTable.activeHeader"),
|
||||
render: (row) => row.active.toString(),
|
||||
},
|
||||
{
|
||||
id: "runCount",
|
||||
content: t("queuePage.jobTable.runCountHeader"),
|
||||
render: (row) => row.runCount,
|
||||
},
|
||||
{
|
||||
id: "failCount",
|
||||
content: t("queuePage.jobTable.failCountHeader"),
|
||||
render: (row) => row.failCount,
|
||||
},
|
||||
{
|
||||
id: "lastRun",
|
||||
content: t("queuePage.jobTable.lastRunHeader"),
|
||||
render: (row) => row.lastRunAt || "-",
|
||||
},
|
||||
{
|
||||
id: "lockedAt",
|
||||
content: t("queuePage.jobTable.lockedAtHeader"),
|
||||
render: (row) => row.lockedAt || "-",
|
||||
},
|
||||
|
||||
{
|
||||
id: "lastFinish",
|
||||
content: t("queuePage.jobTable.lastFinishedAtHeader"),
|
||||
render: (row) => row.lastFinishedAt || "-",
|
||||
},
|
||||
{
|
||||
id: "lastRunTook",
|
||||
content: t("queuePage.jobTable.lastRunTookHeader"),
|
||||
render: (row) => {
|
||||
const value = row.lastRunTook ? row.lastRunTook + " ms" : "-";
|
||||
return value;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const headers = headersData.map((header) => createHeader(header));
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
<Typography variant="h2">{t("queuePage.jobTable.title")}</Typography>
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={jobs}
|
||||
config={{
|
||||
onRowClick: (row) => {
|
||||
const path = TypeToPathMap[row.monitorType];
|
||||
navigate(`/${path}/${row.monitorId}`);
|
||||
},
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover td": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
transition: "background-color .3s ease",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
JobTable.propTypes = {
|
||||
jobs: PropTypes.array,
|
||||
};
|
||||
|
||||
export default JobTable;
|
||||
@@ -1,38 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import StatBox from "@/Components/v1/StatBox/index.jsx";
|
||||
import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const camelToTitle = (str) => {
|
||||
return str
|
||||
.replace(/([A-Z])/g, " $1")
|
||||
.toLowerCase()
|
||||
.replace(/^./, (m) => m.toUpperCase());
|
||||
};
|
||||
|
||||
const Metrics = ({ metrics = {} }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const data = Object.keys(metrics)
|
||||
.filter((key) => key !== "jobsWithFailures")
|
||||
.map((key) => {
|
||||
return { key, title: camelToTitle(key), value: metrics[key] };
|
||||
});
|
||||
|
||||
return (
|
||||
<StatusBoxes flexWrap="wrap">
|
||||
{data.map((metric) => {
|
||||
return (
|
||||
<StatBox
|
||||
key={metric.key}
|
||||
heading={metric.title}
|
||||
subHeading={metric.value}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StatusBoxes>
|
||||
);
|
||||
};
|
||||
export default Metrics;
|
||||
@@ -1,60 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
|
||||
// Utils
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const camelToTitle = (str) => {
|
||||
return str
|
||||
.replace(/([A-Z])/g, " $1")
|
||||
.toLowerCase()
|
||||
.replace(/^./, (m) => m.toUpperCase());
|
||||
};
|
||||
|
||||
const Metrics = ({ metrics = {} }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const keys = Object.keys(metrics);
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "metric",
|
||||
content: t("queuePage.metricsTable.metricHeader"),
|
||||
render: (row) => {
|
||||
return <Typography>{row.key}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "value",
|
||||
content: t("queuePage.metricsTable.valueHeader"),
|
||||
render: (row) => {
|
||||
return <Typography>{row.value}</Typography>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const data = keys
|
||||
.filter((key) => key !== "jobsWithFailures")
|
||||
.map((key) => {
|
||||
return { key: camelToTitle(key), value: metrics[key] };
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
<Typography variant="h2">{t("queuePage.metricsTable.title")}</Typography>
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={data}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Metrics.propTypes = {
|
||||
metrics: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Metrics;
|
||||
@@ -1,73 +0,0 @@
|
||||
// Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import JobTable from "./components/JobTable/index.jsx";
|
||||
import Metrics from "./components/Metrics/index.jsx";
|
||||
import FailedJobTable from "./components/FailedJobTable/index.jsx";
|
||||
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";
|
||||
import { useFetchQueueData, useFlushQueue } from "../../../Hooks/logHooks.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const QueueDetails = () => {
|
||||
// Local state
|
||||
const [trigger, setTrigger] = useState(false);
|
||||
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [jobs, metrics, isLoading, error] = useFetchQueueData(trigger);
|
||||
const [flushQueue, isFlushing, flushError] = useFlushQueue();
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error || flushError) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Stack
|
||||
gap={theme.spacing(20)}
|
||||
mt={theme.spacing(10)}
|
||||
>
|
||||
<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),
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrigger(!trigger);
|
||||
}}
|
||||
loading={isLoading}
|
||||
>
|
||||
{t("queuePage.refreshButton")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => flushQueue(trigger, setTrigger)}
|
||||
loading={isFlushing}
|
||||
>
|
||||
{t("queuePage.flushButton")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueueDetails;
|
||||
@@ -0,0 +1,24 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { Stats } from "@/Pages/Logs/components/Stats";
|
||||
import { StatGauges } from "@/Pages/Logs/components/StatGauges";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import type { Diagnostics } from "@/Types/Diagnostics";
|
||||
|
||||
export const TabDiagnostics = () => {
|
||||
const theme = useTheme();
|
||||
const {
|
||||
data: diagnostics,
|
||||
isLoading: _isLoading,
|
||||
error: _error,
|
||||
refetch: _refetch,
|
||||
} = useGet<Diagnostics>("/diagnostic/system", {}, { refreshInterval: 5000 });
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Stats diagnostics={diagnostics} />
|
||||
<StatGauges diagnostics={diagnostics} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Select } from "@/Components/v2/inputs";
|
||||
import { TableLogs } from "@/Pages/Logs/components/TableLogs";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import { useState } from "react";
|
||||
import type { Log, LogLevelOption } from "@/Types/Log";
|
||||
import { LOG_LEVEL_OPTIONS } from "@/Types/Log";
|
||||
import { t } from "i18next";
|
||||
import type { RootState } from "@/Types/state";
|
||||
|
||||
export const TabLogs = () => {
|
||||
const theme = useTheme();
|
||||
const { data: logs } = useGet<Log[]>("/logs");
|
||||
const [selectedLogLevel, setSelectedLogLevel] = useState<LogLevelOption>("all");
|
||||
const [page, setPage] = useState(0);
|
||||
const rowsPerPage = useSelector(
|
||||
(state: RootState) => state?.ui?.logs?.rowsPerPage ?? 15
|
||||
);
|
||||
|
||||
const filteredLogs = logs
|
||||
?.filter((log) => {
|
||||
if (selectedLogLevel === "all") return true;
|
||||
return log.level === selectedLogLevel;
|
||||
})
|
||||
.reverse()
|
||||
.map((log, idx) => ({ ...log, id: idx }));
|
||||
|
||||
const paginatedLogs =
|
||||
filteredLogs?.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) ?? [];
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Select
|
||||
sx={{ maxWidth: 200 }}
|
||||
fieldLabel={t("pages.logs.logLevelSelect.label")}
|
||||
value={selectedLogLevel}
|
||||
onChange={(e) => setSelectedLogLevel(e.target.value)}
|
||||
>
|
||||
{LOG_LEVEL_OPTIONS.map((level) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={level}
|
||||
value={level}
|
||||
>
|
||||
<Typography textTransform={"capitalize"}>{level}</Typography>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
<TableLogs
|
||||
logs={paginatedLogs}
|
||||
logCount={filteredLogs?.length ?? 0}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { TableJobs, TableFailedJobs } from "./components/TableJobs";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGet, usePost } from "@/Hooks/UseApi";
|
||||
import type { QueueData } from "@/Types/Queue";
|
||||
import { Metrics } from "@/Pages/Logs/components/Metrics";
|
||||
import { Button } from "@/Components/v2/inputs";
|
||||
|
||||
export const TabQueue = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data: queueData,
|
||||
// isLoading,
|
||||
// error,
|
||||
refetch,
|
||||
} = useGet<QueueData>("/queue/all-metrics", {}, { refreshInterval: 5000 });
|
||||
|
||||
const { post, loading: isFlushing } = usePost();
|
||||
|
||||
const jobs = queueData?.jobs ?? [];
|
||||
const metrics = queueData?.metrics ?? null;
|
||||
|
||||
const handleFlushQueue = async () => {
|
||||
await post("/queue/flush", {});
|
||||
refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Metrics metrics={metrics} />
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ textTransform: "uppercase" }}
|
||||
>
|
||||
{t("pages.logs.jobQueue")}
|
||||
</Typography>
|
||||
<TableJobs jobs={jobs} />
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ textTransform: "uppercase" }}
|
||||
>
|
||||
{t("pages.logs.failedJobs")}
|
||||
</Typography>
|
||||
<TableFailedJobs metrics={metrics} />
|
||||
<Stack alignItems={"flex-end"}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleFlushQueue}
|
||||
loading={isFlushing}
|
||||
>
|
||||
{t("common.buttons.flushQueue")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { StatBox } from "@/Components/v2/design-elements";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { QueueMetrics } from "@/Types/Queue";
|
||||
|
||||
interface MetricsProps {
|
||||
metrics: QueueMetrics | null;
|
||||
}
|
||||
|
||||
export const Metrics = ({ metrics }: MetricsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
if (!metrics) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<StatBox
|
||||
title={t("pages.logs.metrics.jobs")}
|
||||
subtitle={String(metrics.jobs)}
|
||||
/>
|
||||
<StatBox
|
||||
palette={metrics.activeJobs > 0 ? "success" : undefined}
|
||||
title={t("pages.logs.metrics.activeJobs")}
|
||||
subtitle={String(metrics.activeJobs)}
|
||||
/>
|
||||
<StatBox
|
||||
palette={metrics.failingJobs > 0 ? "error" : undefined}
|
||||
title={t("pages.logs.metrics.failingJobs")}
|
||||
subtitle={String(metrics.failingJobs)}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.metrics.totalRuns")}
|
||||
subtitle={String(metrics.totalRuns)}
|
||||
/>
|
||||
<StatBox
|
||||
palette={metrics.totalFailures > 0 ? "error" : undefined}
|
||||
title={t("pages.logs.metrics.totalFailures")}
|
||||
subtitle={String(metrics.totalFailures)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { DetailGauge } from "@/Components/v2/design-elements";
|
||||
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material";
|
||||
import type { Diagnostics } from "@/Types/Diagnostics";
|
||||
|
||||
interface StatGaugesProps {
|
||||
diagnostics: Diagnostics | null;
|
||||
}
|
||||
|
||||
const getPercentage = (value: number, total: number) => {
|
||||
if (!value || !total) return 0;
|
||||
return (value / total) * 100;
|
||||
};
|
||||
|
||||
const formatPercentage = new Intl.NumberFormat("en-US", {
|
||||
style: "percent",
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
|
||||
export const StatGauges = ({ diagnostics }: StatGaugesProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
if (!diagnostics) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const heapTotalSize = getPercentage(
|
||||
diagnostics?.v8HeapStats?.totalHeapSizeBytes,
|
||||
diagnostics?.v8HeapStats?.heapSizeLimitBytes
|
||||
);
|
||||
|
||||
const heapUsedSize = getPercentage(
|
||||
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
|
||||
diagnostics?.v8HeapStats?.heapSizeLimitBytes
|
||||
);
|
||||
|
||||
const actualHeapUsed = getPercentage(
|
||||
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
|
||||
diagnostics?.v8HeapStats?.totalHeapSizeBytes
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<DetailGauge
|
||||
title={t("pages.logs.diagnostics.gauges.heapAllocation")}
|
||||
progress={heapTotalSize}
|
||||
upperValue={formatPercentage.format(heapTotalSize / 100)}
|
||||
lowerLabel={t("pages.logs.diagnostics.gauges.total")}
|
||||
lowerValue={prettyBytes(diagnostics.v8HeapStats?.heapSizeLimitBytes ?? 0)}
|
||||
/>
|
||||
<DetailGauge
|
||||
title={t("pages.logs.diagnostics.gauges.heapUsage")}
|
||||
progress={heapUsedSize}
|
||||
upperLabel={t("pages.logs.diagnostics.gauges.availableMemoryPercentage")}
|
||||
upperValue={formatPercentage.format(heapUsedSize / 100)}
|
||||
lowerLabel={t("pages.logs.diagnostics.gauges.used")}
|
||||
lowerValue={prettyBytes(diagnostics.v8HeapStats?.usedHeapSizeBytes ?? 0)}
|
||||
/>
|
||||
<DetailGauge
|
||||
title={t("pages.logs.diagnostics.gauges.heapUtilization")}
|
||||
progress={actualHeapUsed}
|
||||
upperLabel={t("pages.logs.diagnostics.gauges.allocatedPercentage")}
|
||||
upperValue={formatPercentage.format(actualHeapUsed / 100)}
|
||||
lowerLabel={t("pages.logs.diagnostics.gauges.total")}
|
||||
lowerValue={prettyBytes(diagnostics.v8HeapStats?.usedHeapSizeBytes ?? 0)}
|
||||
/>
|
||||
<DetailGauge
|
||||
title={t("pages.logs.diagnostics.gauges.instantCpuUsage")}
|
||||
progress={diagnostics.cpuUsage?.usagePercentage ?? 0}
|
||||
upperLabel={t("pages.logs.diagnostics.gauges.usedSPercentage")}
|
||||
upperValue={formatPercentage.format(
|
||||
(diagnostics.cpuUsage?.usagePercentage ?? 0) / 100
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { StatBox } from "@/Components/v2/design-elements";
|
||||
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Diagnostics } from "@/Types/Diagnostics";
|
||||
|
||||
interface StatsProps {
|
||||
diagnostics: Diagnostics | null;
|
||||
}
|
||||
|
||||
export const Stats = ({ diagnostics }: StatsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
if (!diagnostics) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.stats.eventLoopDelay")}
|
||||
subtitle={prettyMilliseconds(diagnostics.eventLoopDelayMs ?? 0, {
|
||||
millisecondsDecimalDigits: 2,
|
||||
})}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.stats.uptime")}
|
||||
subtitle={prettyMilliseconds(diagnostics.uptimeMs ?? 0, { hideSeconds: true })}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.stats.usedHeapSize")}
|
||||
subtitle={prettyBytes(diagnostics.v8HeapStats?.usedHeapSizeBytes ?? 0)}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.stats.totalHeapSize")}
|
||||
subtitle={prettyBytes(diagnostics.v8HeapStats?.totalHeapSizeBytes ?? 0)}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.stats.osMemoryLimit")}
|
||||
subtitle={prettyBytes(diagnostics.osStats?.totalMemoryBytes ?? 0)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Table } from "@/Components/v2/design-elements";
|
||||
import { Typography, useTheme } from "@mui/material";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
import { formatTimestamp } from "@/Utils/TimeUtils";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { QueueJobFailure, QueueJobSummary, QueueMetrics } from "@/Types/Queue";
|
||||
import type { Header } from "@/Components/v2/design-elements";
|
||||
|
||||
type QueueJobWithId = QueueJobSummary & { id: string | number };
|
||||
|
||||
interface TableJobsProps {
|
||||
jobs: QueueJobSummary[];
|
||||
}
|
||||
|
||||
export const TableJobs = ({ jobs }: TableJobsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const jobsWithId: QueueJobWithId[] = jobs.map((job) => ({
|
||||
...job,
|
||||
id: job.monitorId,
|
||||
}));
|
||||
|
||||
const headers: Header<QueueJobWithId>[] = [
|
||||
{
|
||||
id: "id",
|
||||
content: t("common.table.headers.monitorId"),
|
||||
render: (row) => <Typography fontFamily={"monospace"}>{row.monitorId}</Typography>,
|
||||
},
|
||||
{
|
||||
id: "url",
|
||||
content: t("common.table.headers.url"),
|
||||
render: (row) => <Typography>{row.monitorUrl}</Typography>,
|
||||
},
|
||||
{
|
||||
id: "interval",
|
||||
content: t("common.table.headers.interval"),
|
||||
render: (row) => (
|
||||
<Typography>{prettyMilliseconds(row.monitorInterval ?? 0)}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
content: t("common.table.headers.type"),
|
||||
render: (row) => <Typography>{row.monitorType}</Typography>,
|
||||
},
|
||||
{
|
||||
id: "active",
|
||||
content: t("common.table.headers.active"),
|
||||
render: (row) => <Typography>{row.active.toString()}</Typography>,
|
||||
},
|
||||
{
|
||||
id: "runCount",
|
||||
content: t("pages.logs.table.headers.runCount"),
|
||||
render: (row) => <Typography>{row.runCount}</Typography>,
|
||||
},
|
||||
{
|
||||
id: "failCount",
|
||||
content: t("pages.logs.table.headers.failCount"),
|
||||
render: (row) => <Typography>{row.failCount}</Typography>,
|
||||
},
|
||||
{
|
||||
id: "lastRun",
|
||||
content: t("pages.logs.table.headers.lastRunAt"),
|
||||
render: (row) => <Typography>{formatTimestamp(row.lastRunAt)}</Typography>,
|
||||
},
|
||||
{
|
||||
id: "lockedAt",
|
||||
content: t("pages.logs.table.headers.lockedAt"),
|
||||
render: (row) => <Typography>{formatTimestamp(row.lockedAt)}</Typography>,
|
||||
},
|
||||
|
||||
{
|
||||
id: "lastFinish",
|
||||
content: t("pages.logs.table.headers.lastFinishedAt"),
|
||||
render: (row) => <Typography>{formatTimestamp(row.lastFinishedAt)}</Typography>,
|
||||
},
|
||||
{
|
||||
id: "lastRunTook",
|
||||
content: t("pages.logs.table.headers.lastRunTook"),
|
||||
render: (row) => {
|
||||
const value = row.lastRunTook ? prettyMilliseconds(row.lastRunTook) : "-";
|
||||
return <Typography>{value}</Typography>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
headers={headers}
|
||||
data={jobsWithId}
|
||||
getRowSx={(row) => ({
|
||||
...(row.lockedAt && {
|
||||
"& td": { backgroundColor: theme.palette.success.light },
|
||||
}),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type QueueJobFailureWithId = QueueJobFailure & { id: string | number };
|
||||
|
||||
interface TableFailedJobsProps {
|
||||
metrics: QueueMetrics | null;
|
||||
}
|
||||
export const TableFailedJobs = ({ metrics }: TableFailedJobsProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (!metrics) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const jobFailuresWithId: QueueJobFailureWithId[] = metrics.jobsWithFailures.map(
|
||||
(job) => ({
|
||||
...job,
|
||||
id: job.monitorId,
|
||||
})
|
||||
);
|
||||
|
||||
const headers: Header<QueueJobFailureWithId>[] = [
|
||||
{
|
||||
id: "monitorId",
|
||||
content: t("common.table.headers.monitorId"),
|
||||
render: (row) => {
|
||||
return <Typography fontFamily={"monospace"}>{row.monitorId}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "monitorUrl",
|
||||
content: t("common.table.headers.url"),
|
||||
render: (row) => {
|
||||
return <Typography>{row.monitorUrl}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "failCount",
|
||||
content: t("pages.logs.table.headers.failCount"),
|
||||
render: (row) => {
|
||||
return <Typography>{row.failCount}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "failedAt",
|
||||
content: t("pages.logs.table.headers.lastFailedAt"),
|
||||
render: (row) => {
|
||||
return <Typography>{formatTimestamp(row.failedAt)}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "failReason",
|
||||
content: t("pages.logs.table.headers.failReason"),
|
||||
render: (row) => {
|
||||
return <Typography>{row.failReason}</Typography>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
headers={headers}
|
||||
data={jobFailuresWithId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Table } from "@/Components/v2/design-elements";
|
||||
import { Pagination } from "@/Components/v2/design-elements/Table";
|
||||
import { useTheme } from "@mui/material";
|
||||
import type { Header } from "@/Components/v2/design-elements/Table";
|
||||
import type { Log, LogLevel } from "@/Types/Log";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { setRowsPerPage } from "@/Features/UI/uiSlice";
|
||||
import { formatTimestamp } from "@/Utils/TimeUtils";
|
||||
|
||||
type LogWithId = Log & { id: number };
|
||||
|
||||
interface TableLogsProps {
|
||||
logs: LogWithId[];
|
||||
logCount: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
}
|
||||
|
||||
const LevelBadge = ({ level }: { level: LogLevel }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const levelColors: Record<LogLevel, string> = {
|
||||
info: theme.palette.success.main,
|
||||
warn: theme.palette.warning.main,
|
||||
error: theme.palette.error.main,
|
||||
debug: theme.palette.info.main,
|
||||
};
|
||||
|
||||
const color = levelColors[level] || theme.palette.text.primary;
|
||||
|
||||
return (
|
||||
<Typography
|
||||
fontWeight={600}
|
||||
color={color}
|
||||
textTransform={"uppercase"}
|
||||
>
|
||||
{level}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const rowsPerPage = useSelector(
|
||||
(state: RootState) => state?.ui?.logs?.rowsPerPage ?? 15
|
||||
);
|
||||
|
||||
const headers: Header<Log & { id: number }>[] = [
|
||||
{
|
||||
id: "timestamp",
|
||||
content: t("pages.logs.table.headers.timestamp"),
|
||||
render: (row) => (
|
||||
<Typography sx={{ fontFamily: "monospace" }}>
|
||||
{formatTimestamp(row.timestamp)}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "level",
|
||||
content: t("pages.logs.table.headers.level"),
|
||||
render: (row) => <LevelBadge level={row.level} />,
|
||||
},
|
||||
{
|
||||
id: "service",
|
||||
content: t("pages.logs.table.headers.service"),
|
||||
render: (row) => <Typography>{row.service || "-"}</Typography>,
|
||||
},
|
||||
{
|
||||
id: "method",
|
||||
content: t("pages.logs.table.headers.method"),
|
||||
render: (row) => (
|
||||
<Typography fontFamily={"monospace"}>{row.method || "-"}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "message",
|
||||
content: t("common.table.headers.message"),
|
||||
render: (row) => (
|
||||
<Typography
|
||||
maxWidth={400}
|
||||
overflow={"hidden"}
|
||||
textOverflow={"ellipsis"}
|
||||
whiteSpace={"nowrap"}
|
||||
>
|
||||
{row.message || "-"}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePageChange = (
|
||||
_e: React.MouseEvent<HTMLButtonElement> | null,
|
||||
newPage: number
|
||||
) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleRowsPerPageChange = (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
) => {
|
||||
dispatch(setRowsPerPage({ value: Number(e.target.value), table: "logs" }));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Table
|
||||
headers={headers}
|
||||
data={logs}
|
||||
emptyViewText={t("pages.logs.noLogs")}
|
||||
/>
|
||||
<Pagination
|
||||
itemsOnPage={logs.length}
|
||||
component="div"
|
||||
count={logCount}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onPageChange={handlePageChange}
|
||||
onRowsPerPageChange={handleRowsPerPageChange}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import Queue from "./Queue/index.jsx";
|
||||
import LogsComponent from "./Logs/index.jsx";
|
||||
import Diagnostics from "./Diagnostics/index.jsx";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
|
||||
const Logs = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
// Local state
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
// Handlers
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
const BREADCRUMBS = [{ name: t("logsPage.title"), path: "/logs" }];
|
||||
return (
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
sx={{
|
||||
position: "sticky",
|
||||
top: theme.spacing(0),
|
||||
backdropFilter: "blur(10px)",
|
||||
zIndex: theme.zIndex.appBar,
|
||||
}}
|
||||
>
|
||||
<Tab label={t("logsPage.tabs.logs")} />
|
||||
<Tab label={t("logsPage.tabs.queue")} />
|
||||
<Tab label={t("logsPage.tabs.diagnostics")} />
|
||||
</Tabs>
|
||||
{value === 0 && <LogsComponent />}
|
||||
{value === 1 && <Queue />}
|
||||
{value === 2 && <Diagnostics />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logs;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { BasePage, Tabs, Tab } from "@/Components/v2/design-elements";
|
||||
import { TabLogs } from "@/Pages/Logs/TabLogs";
|
||||
import { TabQueue } from "@/Pages/Logs/TabQueue";
|
||||
import { TabDiagnostics } from "@/Pages/Logs/TabDiagnostics";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const LogsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<number>(2);
|
||||
return (
|
||||
<BasePage>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, newValue: number) => setActiveTab(newValue)}
|
||||
>
|
||||
<Tab label={t("pages.logs.tabs.logs")} />
|
||||
<Tab label={t("pages.logs.tabs.queue")} />
|
||||
<Tab label={t("pages.logs.tabs.diagnostics")} />
|
||||
</Tabs>
|
||||
{activeTab === 0 && <TabLogs />}
|
||||
{activeTab === 1 && <TabQueue />}
|
||||
{activeTab === 2 && <TabDiagnostics />}
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogsPage;
|
||||
@@ -55,7 +55,7 @@ import ProtectedRoute from "../Components/v1/ProtectedRoute";
|
||||
import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute";
|
||||
import withAdminCheck from "@/Components/v1/HOC/withAdminCheck";
|
||||
import BulkImport from "../Pages/Uptime/BulkImport/index.jsx";
|
||||
import Logs from "../Pages/Logs/index.jsx";
|
||||
import Logs from "../Pages/Logs";
|
||||
|
||||
import CreateMonitor from "@/Pages/CreateMonitor";
|
||||
|
||||
@@ -350,7 +350,9 @@ const Routes = () => {
|
||||
path="logs"
|
||||
element={
|
||||
<RoleProtectedRoute roles={["admin", "superadmin"]}>
|
||||
<Logs />
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<Logs />
|
||||
</ThemeProvider>
|
||||
</RoleProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
export interface Diagnostics {
|
||||
osStats: OsStats;
|
||||
memoryUsage: MemoryUsage;
|
||||
cpuUsage: CpuUsage;
|
||||
v8HeapStats: V8HeapStats;
|
||||
eventLoopDelayMs: number;
|
||||
uptimeMs: number;
|
||||
}
|
||||
|
||||
export interface OsStats {
|
||||
freeMemoryBytes: number;
|
||||
totalMemoryBytes: number;
|
||||
}
|
||||
|
||||
export interface MemoryUsage {
|
||||
rss: number;
|
||||
heapTotal: number;
|
||||
heapUsed: number;
|
||||
external: number;
|
||||
arrayBuffers: number;
|
||||
}
|
||||
|
||||
export interface CpuUsage {
|
||||
userUsageMs: number;
|
||||
systemUsageMs: number;
|
||||
usagePercentage: number;
|
||||
}
|
||||
|
||||
export interface V8HeapStats {
|
||||
totalHeapSizeBytes: number;
|
||||
usedHeapSizeBytes: number;
|
||||
heapSizeLimitBytes: number;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export const LOG_LEVELS = ["debug", "info", "warn", "error"] as const;
|
||||
export type LogLevel = (typeof LOG_LEVELS)[number];
|
||||
export const LOG_LEVEL_OPTIONS = ["all", ...LOG_LEVELS] as const;
|
||||
export type LogLevelOption = (typeof LOG_LEVEL_OPTIONS)[number];
|
||||
export interface Log {
|
||||
message: string;
|
||||
service?: string;
|
||||
method?: string;
|
||||
details?: Record<string, unknown>;
|
||||
stack?: string;
|
||||
level: LogLevel;
|
||||
timestamp: string;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { MonitorType } from "./Monitor";
|
||||
|
||||
export interface QueueJobFailure {
|
||||
monitorId: string | number;
|
||||
monitorUrl: string | null;
|
||||
monitorType: MonitorType | null;
|
||||
failedAt: number | null;
|
||||
failCount: number;
|
||||
failReason: string | null;
|
||||
}
|
||||
|
||||
export interface QueueMetrics {
|
||||
jobs: number;
|
||||
activeJobs: number;
|
||||
failingJobs: number;
|
||||
jobsWithFailures: QueueJobFailure[];
|
||||
totalRuns: number;
|
||||
totalFailures: number;
|
||||
}
|
||||
|
||||
export interface QueueJobSummary {
|
||||
monitorId: string | number;
|
||||
monitorUrl: string | null;
|
||||
monitorType: MonitorType | null;
|
||||
monitorInterval: number | null;
|
||||
active: boolean;
|
||||
lockedAt: number | null;
|
||||
runCount: number;
|
||||
failCount: number;
|
||||
failReason: string | null;
|
||||
lastRunAt: number | null;
|
||||
lastFinishedAt: number | null;
|
||||
lastRunTook: number | null;
|
||||
lastFailedAt: number | null;
|
||||
}
|
||||
|
||||
export interface QueueData {
|
||||
jobs: QueueJobSummary[];
|
||||
metrics: QueueMetrics;
|
||||
}
|
||||
@@ -19,6 +19,9 @@ export interface UIState {
|
||||
infrastructure: {
|
||||
rowsPerPage: number;
|
||||
};
|
||||
logs: {
|
||||
rowsPerPage: number;
|
||||
};
|
||||
sidebar: {
|
||||
collapsed: boolean;
|
||||
};
|
||||
|
||||
@@ -50,3 +50,9 @@ export const tooltipDateFormatLookup = (range: string) => {
|
||||
}
|
||||
return format;
|
||||
};
|
||||
|
||||
export const formatTimestamp = (timestamp: string | number | null): string => {
|
||||
if (!timestamp) return "-";
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
@@ -199,7 +199,8 @@
|
||||
"save": "Save",
|
||||
"test": "Test",
|
||||
"testNotifications": "Test notifications",
|
||||
"toggleTheme": "Toggles light & dark"
|
||||
"toggleTheme": "Toggles light & dark",
|
||||
"flushQueue": "Flush queue"
|
||||
},
|
||||
"charts": {
|
||||
"labels": {
|
||||
@@ -230,9 +231,13 @@
|
||||
"dateTime": "Date & time",
|
||||
"message": "Message",
|
||||
"monitor": "Monitor",
|
||||
"monitorId": "Monitor ID",
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"type": "Type"
|
||||
"type": "Type",
|
||||
"url": "Url",
|
||||
"interval": "Interval",
|
||||
"active": "Active"
|
||||
}
|
||||
},
|
||||
"toasts": {
|
||||
@@ -777,6 +782,63 @@
|
||||
"title": "An infrastructure monitor is used to:"
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
"tabs": {
|
||||
"diagnostics": "Diagnostics",
|
||||
"logs": "Logs",
|
||||
"queue": "Job queue"
|
||||
},
|
||||
"logLevelSelect": {
|
||||
"label": "Log level"
|
||||
},
|
||||
"jobQueue": "Job queue",
|
||||
"failedJobs": "Failed jobs",
|
||||
"noLogs": "No logs found",
|
||||
"metrics": {
|
||||
"jobs": "Jobs",
|
||||
"activeJobs": "Active jobs",
|
||||
"failingJobs": "Failing jobs",
|
||||
"totalRuns": "Total runs",
|
||||
"totalFailures": "Total failures"
|
||||
},
|
||||
"diagnostics": {
|
||||
"stats": {
|
||||
"eventLoopDelay": "Event loop delay",
|
||||
"uptime": "Uptime",
|
||||
"usedHeapSize": "Used heap size",
|
||||
"totalHeapSize": "Total heap size",
|
||||
"osMemoryLimit": "OS memory limit"
|
||||
},
|
||||
"gauges": {
|
||||
"heapAllocation": "Heap allocaton",
|
||||
"heapUsage": "Heap usage",
|
||||
"heapUtilization": "Heap utilization",
|
||||
"instantCpuUsage": "Instant CPU usage",
|
||||
"availableMemoryPercentage": "% of available memory",
|
||||
"allocatedPercentage": "% allocated",
|
||||
"usedSPercentage": "% of 1s used by CPU",
|
||||
"total": "Total",
|
||||
"used": "Used"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"timestamp": "Timestamp",
|
||||
"level": "Level",
|
||||
"service": "Service",
|
||||
"method": "Method",
|
||||
"monitorId": "Monitor ID",
|
||||
"runCount": "Run count",
|
||||
"failCount": "Fail count",
|
||||
"lastRunAt": "Last run at",
|
||||
"lockedAt": "Locked at",
|
||||
"lastFinishedAt": "Last finished at",
|
||||
"lastRunTook": "Last run took",
|
||||
"lastFailedAt": "Last failed at",
|
||||
"failReason": "Fail reason"
|
||||
}
|
||||
}
|
||||
},
|
||||
"maintenanceWindow": {
|
||||
"fallback": {
|
||||
"actionButton": "Let's create your first maintenance window!",
|
||||
|
||||
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default defineConfig(({}) => {
|
||||
let version = "3.2.1";
|
||||
let version = "3.3";
|
||||
|
||||
return {
|
||||
base: "/",
|
||||
|
||||
Reference in new issue
Block a user