queue tab
This commit is contained in:
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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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": {
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user