diff --git a/client/src/Pages/Queue/components/FailedJobTable/index.jsx b/client/src/Pages/Queue/components/FailedJobTable/index.jsx
new file mode 100644
index 000000000..d7284f9da
--- /dev/null
+++ b/client/src/Pages/Queue/components/FailedJobTable/index.jsx
@@ -0,0 +1,82 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import DataTable from "../../../../Components/Table";
+
+import { useNavigate } from "react-router-dom";
+import { useTheme } from "@emotion/react";
+import { TypeToPathMap } from "../../../../Utils/monitorUtils";
+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 (
+
+ {t("queuePage.failedJobTable.title")}
+ {
+ const path = TypeToPathMap[row.monitorType];
+ navigate(`/${path}/${row.monitorId}`);
+ },
+ rowSX: {
+ cursor: "pointer",
+ "&:hover td": {
+ backgroundColor: theme.palette.tertiary.main,
+ transition: "background-color .3s ease",
+ },
+ },
+ }}
+ />
+
+ );
+};
+
+FailedJobTable.propTypes = {
+ metrics: PropTypes.object,
+};
+
+export default FailedJobTable;
diff --git a/client/src/Pages/Queue/components/JobTable/index.jsx b/client/src/Pages/Queue/components/JobTable/index.jsx
new file mode 100644
index 000000000..3bce34fff
--- /dev/null
+++ b/client/src/Pages/Queue/components/JobTable/index.jsx
@@ -0,0 +1,168 @@
+import Stack from "@mui/material/Stack";
+import DataTable from "../../../../Components/Table";
+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/monitorUtils";
+import { useTranslation } from "react-i18next";
+
+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 headers = [
+ {
+ id: "id",
+ content: t("queuePage.jobTable.idHeader"),
+ render: (row) => {
+ return row.monitorId;
+ },
+ getCellSx: (row) => {
+ return buildSx(row);
+ },
+ },
+ {
+ id: "url",
+ content: t("queuePage.jobTable.urlHeader"),
+ render: (row) => {
+ return row.monitorUrl;
+ },
+ getCellSx: (row) => {
+ return buildSx(row);
+ },
+ },
+ {
+ id: "type",
+ content: t("queuePage.jobTable.typeHeader"),
+ render: (row) => {
+ return row.monitorType;
+ },
+ getCellSx: (row) => {
+ return buildSx(row);
+ },
+ },
+ {
+ id: "active",
+ content: t("queuePage.jobTable.activeHeader"),
+ render: (row) => {
+ return row.active.toString();
+ },
+ getCellSx: (row) => {
+ return buildSx(row);
+ },
+ },
+ {
+ id: "runCount",
+ content: t("queuePage.jobTable.runCountHeader"),
+ render: (row) => {
+ return row.runCount;
+ },
+ getCellSx: (row) => {
+ return buildSx(row);
+ },
+ },
+ {
+ id: "failCount",
+ content: t("queuePage.jobTable.failCountHeader"),
+ render: (row) => {
+ return row.failCount;
+ },
+ getCellSx: (row) => {
+ return buildSx(row);
+ },
+ },
+ {
+ id: "lastRun",
+ content: t("queuePage.jobTable.lastRunHeader"),
+ render: (row) => {
+ return row.lastRunAt || "-";
+ },
+ getCellSx: (row) => {
+ return buildSx(row);
+ },
+ },
+ {
+ id: "lockedAt",
+ content: t("queuePage.jobTable.lockedAtHeader"),
+ render: (row) => {
+ return row.lockedAt || "-";
+ },
+ getCellSx: (row) => {
+ return buildSx(row);
+ },
+ },
+ {
+ id: "lastFinish",
+ content: t("queuePage.jobTable.lastFinishedAtHeader"),
+ render: (row) => {
+ return row.lastFinishedAt || "-";
+ },
+ getCellSx: (row) => {
+ return buildSx(row);
+ },
+ },
+ {
+ id: "lastRunTook",
+ content: t("queuePage.jobTable.lastRunTookHeader"),
+ render: (row) => {
+ const value = row.lastRunTook ? row.lastRunTook + " ms" : "-";
+ return value;
+ },
+ getCellSx: (row) => {
+ return buildSx(row);
+ },
+ },
+ ];
+
+ return (
+
+ {t("queuePage.jobTable.title")}
+ {
+ const path = TypeToPathMap[row.monitorType];
+ navigate(`/${path}/${row.monitorId}`);
+ },
+ rowSX: {
+ cursor: "pointer",
+ "&:hover td": {
+ backgroundColor: theme.palette.tertiary.main,
+ transition: "background-color .3s ease",
+ },
+ },
+ }}
+ />
+
+ );
+};
+
+JobTable.propTypes = {
+ jobs: PropTypes.array,
+};
+
+export default JobTable;
diff --git a/client/src/Pages/Queue/components/MetricsTable/index.jsx b/client/src/Pages/Queue/components/MetricsTable/index.jsx
new file mode 100644
index 000000000..818b4e543
--- /dev/null
+++ b/client/src/Pages/Queue/components/MetricsTable/index.jsx
@@ -0,0 +1,60 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import DataTable from "../../../../Components/Table";
+
+// 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 {row.key};
+ },
+ },
+ {
+ id: "value",
+ content: t("queuePage.metricsTable.valueHeader"),
+ render: (row) => {
+ return {row.value};
+ },
+ },
+ ];
+
+ const data = keys
+ .filter((key) => key !== "jobsWithFailures")
+ .map((key) => {
+ return { key: camelToTitle(key), value: metrics[key] };
+ });
+
+ return (
+
+ {t("queuePage.metricsTable.title")}
+
+
+ );
+};
+
+Metrics.propTypes = {
+ metrics: PropTypes.object,
+};
+
+export default Metrics;
diff --git a/client/src/Pages/Queue/index.jsx b/client/src/Pages/Queue/index.jsx
new file mode 100644
index 000000000..eb732560c
--- /dev/null
+++ b/client/src/Pages/Queue/index.jsx
@@ -0,0 +1,69 @@
+// Components
+import Stack from "@mui/material/Stack";
+import Breadcrumbs from "../../Components/Breadcrumbs";
+import JobTable from "./components/JobTable";
+import MetricsTable from "./components/MetricsTable";
+import FailedJobTable from "./components/FailedJobTable";
+import ButtonGroup from "@mui/material/ButtonGroup";
+import Button from "@mui/material/Button";
+
+// Utils
+import { useState } from "react";
+import { useFetchQueueData, useFlushQueue } from "../../Hooks/queueHooks";
+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();
+
+ const BREADCRUMBS = [{ name: t("queuePage.title"), path: "/queue" }];
+ if (isLoading) return
Loading...
;
+ if (error || flushError) return Error: {error.message}
;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default QueueDetails;