From b0686c0576d46a8b4e2d3c279aadab340b710b9b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Feb 2026 21:40:36 +0000 Subject: [PATCH] queue tab --- .../Components/v2/design-elements/Table.tsx | 4 + client/src/Pages/Logs/Metrics.tsx | 50 ++++++ client/src/Pages/Logs/TabQueue.tsx | 46 ++++- client/src/Pages/Logs/TableJobs.tsx | 164 ++++++++++++++++++ client/src/Pages/Logs/TableLogs.tsx | 28 +-- client/src/Types/Queue.ts | 40 +++++ client/src/Utils/TimeUtils.ts | 6 + client/src/locales/en.json | 22 ++- 8 files changed, 335 insertions(+), 25 deletions(-) create mode 100644 client/src/Pages/Logs/Metrics.tsx create mode 100644 client/src/Pages/Logs/TableJobs.tsx create mode 100644 client/src/Types/Queue.ts diff --git a/client/src/Components/v2/design-elements/Table.tsx b/client/src/Components/v2/design-elements/Table.tsx index aea8b40a0..35cd4512f 100644 --- a/client/src/Components/v2/design-elements/Table.tsx +++ b/client/src/Components/v2/design-elements/Table.tsx @@ -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 = { @@ -47,6 +48,7 @@ type DataTableProps = renderExpandedContent?: (row: T) => React.ReactNode; emptyViewText?: string; emptyViewPositive?: boolean; + getRowSx?: (row: T) => SxProps; }; export function DataTable< @@ -64,6 +66,7 @@ export function DataTable< renderExpandedContent, emptyViewText, emptyViewPositive, + getRowSx, }: DataTableProps) { const theme = useTheme(); const [expanded, setExpanded] = useState<(string | number) | null>(null); @@ -204,6 +207,7 @@ export function DataTable< { if (expandableRows) handleExpand(row); diff --git a/client/src/Pages/Logs/Metrics.tsx b/client/src/Pages/Logs/Metrics.tsx new file mode 100644 index 000000000..8f96ec5ac --- /dev/null +++ b/client/src/Pages/Logs/Metrics.tsx @@ -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 ( + + + 0 ? "success" : undefined} + title={t("pages.logs.metrics.activeJobs")} + subtitle={String(metrics.activeJobs)} + /> + 0 ? "error" : undefined} + title={t("pages.logs.metrics.failingJobs")} + subtitle={String(metrics.failingJobs)} + /> + + 0 ? "error" : undefined} + title={t("pages.logs.metrics.totalFailures")} + subtitle={String(metrics.totalFailures)} + /> + + ); +}; diff --git a/client/src/Pages/Logs/TabQueue.tsx b/client/src/Pages/Logs/TabQueue.tsx index e327218d3..15efd56b7 100644 --- a/client/src/Pages/Logs/TabQueue.tsx +++ b/client/src/Pages/Logs/TabQueue.tsx @@ -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("/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 ( + + + + + + + + + ); }; diff --git a/client/src/Pages/Logs/TableJobs.tsx b/client/src/Pages/Logs/TableJobs.tsx new file mode 100644 index 000000000..e9608df9a --- /dev/null +++ b/client/src/Pages/Logs/TableJobs.tsx @@ -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[] = [ + { + id: "id", + content: t("common.table.headers.monitorId"), + render: (row) => {row.monitorId}, + }, + { + id: "url", + content: t("common.table.headers.url"), + render: (row) => {row.monitorUrl}, + }, + { + id: "interval", + content: t("common.table.headers.interval"), + render: (row) => ( + {prettyMilliseconds(row.monitorInterval ?? 0)} + ), + }, + { + id: "type", + content: t("common.table.headers.type"), + render: (row) => {row.monitorType}, + }, + { + id: "active", + content: t("common.table.headers.active"), + render: (row) => {row.active.toString()}, + }, + { + id: "runCount", + content: t("pages.logs.table.headers.runCount"), + render: (row) => {row.runCount}, + }, + { + id: "failCount", + content: t("pages.logs.table.headers.failCount"), + render: (row) => {row.failCount}, + }, + { + id: "lastRun", + content: t("pages.logs.table.headers.lastRunAt"), + render: (row) => {formatTimestamp(row.lastRunAt)}, + }, + { + id: "lockedAt", + content: t("pages.logs.table.headers.lockedAt"), + render: (row) => {formatTimestamp(row.lockedAt)}, + }, + + { + id: "lastFinish", + content: t("pages.logs.table.headers.lastFinishedAt"), + render: (row) => {formatTimestamp(row.lastFinishedAt)}, + }, + { + id: "lastRunTook", + content: t("pages.logs.table.headers.lastRunTook"), + render: (row) => { + const value = row.lastRunTook ? prettyMilliseconds(row.lastRunTook) : "-"; + return {value}; + }, + }, + ]; + + return ( + ({ + ...(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[] = [ + { + id: "monitorId", + content: t("common.table.headers.monitorId"), + render: (row) => { + return {row.monitorId}; + }, + }, + { + id: "monitorUrl", + content: t("common.table.headers.url"), + render: (row) => { + return {row.monitorUrl}; + }, + }, + { + id: "failCount", + content: t("pages.logs.table.headers.failCount"), + render: (row) => { + return {row.failCount}; + }, + }, + { + id: "failedAt", + content: t("pages.logs.table.headers.lastFailedAt"), + render: (row) => { + return {formatTimestamp(row.failedAt)}; + }, + }, + { + id: "failReason", + content: t("pages.logs.table.headers.failReason"), + render: (row) => { + return {row.failReason}; + }, + }, + ]; + + return ( +
+ ); +}; diff --git a/client/src/Pages/Logs/TableLogs.tsx b/client/src/Pages/Logs/TableLogs.tsx index e565739bd..dace0db43 100644 --- a/client/src/Pages/Logs/TableLogs.tsx +++ b/client/src/Pages/Logs/TableLogs.tsx @@ -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) => ( - + {formatTimestamp(row.timestamp)} ), @@ -73,17 +68,13 @@ export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) => { id: "service", content: t("pages.logs.table.headers.service"), - render: (row) => ( - {row.service || "-"} - ), + render: (row) => {row.service || "-"}, }, { id: "method", content: t("pages.logs.table.headers.method"), render: (row) => ( - - {row.method || "-"} - + {row.method || "-"} ), }, { @@ -91,13 +82,10 @@ export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) => content: t("common.table.headers.message"), render: (row) => ( {row.message || "-"} diff --git a/client/src/Types/Queue.ts b/client/src/Types/Queue.ts new file mode 100644 index 000000000..77a770aeb --- /dev/null +++ b/client/src/Types/Queue.ts @@ -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; +} diff --git a/client/src/Utils/TimeUtils.ts b/client/src/Utils/TimeUtils.ts index a19a0bf8b..abd56ce44 100644 --- a/client/src/Utils/TimeUtils.ts +++ b/client/src/Utils/TimeUtils.ts @@ -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(); +}; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 6ec628238..307161905 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -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" } } },