Merge pull request #3249 from bluewave-labs/feat/v2-logs

Feat/v2 logs
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-02-06 11:00:31 -08:00
commit 9caa6c830f
34 files changed
+894 -1021

No files matched your search

+13 -1
View File
@@ -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"
},
+1
View File
@@ -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);
+6 -2
View File
@@ -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`;
};
-176
View File
@@ -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;
-73
View File
@@ -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;
+24
View File
@@ -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>
);
};
+63
View File
@@ -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>
);
};
+60
View File
@@ -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>
);
};
-50
View File
@@ -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;
+29
View File
@@ -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;
+4 -2
View File
@@ -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>
}
/>
+33
View File
@@ -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;
}
+13
View File
@@ -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;
}
+40
View File
@@ -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;
}
+3
View File
@@ -19,6 +19,9 @@ export interface UIState {
infrastructure: {
rowsPerPage: number;
};
logs: {
rowsPerPage: number;
};
sidebar: {
collapsed: boolean;
};
+6
View File
@@ -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();
};
+64 -2
View File
@@ -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!",
+1 -1
View File
@@ -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: "/",