diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js index 65aff99a8..014940a9a 100644 --- a/client/src/Features/UI/uiSlice.js +++ b/client/src/Features/UI/uiSlice.js @@ -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; diff --git a/client/src/Pages/Logs/TabDiagnostics.tsx b/client/src/Pages/Logs/TabDiagnostics.tsx new file mode 100644 index 000000000..92b8801e0 --- /dev/null +++ b/client/src/Pages/Logs/TabDiagnostics.tsx @@ -0,0 +1,3 @@ +export const TabDiagnostics = () => { + return "Diagnostics"; +}; diff --git a/client/src/Pages/Logs/TabLogs.tsx b/client/src/Pages/Logs/TabLogs.tsx new file mode 100644 index 000000000..b4d601429 --- /dev/null +++ b/client/src/Pages/Logs/TabLogs.tsx @@ -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("/logs"); + const [selectedLogLevel, setSelectedLogLevel] = useState("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 ( + + + + + ); +}; diff --git a/client/src/Pages/Logs/TabQueue.tsx b/client/src/Pages/Logs/TabQueue.tsx new file mode 100644 index 000000000..e327218d3 --- /dev/null +++ b/client/src/Pages/Logs/TabQueue.tsx @@ -0,0 +1,3 @@ +export const TabQueue = () => { + return "Queue"; +}; diff --git a/client/src/Pages/Logs/TableLogs.tsx b/client/src/Pages/Logs/TableLogs.tsx new file mode 100644 index 000000000..e565739bd --- /dev/null +++ b/client/src/Pages/Logs/TableLogs.tsx @@ -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 = { + 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 ( + + {level} + + ); +}; + +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[] = [ + { + id: "timestamp", + content: t("pages.logs.table.headers.timestamp"), + render: (row) => ( + + {formatTimestamp(row.timestamp)} + + ), + }, + { + id: "level", + content: t("pages.logs.table.headers.level"), + render: (row) => , + }, + { + id: "service", + content: t("pages.logs.table.headers.service"), + render: (row) => ( + {row.service || "-"} + ), + }, + { + id: "method", + content: t("pages.logs.table.headers.method"), + render: (row) => ( + + {row.method || "-"} + + ), + }, + { + id: "message", + content: t("common.table.headers.message"), + render: (row) => ( + + {row.message || "-"} + + ), + }, + ]; + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + dispatch(setRowsPerPage({ value: Number(e.target.value), table: "logs" })); + setPage(0); + }; + + return ( + + + + + ); +}; diff --git a/client/src/Pages/Logs/index.tsx b/client/src/Pages/Logs/index.tsx new file mode 100644 index 000000000..2642e177b --- /dev/null +++ b/client/src/Pages/Logs/index.tsx @@ -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(0); + return ( + + setActiveTab(newValue)} + > + + + + + {activeTab === 0 && } + {activeTab === 1 && } + {activeTab === 2 && } + + ); +}; + +export default LogsPage; diff --git a/client/src/Pages/Logs/index.jsx b/client/src/Pages/Logs/old.jsx similarity index 100% rename from client/src/Pages/Logs/index.jsx rename to client/src/Pages/Logs/old.jsx diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index d4a5619d6..22b5b446a 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -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={ - + + + } /> diff --git a/client/src/Types/Log.ts b/client/src/Types/Log.ts new file mode 100644 index 000000000..b2038e31f --- /dev/null +++ b/client/src/Types/Log.ts @@ -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; + stack?: string; + level: LogLevel; + timestamp: string; +} diff --git a/client/src/Types/state.ts b/client/src/Types/state.ts index 137d67b66..1575fffb8 100644 --- a/client/src/Types/state.ts +++ b/client/src/Types/state.ts @@ -19,6 +19,9 @@ export interface UIState { infrastructure: { rowsPerPage: number; }; + logs: { + rowsPerPage: number; + }; sidebar: { collapsed: boolean; }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 644057425..6ec628238 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -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!", diff --git a/client/vite.config.ts b/client/vite.config.ts index 3d8d66335..f381eb5ed 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -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: "/",