Merge pull request #3633 from bluewave-labs/fix/queue-table

fix: queue table
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-05-04 12:19:39 -07:00
commit 6a8f97ae65
9 files changed
+165 -37

No files matched your search

+11 -25
View File
@@ -10,6 +10,7 @@ import type { QueueData } from "@/Types/Queue";
import { Metrics } from "@/Pages/Logs/components/Metrics";
import { Button } from "@/Components/inputs";
import { EmptyState } from "@/Components/design-elements";
import { SPACING } from "@/Utils/Theme/constants";
export const TabQueue = () => {
const theme = useTheme();
@@ -19,7 +20,7 @@ export const TabQueue = () => {
isLoading,
// error,
refetch,
} = useGet<QueueData>("/queue/all-metrics", {}, { refreshInterval: 5000 });
} = useGet<QueueData>("/queue/all-metrics", {}, { refreshInterval: 1000 });
const { post, loading: isFlushing } = usePost();
@@ -49,32 +50,22 @@ export const TabQueue = () => {
<Stack gap={theme.spacing(1)}>
<Typography
variant="eyebrow"
color="text.secondary"
color={theme.palette.text.secondary}
>
{t("pages.logs.jobQueue")}
</Typography>
<Typography
sx={{
fontSize: 13,
color: theme.palette.text.secondary,
}}
>
<Typography color={theme.palette.text.secondary}>
<Trans
i18nKey="pages.logs.jobQueueExplainer"
components={{
highlight: (
<Box
component="span"
sx={{
backgroundColor:
theme.palette.mode === "dark"
? "rgba(19, 113, 91, 0.18)"
: "#ECF7F2",
color: theme.palette.text.primary,
px: 1,
py: 0.25,
borderRadius: 0.5,
}}
bgcolor={theme.palette.rowStatus.running}
color={theme.palette.text.primary}
px={SPACING.SM}
py={SPACING.XXS}
borderRadius={theme.shape.borderRadius}
/>
),
}}
@@ -87,16 +78,11 @@ export const TabQueue = () => {
<Stack gap={theme.spacing(1)}>
<Typography
variant="eyebrow"
color="text.secondary"
color={theme.palette.text.secondary}
>
{t("pages.logs.failedJobs")}
</Typography>
<Typography
sx={{
fontSize: 13,
color: theme.palette.text.secondary,
}}
>
<Typography color={theme.palette.text.secondary}>
{t("pages.logs.failedJobsExplainer")}
</Typography>
</Stack>
+41 -10
View File
@@ -30,6 +30,22 @@ export const TableJobs = ({ jobs }: TableJobsProps) => {
};
const headers: Header<QueueJobWithId>[] = [
{
id: "state",
content: t("pages.logs.table.headers.state"),
render: (row) => {
if (row.lockedAt) {
return t("common.labels.running");
}
if (row.monitorActive === true) {
return t("common.labels.active");
}
if (row.monitorActive === false) {
return t("common.labels.paused");
}
return t("common.labels.idle");
},
},
{
id: "id",
content: t("common.table.headers.monitorId"),
@@ -47,7 +63,10 @@ export const TableJobs = ({ jobs }: TableJobsProps) => {
id: "url",
content: t("common.table.headers.url"),
render: (row) => (
<Typography title={row.monitorUrl ?? ""} sx={cellSx}>
<Typography
title={row.monitorUrl ?? ""}
sx={cellSx}
>
{row.monitorUrl}
</Typography>
),
@@ -60,9 +79,15 @@ export const TableJobs = ({ jobs }: TableJobsProps) => {
{
id: "interval",
content: t("common.table.headers.interval"),
render: (row) => (
<Typography sx={cellSx}>{prettyMilliseconds(row.monitorInterval ?? 0)}</Typography>
),
render: (row) => {
let interval;
if (row.monitorId.toString().includes("geo")) {
interval = row.monitorGeoInterval ?? 0;
} else {
interval = row.repeat ?? 0;
}
return <Typography sx={cellSx}>{prettyMilliseconds(interval)}</Typography>;
},
},
{
id: "lastRun",
@@ -70,7 +95,10 @@ export const TableJobs = ({ jobs }: TableJobsProps) => {
render: (row) => {
const v = formatTimestamp(row.lastRunAt) ?? "-";
return (
<Typography title={v} sx={cellSx}>
<Typography
title={v}
sx={cellSx}
>
{v}
</Typography>
);
@@ -90,7 +118,10 @@ export const TableJobs = ({ jobs }: TableJobsProps) => {
render: (row) => {
const v = formatTimestamp(row.lockedAt) ?? "-";
return (
<Typography title={v} sx={cellSx}>
<Typography
title={v}
sx={cellSx}
>
{v}
</Typography>
);
@@ -98,16 +129,16 @@ export const TableJobs = ({ jobs }: TableJobsProps) => {
},
];
const isDark = theme.palette.mode === "dark";
const runningBg = isDark ? "rgba(19, 113, 91, 0.18)" : "#ECF7F2";
return (
<Table
headers={headers}
data={jobsWithId}
getRowSx={(row) => ({
...(row.lockedAt && {
"& td": { backgroundColor: runningBg },
"& td": { backgroundColor: theme.palette.rowStatus.running },
}),
...(row.monitorActive === false && {
"& td": { backgroundColor: theme.palette.rowStatus.paused },
}),
})}
/>
+3
View File
@@ -23,6 +23,8 @@ export interface QueueJobSummary {
monitorUrl: string | null;
monitorType: MonitorType | null;
monitorInterval: number | null;
monitorGeoInterval: number | null;
monitorActive: boolean | null;
active: boolean;
lockedAt: number | null;
runCount: number;
@@ -32,6 +34,7 @@ export interface QueueJobSummary {
lastFinishedAt: number | null;
lastRunTook: number | null;
lastFailedAt: number | null;
repeat: number | null;
}
export interface QueueData {
+13
View File
@@ -1,3 +1,5 @@
import { alpha } from "@mui/material/styles";
const typographyBase = 13;
export const typographyLevels = {
@@ -17,6 +19,9 @@ export const colors = {
gray850: "#1c1c21",
brandGreen: "#13715B",
brandGreenLight: "#4DAF94",
green200: "#ECF7F2",
yellow200: "#FFF4E5",
orange500: "#FFA500",
};
export const lightPalette = {
@@ -29,6 +34,10 @@ export const lightPalette = {
sidebar: {
accent: colors.brandGreen,
},
rowStatus: {
running: colors.green200,
paused: colors.yellow200,
},
};
export const darkPalette = {
@@ -45,4 +54,8 @@ export const darkPalette = {
sidebar: {
accent: colors.brandGreenLight,
},
rowStatus: {
running: alpha(colors.brandGreen, 0.18),
paused: alpha(colors.orange500, 0.18),
},
};
+2
View File
@@ -15,9 +15,11 @@ declare module "@mui/material/styles" {
}
interface Palette {
sidebar: { accent: string };
rowStatus: { running: string; paused: string };
}
interface PaletteOptions {
sidebar?: { accent: string };
rowStatus?: { running: string; paused: string };
}
}
+1
View File
@@ -1,4 +1,5 @@
export const SPACING = {
XXS: 0.25,
XS: 0.5,
SM: 1,
MD: 1.5,
+4 -1
View File
@@ -70,7 +70,9 @@
},
"labels": {
"active": "Active",
"paused": "paused",
"running": "Running",
"paused": "Paused",
"idle": "Idle",
"na": "N/A",
"resolved": "Resolved",
"responseTime": "Response time"
@@ -887,6 +889,7 @@
},
"table": {
"headers": {
"state": "State",
"timestamp": "Timestamp",
"level": "Level",
"service": "Service",
@@ -28,6 +28,8 @@ type QueueJobSummary = {
monitorUrl: string | null;
monitorType: string | null;
monitorInterval: number | null;
monitorGeoInterval: number | null;
monitorActive: boolean | null;
active: boolean;
lockedAt: number | null;
runCount: number;
@@ -37,6 +39,7 @@ type QueueJobSummary = {
lastFinishedAt: number | null;
lastRunTook: number | null;
lastFailedAt: number | null;
repeat: number | null;
};
export interface ISuperSimpleQueue {
@@ -223,6 +226,8 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
if (result === false) {
throw new Error("Failed to pause monitor");
}
this.scheduler.updateJob(monitor.id, { data: monitor });
const geoJob = await this.scheduler.getJob(`${monitor.id}-geo`);
if (geoJob) {
const geoRes = await this.scheduler.pauseJob(`${monitor.id}-geo`);
@@ -233,6 +238,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
method: "pauseJob",
});
}
this.scheduler.updateJob(`${monitor.id}-geo`, { data: monitor });
}
this.logger.debug({
@@ -247,6 +253,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
if (result === false) {
throw new Error("Failed to resume monitor");
}
this.scheduler.updateJob(monitor.id, { data: monitor });
const geoJob = await this.scheduler.getJob(`${monitor.id}-geo`);
if (geoJob) {
@@ -258,6 +265,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
method: "resumeJob",
});
}
this.scheduler.updateJob(`${monitor.id}-geo`, { data: monitor });
}
this.logger.debug({
message: `Resumed monitor ${monitor.id}`,
@@ -359,7 +367,10 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
monitorUrl: job.data?.url || null,
monitorType: job.data?.type || null,
monitorInterval: job.data?.interval || null,
monitorGeoInterval: job.data?.geoCheckInterval || null,
monitorActive: job.data?.isActive ?? null,
active: job.active,
repeat: job.repeat ?? null,
lockedAt: job.lockedAt ?? null,
runCount: job.runCount ?? 0,
failCount: job.failCount ?? 0,
@@ -302,6 +302,17 @@ describe("SuperSimpleQueue", () => {
expect(logger.debug).toHaveBeenCalledWith(expect.objectContaining({ message: "Paused monitor mon-1" }));
});
it("refreshes scheduler data with current monitor on pause", async () => {
const { queue, scheduler } = createQueue();
(scheduler.getJob as jest.Mock).mockResolvedValue({ id: "mon-1-geo" });
const monitor = makeMonitor({ isActive: false, status: "paused" });
await queue.pauseJob(monitor);
expect(scheduler.updateJob).toHaveBeenCalledWith("mon-1", { data: monitor });
expect(scheduler.updateJob).toHaveBeenCalledWith("mon-1-geo", { data: monitor });
});
it("does not pause geo job when it does not exist", async () => {
const { queue, scheduler } = createQueue();
await queue.pauseJob(makeMonitor());
@@ -309,12 +320,26 @@ describe("SuperSimpleQueue", () => {
expect(scheduler.pauseJob).not.toHaveBeenCalledWith("mon-1-geo");
});
it("does not refresh geo data when geo job does not exist", async () => {
const { queue, scheduler } = createQueue();
await queue.pauseJob(makeMonitor());
expect(scheduler.updateJob).toHaveBeenCalledWith("mon-1", expect.objectContaining({ data: expect.anything() }));
expect(scheduler.updateJob).not.toHaveBeenCalledWith("mon-1-geo", expect.anything());
});
it("throws when scheduler.pauseJob returns false for the main job", async () => {
const { queue, scheduler } = createQueue();
(scheduler.pauseJob as jest.Mock).mockResolvedValue(false);
await expect(queue.pauseJob(makeMonitor())).rejects.toThrow("Failed to pause monitor");
});
it("does not refresh data when main pauseJob throws", async () => {
const { queue, scheduler } = createQueue();
(scheduler.pauseJob as jest.Mock).mockResolvedValue(false);
await expect(queue.pauseJob(makeMonitor())).rejects.toThrow();
expect(scheduler.updateJob).not.toHaveBeenCalled();
});
it("logs error but does not throw when geo pauseJob returns false", async () => {
const { queue, scheduler, logger } = createQueue();
(scheduler.getJob as jest.Mock).mockResolvedValue({ id: "mon-1-geo" });
@@ -338,6 +363,17 @@ describe("SuperSimpleQueue", () => {
expect(logger.debug).toHaveBeenCalledWith(expect.objectContaining({ message: "Resumed monitor mon-1" }));
});
it("refreshes scheduler data with current monitor on resume", async () => {
const { queue, scheduler } = createQueue();
(scheduler.getJob as jest.Mock).mockResolvedValue({ id: "mon-1-geo" });
const monitor = makeMonitor({ isActive: true, status: "initializing" });
await queue.resumeJob(monitor);
expect(scheduler.updateJob).toHaveBeenCalledWith("mon-1", { data: monitor });
expect(scheduler.updateJob).toHaveBeenCalledWith("mon-1-geo", { data: monitor });
});
it("does not resume geo job when it does not exist", async () => {
const { queue, scheduler } = createQueue();
await queue.resumeJob(makeMonitor());
@@ -345,12 +381,26 @@ describe("SuperSimpleQueue", () => {
expect(scheduler.resumeJob).not.toHaveBeenCalledWith("mon-1-geo");
});
it("does not refresh geo data when geo job does not exist", async () => {
const { queue, scheduler } = createQueue();
await queue.resumeJob(makeMonitor());
expect(scheduler.updateJob).toHaveBeenCalledWith("mon-1", expect.objectContaining({ data: expect.anything() }));
expect(scheduler.updateJob).not.toHaveBeenCalledWith("mon-1-geo", expect.anything());
});
it("throws when scheduler.resumeJob returns false for the main job", async () => {
const { queue, scheduler } = createQueue();
(scheduler.resumeJob as jest.Mock).mockResolvedValue(false);
await expect(queue.resumeJob(makeMonitor())).rejects.toThrow("Failed to resume monitor");
});
it("does not refresh data when main resumeJob throws", async () => {
const { queue, scheduler } = createQueue();
(scheduler.resumeJob as jest.Mock).mockResolvedValue(false);
await expect(queue.resumeJob(makeMonitor())).rejects.toThrow();
expect(scheduler.updateJob).not.toHaveBeenCalled();
});
it("logs error but does not throw when geo resumeJob returns false", async () => {
const { queue, scheduler, logger } = createQueue();
(scheduler.getJob as jest.Mock).mockResolvedValue({ id: "mon-1-geo" });
@@ -507,6 +557,7 @@ describe("SuperSimpleQueue", () => {
{
id: "m1",
active: true,
repeat: 60000,
lockedAt: null,
runCount: 5,
failCount: 1,
@@ -514,7 +565,7 @@ describe("SuperSimpleQueue", () => {
lastRunAt: 1000,
lastFinishedAt: 1100,
lastFailedAt: 900,
data: { url: "http://a.com", type: "http", interval: 60000 },
data: { url: "http://a.com", type: "http", interval: 60000, geoCheckInterval: 300000, isActive: true },
},
]);
const jobs = await queue.getJobs();
@@ -525,7 +576,10 @@ describe("SuperSimpleQueue", () => {
monitorUrl: "http://a.com",
monitorType: "http",
monitorInterval: 60000,
monitorGeoInterval: 300000,
monitorActive: true,
active: true,
repeat: 60000,
lockedAt: null,
runCount: 5,
failCount: 1,
@@ -553,6 +607,30 @@ describe("SuperSimpleQueue", () => {
expect(jobs[0].monitorUrl).toBeNull();
expect(jobs[0].monitorType).toBeNull();
expect(jobs[0].monitorInterval).toBeNull();
expect(jobs[0].monitorGeoInterval).toBeNull();
expect(jobs[0].monitorActive).toBeNull();
expect(jobs[0].repeat).toBeNull();
});
it("exposes monitor.isActive as monitorActive in the summary", async () => {
const { queue, scheduler } = createQueue();
(scheduler.getJobs as jest.Mock).mockResolvedValue([
{ id: "m1", active: true, data: { url: "a", type: "http", interval: 60000, isActive: true } },
{ id: "m2", active: false, data: { url: "b", type: "http", interval: 60000, isActive: false } },
]);
const jobs = await queue.getJobs();
expect(jobs[0].monitorActive).toBe(true);
expect(jobs[1].monitorActive).toBe(false);
});
it("exposes geo check interval and repeat", async () => {
const { queue, scheduler } = createQueue();
(scheduler.getJobs as jest.Mock).mockResolvedValue([
{ id: "m1", active: true, repeat: 120000, data: { url: "a", type: "http", interval: 60000, geoCheckInterval: 600000 } },
]);
const jobs = await queue.getJobs();
expect(jobs[0].repeat).toBe(120000);
expect(jobs[0].monitorGeoInterval).toBe(600000);
});
});