queue tab

This commit is contained in:
Alex Holliday committed 2026-02-05 21:40:36 +00:00
1 parent a148f56b22
commit b0686c0576
8 files changed
+335 -25

No files matched your search

@@ -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);
+50
View File
@@ -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>
);
};
+45 -1
View File
@@ -1,3 +1,47 @@
import Stack from "@mui/material/Stack";
import { TableJobs, TableFailedJobs } from "@/Pages/Logs/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/Metrics";
import { Button } from "@/Components/v2/inputs";
export const TabQueue = () => {
return "Queue";
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} />
<TableJobs jobs={jobs} />
<TableFailedJobs metrics={metrics} />
<Stack alignItems={"flex-end"}>
<Button
variant="contained"
onClick={handleFlushQueue}
loading={isFlushing}
>
{t("common.buttons.flushQueue")}
</Button>
</Stack>
</Stack>
);
};
+164
View File
@@ -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}
/>
);
};
+8 -20
View File
@@ -9,6 +9,7 @@ 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 };
@@ -42,12 +43,6 @@ const LevelBadge = ({ level }: { level: LogLevel }) => {
);
};
const formatTimestamp = (timestamp: string): string => {
if (!timestamp) return "-";
const date = new Date(timestamp);
return date.toLocaleString();
};
export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) => {
const { t } = useTranslation();
const dispatch = useDispatch();
@@ -60,7 +55,7 @@ export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) =>
id: "timestamp",
content: t("pages.logs.table.headers.timestamp"),
render: (row) => (
<Typography sx={{ fontSize: 13, fontFamily: "monospace" }}>
<Typography sx={{ fontFamily: "monospace" }}>
{formatTimestamp(row.timestamp)}
</Typography>
),
@@ -73,17 +68,13 @@ export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) =>
{
id: "service",
content: t("pages.logs.table.headers.service"),
render: (row) => (
<Typography sx={{ fontSize: 13 }}>{row.service || "-"}</Typography>
),
render: (row) => <Typography>{row.service || "-"}</Typography>,
},
{
id: "method",
content: t("pages.logs.table.headers.method"),
render: (row) => (
<Typography sx={{ fontSize: 13, fontFamily: "monospace" }}>
{row.method || "-"}
</Typography>
<Typography fontFamily={"monospace"}>{row.method || "-"}</Typography>
),
},
{
@@ -91,13 +82,10 @@ export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) =>
content: t("common.table.headers.message"),
render: (row) => (
<Typography
sx={{
fontSize: 13,
maxWidth: 400,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
maxWidth={400}
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
>
{row.message || "-"}
</Typography>
+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;
}
+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();
};
+18 -4
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": {
@@ -787,6 +792,13 @@
"label": "Log level"
},
"noLogs": "No logs found",
"metrics": {
"jobs": "Jobs",
"activeJobs": "Active jobs",
"failingJobs": "Failing jobs",
"totalRuns": "Total runs",
"totalFailures": "Total failures"
},
"table": {
"headers": {
"timestamp": "Timestamp",
@@ -798,8 +810,10 @@
"failCount": "Fail count",
"lastRunAt": "Last run at",
"lockedAt": "Locked at",
"lastFinisehdAt": "Last finished at",
"lastRunTook": "Last run took"
"lastFinishedAt": "Last finished at",
"lastRunTook": "Last run took",
"lastFailedAt": "Last failed at",
"failReason": "Fail reason"
}
}
},