This commit is contained in:
Alex Holliday
2026-02-05 20:23:04 +00:00
parent 3b25b37c22
commit a148f56b22
12 changed files with 291 additions and 5 deletions
+6 -2
View File
@@ -19,6 +19,9 @@ const initialState = {
infrastructure: {
rowsPerPage: 5,
},
logs: {
rowsPerPage: 15,
},
sidebar: {
collapsed: false,
},
@@ -41,9 +44,10 @@ const uiSlice = createSlice({
},
setRowsPerPage: (state, action) => {
const { table, value } = action.payload;
if (state[table]) {
state[table].rowsPerPage = value;
if (!state[table]) {
state[table] = {};
}
state[table].rowsPerPage = value;
},
toggleSidebar: (state) => {
state.sidebar.collapsed = !state.sidebar.collapsed;
+3
View File
@@ -0,0 +1,3 @@
export const TabDiagnostics = () => {
return "Diagnostics";
};
+63
View File
@@ -0,0 +1,63 @@
import Stack from "@mui/material/Stack";
import MenuItem from "@mui/material/MenuItem";
import Typography from "@mui/material/Typography";
import { useSelector } from "react-redux";
import { Select } from "@/Components/v2/inputs";
import { TableLogs } from "./TableLogs";
import { useTheme } from "@mui/material";
import { useGet } from "@/Hooks/UseApi";
import { useState } from "react";
import type { Log, LogLevelOption } from "@/Types/Log";
import { LOG_LEVEL_OPTIONS } from "@/Types/Log";
import { t } from "i18next";
import type { RootState } from "@/Types/state";
export const TabLogs = () => {
const theme = useTheme();
const { data: logs } = useGet<Log[]>("/logs");
const [selectedLogLevel, setSelectedLogLevel] = useState<LogLevelOption>("all");
const [page, setPage] = useState(0);
const rowsPerPage = useSelector(
(state: RootState) => state?.ui?.logs?.rowsPerPage ?? 15
);
const filteredLogs = logs
?.filter((log) => {
if (selectedLogLevel === "all") return true;
return log.level === selectedLogLevel;
})
.reverse()
.map((log, idx) => ({ ...log, id: idx }));
const paginatedLogs =
filteredLogs?.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) ?? [];
return (
<Stack gap={theme.spacing(8)}>
<Select
sx={{ maxWidth: 200 }}
fieldLabel={t("pages.logs.logLevelSelect.label")}
value={selectedLogLevel}
onChange={(e) => setSelectedLogLevel(e.target.value)}
>
{LOG_LEVEL_OPTIONS.map((level) => {
return (
<MenuItem
key={level}
value={level}
>
<Typography textTransform={"capitalize"}>{level}</Typography>
</MenuItem>
);
})}
</Select>
<TableLogs
logs={paginatedLogs}
logCount={filteredLogs?.length ?? 0}
page={page}
setPage={setPage}
/>
</Stack>
);
};
+3
View File
@@ -0,0 +1,3 @@
export const TabQueue = () => {
return "Queue";
};
+140
View File
@@ -0,0 +1,140 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { Table } from "@/Components/v2/design-elements";
import { Pagination } from "@/Components/v2/design-elements/Table";
import { useTheme } from "@mui/material";
import type { Header } from "@/Components/v2/design-elements/Table";
import type { Log, LogLevel } from "@/Types/Log";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
import type { RootState } from "@/Types/state";
import { setRowsPerPage } from "@/Features/UI/uiSlice";
type LogWithId = Log & { id: number };
interface TableLogsProps {
logs: LogWithId[];
logCount: number;
page: number;
setPage: (page: number) => void;
}
const LevelBadge = ({ level }: { level: LogLevel }) => {
const theme = useTheme();
const levelColors: Record<LogLevel, string> = {
info: theme.palette.success.main,
warn: theme.palette.warning.main,
error: theme.palette.error.main,
debug: theme.palette.info.main,
};
const color = levelColors[level] || theme.palette.text.primary;
return (
<Typography
fontWeight={600}
color={color}
textTransform={"uppercase"}
>
{level}
</Typography>
);
};
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();
const rowsPerPage = useSelector(
(state: RootState) => state?.ui?.logs?.rowsPerPage ?? 15
);
const headers: Header<Log & { id: number }>[] = [
{
id: "timestamp",
content: t("pages.logs.table.headers.timestamp"),
render: (row) => (
<Typography sx={{ fontSize: 13, fontFamily: "monospace" }}>
{formatTimestamp(row.timestamp)}
</Typography>
),
},
{
id: "level",
content: t("pages.logs.table.headers.level"),
render: (row) => <LevelBadge level={row.level} />,
},
{
id: "service",
content: t("pages.logs.table.headers.service"),
render: (row) => (
<Typography sx={{ fontSize: 13 }}>{row.service || "-"}</Typography>
),
},
{
id: "method",
content: t("pages.logs.table.headers.method"),
render: (row) => (
<Typography sx={{ fontSize: 13, fontFamily: "monospace" }}>
{row.method || "-"}
</Typography>
),
},
{
id: "message",
content: t("common.table.headers.message"),
render: (row) => (
<Typography
sx={{
fontSize: 13,
maxWidth: 400,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{row.message || "-"}
</Typography>
),
},
];
const handlePageChange = (
_e: React.MouseEvent<HTMLButtonElement> | null,
newPage: number
) => {
setPage(newPage);
};
const handleRowsPerPageChange = (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => {
dispatch(setRowsPerPage({ value: Number(e.target.value), table: "logs" }));
setPage(0);
};
return (
<Box>
<Table
headers={headers}
data={logs}
emptyViewText={t("pages.logs.noLogs")}
/>
<Pagination
itemsOnPage={logs.length}
component="div"
count={logCount}
page={page}
rowsPerPage={rowsPerPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handleRowsPerPageChange}
/>
</Box>
);
};
+29
View File
@@ -0,0 +1,29 @@
import { BasePage, Tabs, Tab } from "@/Components/v2/design-elements";
import { TabLogs } from "@/Pages/Logs/TabLogs";
import { TabQueue } from "@/Pages/Logs/TabQueue";
import { TabDiagnostics } from "@/Pages/Logs/TabDiagnostics";
import { useState } from "react";
import { useTranslation } from "react-i18next";
const LogsPage = () => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<number>(0);
return (
<BasePage>
<Tabs
value={activeTab}
onChange={(_, newValue: number) => setActiveTab(newValue)}
>
<Tab label={t("pages.logs.tabs.logs")} />
<Tab label={t("pages.logs.tabs.queue")} />
<Tab label={t("pages.logs.tabs.diagnostics")} />
</Tabs>
{activeTab === 0 && <TabLogs />}
{activeTab === 1 && <TabQueue />}
{activeTab === 2 && <TabDiagnostics />}
</BasePage>
);
};
export default LogsPage;
+4 -2
View File
@@ -55,7 +55,7 @@ import ProtectedRoute from "../Components/v1/ProtectedRoute";
import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute";
import withAdminCheck from "@/Components/v1/HOC/withAdminCheck";
import BulkImport from "../Pages/Uptime/BulkImport/index.jsx";
import Logs from "../Pages/Logs/index.jsx";
import Logs from "../Pages/Logs";
import CreateMonitor from "@/Pages/CreateMonitor";
@@ -350,7 +350,9 @@ const Routes = () => {
path="logs"
element={
<RoleProtectedRoute roles={["admin", "superadmin"]}>
<Logs />
<ThemeProvider theme={v2theme}>
<Logs />
</ThemeProvider>
</RoleProtectedRoute>
}
/>
+13
View File
@@ -0,0 +1,13 @@
export const LOG_LEVELS = ["debug", "info", "warn", "error"] as const;
export type LogLevel = (typeof LOG_LEVELS)[number];
export const LOG_LEVEL_OPTIONS = ["all", ...LOG_LEVELS] as const;
export type LogLevelOption = (typeof LOG_LEVEL_OPTIONS)[number];
export interface Log {
message: string;
service?: string;
method?: string;
details?: Record<string, unknown>;
stack?: string;
level: LogLevel;
timestamp: string;
}
+3
View File
@@ -19,6 +19,9 @@ export interface UIState {
infrastructure: {
rowsPerPage: number;
};
logs: {
rowsPerPage: number;
};
sidebar: {
collapsed: boolean;
};
+26
View File
@@ -777,6 +777,32 @@
"title": "An infrastructure monitor is used to:"
}
},
"logs": {
"tabs": {
"diagnostics": "Diagnostics",
"logs": "Logs",
"queue": "Job queue"
},
"logLevelSelect": {
"label": "Log level"
},
"noLogs": "No logs found",
"table": {
"headers": {
"timestamp": "Timestamp",
"level": "Level",
"service": "Service",
"method": "Method",
"monitorId": "Monitor ID",
"runCount": "Run count",
"failCount": "Fail count",
"lastRunAt": "Last run at",
"lockedAt": "Locked at",
"lastFinisehdAt": "Last finished at",
"lastRunTook": "Last run took"
}
}
},
"maintenanceWindow": {
"fallback": {
"actionButton": "Let's create your first maintenance window!",
+1 -1
View File
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig(({}) => {
let version = "3.2.1";
let version = "3.3";
return {
base: "/",