Merge pull request #2513 from bluewave-labs/feat/queue-page-update

feat: move queue to logs page
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-06-23 20:10:46 +08:00
commit b6680be345
23 files changed
+641 -232

No files matched your search

+2 -1
View File
@@ -8,7 +8,6 @@ import {
ListItemButton,
ListItemIcon,
ListItemText,
ListSubheader,
Menu,
MenuItem,
Stack,
@@ -39,6 +38,7 @@ import Docs from "../../assets/icons/docs.svg?react";
import StatusPages from "../../assets/icons/status-pages.svg?react";
import Discussions from "../../assets/icons/discussions.svg?react";
import Notifications from "../../assets/icons/notifications.svg?react";
import Logs from "../../assets/icons/logs.svg?react";
import "./index.css";
@@ -64,6 +64,7 @@ const getMenu = (t) => [
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
{ name: t("menu.logs"), path: "logs", icon: <Logs /> },
{
name: t("menu.settings"),
+1 -1
View File
@@ -4,7 +4,7 @@ import SkeletonLayout from "./skeleton";
// Utils
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
const StatusBoxes = ({ shouldRender, flexWrap = "nowrap", children }) => {
const StatusBoxes = ({ shouldRender = true, flexWrap = "nowrap", children }) => {
const theme = useTheme();
if (!shouldRender) {
return (
+35
View File
@@ -0,0 +1,35 @@
import { useState, useEffect } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
const useFetchLogs = () => {
const { t } = useTranslation();
const [logs, setLogs] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
useEffect(() => {
const fetchLogs = async () => {
try {
setIsLoading(true);
const response = await networkService.getLogs();
setLogs(response.data.data);
createToast({
body: t("logsPage.toast.fetchLogsSuccess"),
});
} catch (error) {
setError(error);
createToast({
message: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchLogs();
}, [t]);
return [logs, isLoading, error];
};
export { useFetchLogs };
+97
View File
@@ -0,0 +1,97 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Select from "../../../Components/Inputs/Select";
import Typography from "@mui/material/Typography";
import { useFetchLogs } from "../../../Hooks/logHooks";
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
const formatLog = (theme, log, idx) => {
const levelColors = {
info: theme.palette.success.main,
warn: theme.palette.warning.main,
error: theme.palette.error.main,
debug: theme.palette.accent.main,
};
const color = levelColors[log.level] || theme.palette.primary.contrastText;
return (
<span key={idx}>
<span>[{log.timestamp}]</span>{" "}
<span style={{ color, fontWeight: "bold" }}>{log.level.toUpperCase()}</span>
{": "}
{`(${log.service})`}
{`(${log.method})`}
{": "}
{log.message}
<br />
</span>
);
};
const Logs = () => {
// Local state
const [logLevel, setLogLevel] = useState("all");
// Hooks
const theme = useTheme();
const { t } = useTranslation();
const [logs, isLoading, error] = useFetchLogs();
// Setup
const LOG_LEVELS = [
{ _id: "all", name: t("logsPage.logLevelSelect.values.all") },
{ _id: "info", name: t("logsPage.logLevelSelect.values.info") },
{ _id: "warn", name: t("logsPage.logLevelSelect.values.warn") },
{ _id: "error", name: t("logsPage.logLevelSelect.values.error") },
{ _id: "debug", name: t("logsPage.logLevelSelect.values.debug") },
];
return (
<Stack gap={theme.spacing(4)}>
<Box>
<Typography variant="body">{t("logsPage.description")}</Typography>
</Box>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
<Typography>{t("logsPage.logLevelSelect.title")}</Typography>
<Select
items={LOG_LEVELS}
value={logLevel}
onChange={(e) => {
setLogLevel(e.target.value);
}}
/>
</Stack>
<Box
component="pre"
sx={{
fontFamily: "monospace",
color: theme.palette.primary.contrastText,
padding: 2,
borderRadius: 1,
overflowX: "auto",
whiteSpace: "pre-wrap",
wordWrap: "break-word",
}}
>
<code>
{logs
?.filter((log) => {
if (logLevel === "all") return true;
return log.level === logLevel;
})
.reverse()
.map((log, idx) => formatLog(theme, log, idx))}
</code>
</Box>
</Stack>
);
};
export default Logs;
@@ -1,10 +1,10 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import DataTable from "../../../../Components/Table";
import DataTable from "../../../../../Components/Table";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { TypeToPathMap } from "../../../../Utils/monitorUtils";
import { TypeToPathMap } from "../../../../../Utils/monitorUtils";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
@@ -1,13 +1,13 @@
import Stack from "@mui/material/Stack";
import DataTable from "../../../../Components/Table";
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 { TypeToPathMap } from "../../../../../Utils/monitorUtils";
import { useTranslation } from "react-i18next";
import { createHeaderFactory } from "../../../../Components/Table/TableUtils";
import { createHeaderFactory } from "../../../../../Components/Table/TableUtils";
const JobTable = ({ jobs = [] }) => {
const theme = useTheme();
@@ -0,0 +1,43 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import StatBox from "../../../../../Components/StatBox";
import StatusBoxes from "../../../../../Components/StatusBoxes";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
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 data = Object.keys(metrics)
.filter((key) => key !== "jobsWithFailures")
.map((key) => {
return { key, title: camelToTitle(key), value: metrics[key] };
});
return (
<Stack gap={theme.spacing(2)}>
<Typography variant="h2">{t("queuePage.metricsTable.title")}</Typography>
<StatusBoxes flexWrap="wrap">
{data.map((metric) => {
return (
<StatBox
key={metric.key}
heading={metric.title}
subHeading={metric.value}
/>
);
})}
</StatusBoxes>
</Stack>
);
};
export default Metrics;
@@ -1,6 +1,6 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import DataTable from "../../../../Components/Table";
import DataTable from "../../../../../Components/Table";
// Utils
import { useTranslation } from "react-i18next";
@@ -1,15 +1,14 @@
// Components
import Stack from "@mui/material/Stack";
import Breadcrumbs from "../../Components/Breadcrumbs";
import JobTable from "./components/JobTable";
import MetricsTable from "./components/MetricsTable";
import Metrics from "./components/Metrics";
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 { useFetchQueueData, useFlushQueue } from "../../../Hooks/queueHooks";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
@@ -23,15 +22,13 @@ const QueueDetails = () => {
const [jobs, metrics, isLoading, error] = useFetchQueueData(trigger);
const [flushQueue, isFlushing, flushError] = useFlushQueue();
const BREADCRUMBS = [{ name: t("queuePage.title"), path: "/queue" }];
if (isLoading) return <div>Loading...</div>;
if (error || flushError) return <div>Error: {error.message}</div>;
return (
<Stack gap={theme.spacing(20)}>
<Breadcrumbs list={BREADCRUMBS} />
<Metrics metrics={metrics} />
<JobTable jobs={jobs} />
<MetricsTable metrics={metrics} />
<FailedJobTable metrics={metrics} />
<ButtonGroup
+41
View File
@@ -0,0 +1,41 @@
import Stack from "@mui/material/Stack";
import Breadcrumbs from "../../Components/Breadcrumbs";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Queue from "./Queue";
import LogsComponent from "./Logs";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useState } from "react";
const Logs = () => {
const { t } = useTranslation();
const theme = useTheme();
// Local state
const [value, setValue] = useState(0);
// Handlers
const handleChange = (event, newValue) => {
setValue(newValue);
};
const BREADCRUMBS = [{ name: t("logsPage.title"), path: "/logs" }];
return (
<Stack gap={theme.spacing(20)}>
<Breadcrumbs list={BREADCRUMBS} />
<Tabs
value={value}
onChange={handleChange}
>
<Tab label={t("logsPage.tabs.logs")} />
<Tab label={t("logsPage.tabs.queue")} />
</Tabs>
{value === 0 && <LogsComponent />}
{value === 1 && <Queue />}
</Stack>
);
};
export default Logs;
-37
View File
@@ -1,37 +0,0 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import ConfigBox from "../../Components/ConfigBox";
// Utils
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import Link from "../../Components/Link";
const SettingsDev = ({ isAdmin, HEADER_SX }) => {
const { t } = useTranslation();
if (!isAdmin) return null;
return (
<ConfigBox>
<Box>
<Typography
component="h1"
variant="h2"
>
{t("settingsDev")}
</Typography>
<Typography sx={HEADER_SX}>{t("settingsDevDescription")}</Typography>
</Box>
<Box>
<Link
level="secondary"
label={t("settingsDevViewJobQueueDetails")}
url="/queue"
external={false}
/>
</Box>
</ConfigBox>
);
};
export default SettingsDev;
+1 -5
View File
@@ -8,7 +8,6 @@ import SettingsPagespeed from "./SettingsPagespeed";
import SettingsDemoMonitors from "./SettingsDemoMonitors";
import SettingsAbout from "./SettingsAbout";
import SettingsEmail from "./SettingsEmail";
import SettingsDev from "./SettingsDev";
import Button from "@mui/material/Button";
// Utils
import { settingsValidation } from "../../Validation/validation";
@@ -209,10 +208,7 @@ const Settings = () => {
emailPasswordHasBeenReset={emailPasswordHasBeenReset}
setEmailPasswordHasBeenReset={setEmailPasswordHasBeenReset}
/>
<SettingsDev
isAdmin={isAdmin}
HEADER_SX={HEADING_SX}
/>
<SettingsAbout />
<Stack
direction="row"
+4 -3
View File
@@ -51,7 +51,7 @@ import ProtectedRoute from "../Components/ProtectedRoute";
import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance";
import withAdminCheck from "../Components/HOC/withAdminCheck";
import BulkImport from "../Pages/Uptime/BulkImport";
import Queue from "../Pages/Queue";
import Logs from "../Pages/Logs";
const Routes = () => {
const AdminCheckedRegister = withAdminCheck(AuthRegister);
@@ -187,9 +187,10 @@ const Routes = () => {
path="account/team"
element={<Account open={"team"} />}
/>
<Route
path="queue"
element={<Queue />}
path="logs"
element={<Logs />}
/>
</Route>
+8
View File
@@ -1044,6 +1044,14 @@ class NetworkService {
});
return response;
}
async getLogs() {
return this.axiosInstance.get(`/logs`, {
headers: {
"Content-Type": "application/json",
},
});
}
}
export default NetworkService;
+3
View File
@@ -407,6 +407,7 @@ const baseTheme = (palette) => ({
}),
},
},
MuiTab: {
styleOverrides: {
root: ({ theme }) => ({
@@ -475,6 +476,8 @@ const baseTheme = (palette) => ({
MuiTabs: {
styleOverrides: {
root: ({ theme }) => ({
height: "34px",
minHeight: "34px",
display: "inline-flex",
borderRadius: 0,
"& .MuiTabs-indicator": {
+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M21 5C21 6.65685 16.9706 8 12 8C7.02944 8 3 6.65685 3 5M21 5C21 3.34315 16.9706 2 12 2C7.02944 2 3 3.34315 3 5M21 5V19C21 20.66 17 22 12 22C7 22 3 20.66 3 19V5M21 12C21 13.66 17 15 12 15C7 15 3 13.66 3 12"
stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 421 B

+64 -1
View File
@@ -429,6 +429,7 @@
"statusPages": "Status pages",
"maintenance": "Maintenance",
"integrations": "Integrations",
"logs": "Logs",
"settings": "Settings",
"support": "Support",
"discussions": "Discussions",
@@ -755,5 +756,67 @@
"settingsEmailSecure": "Secure - Use SSL",
"settingsEmailPool": "Pool - Enable connection pooling",
"sendTestNotifications": "Send test notifications",
"selectAll": "Select all"
"selectAll": "Select all",
"logsPage": {
"title": "Logs",
"description": "This page shows the latest 1000 lines of logs from the Checkmate server",
"tabs": {
"queue": "Job queue",
"logs": "Server logs"
},
"toast": {
"fetchLogsSuccess": "Logs fetched successfully"
},
"logLevelSelect": {
"title": "Log level",
"values": {
"all": "All",
"info": "Info",
"warn": "Warn",
"error": "Error",
"debug": "Debug"
}
}
},
"queuePage": {
"title": "Queue",
"refreshButton": "Refresh",
"flushButton": "Flush queue",
"jobTable": {
"title": "Jobs currently in queue",
"idHeader": "Monitor ID",
"urlHeader": "URL",
"typeHeader": "Type",
"activeHeader": "Active",
"lockedAtHeader": "Locked at",
"runCountHeader": "Run count",
"failCountHeader": "Fail count",
"lastRunHeader": "Last run at",
"lastFinishedAtHeader": "Last finished at",
"lastRunTookHeader": "Last run took"
},
"metricsTable": {
"title": "Queue metrics",
"metricHeader": "Metric",
"valueHeader": "Value"
},
"failedJobTable": {
"title": "Failed jobs",
"monitorIdHeader": "Monitor ID",
"monitorUrlHeader": "Monitor URL",
"failCountHeader": "Fail count",
"failedAtHeader": "Last failed at",
"failReasonHeader": "Fail reason"
}
},
"export": {
"title": "Export Monitors",
"success": "Monitors exported successfully!",
"failed": "Failed to export monitors"
},
"monitorActions": {
"title": "Export/Import",
"import": "Import Monitors",
"export": "Export Monitors"
}
}