From e610acb9c7ee7709002762eb0c75cb758963b307 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 20 Jun 2025 14:27:50 +0800 Subject: [PATCH] Add Queue page and components --- .../Queue/components/FailedJobTable/index.jsx | 82 +++++++++ .../Pages/Queue/components/JobTable/index.jsx | 168 ++++++++++++++++++ .../Queue/components/MetricsTable/index.jsx | 60 +++++++ client/src/Pages/Queue/index.jsx | 69 +++++++ 4 files changed, 379 insertions(+) create mode 100644 client/src/Pages/Queue/components/FailedJobTable/index.jsx create mode 100644 client/src/Pages/Queue/components/JobTable/index.jsx create mode 100644 client/src/Pages/Queue/components/MetricsTable/index.jsx create mode 100644 client/src/Pages/Queue/index.jsx 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;