mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-19 07:58:46 -05:00
logs tab
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const TabDiagnostics = () => {
|
||||
return "Diagnostics";
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const TabQueue = () => {
|
||||
return "Queue";
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -19,6 +19,9 @@ export interface UIState {
|
||||
infrastructure: {
|
||||
rowsPerPage: number;
|
||||
};
|
||||
logs: {
|
||||
rowsPerPage: number;
|
||||
};
|
||||
sidebar: {
|
||||
collapsed: boolean;
|
||||
};
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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: "/",
|
||||
|
||||
Reference in New Issue
Block a user