Merge pull request #3133 from bluewave-labs/fix/ui-improvements-logs-incidents

Improve UI consistency for Logs and Incidents pages
This commit is contained in:
Gorkem Cetin (BWL) authored and GitHub committed 2026-01-16 14:47:21 -05:00
commit f1a6afb860
11 files changed
+459 -346

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Checkmate is an open-source uptime and infrastructure monitoring application. It monitors server hardware, uptime, response times, and incidents with real-time alerts. The companion agent [Capture](https://github.com/bluewave-labs/capture) provides infrastructure metrics (CPU, RAM, disk, temperature).
## Development Commands
### Client (React/Vite)
```bash
cd client
npm install
npm run dev # Start dev server at http://localhost:5173
npm run build # TypeScript check + production build
npm run lint # ESLint (strict, max-warnings 0)
npm run format # Prettier formatting
npm run format-check # Check formatting
```
### Server (Node.js/Express)
```bash
cd server
npm install
npm run dev # Start with hot-reload (nodemon + tsx) at http://localhost:52345
npm run build # TypeScript compile + path alias resolution
npm run test # Run Mocha tests with c8 coverage
npm run lint # ESLint v9
npm run lint-fix # Auto-fix lint issues
npm run format # Prettier formatting
```
### Docker Development
```bash
cd docker/dev
./build_images.sh
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0
```
## Environment Setup
### Server `.env` (minimum required)
```env
CLIENT_HOST="http://localhost:5173"
JWT_SECRET="my_secret_key_change_this"
DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db"
TOKEN_TTL="99d"
ORIGIN="localhost"
LOG_LEVEL="debug"
```
### Client `.env`
```env
VITE_APP_API_BASE_URL="http://localhost:52345/api/v1"
VITE_APP_LOG_LEVEL="debug"
```
## Architecture
### Monorepo Structure
- `/client` - React 18 + TypeScript + Vite + MUI frontend
- `/server` - Node.js 20+ + Express + TypeScript backend
- `/docker` - Multi-environment Docker configs (dev, staging, prod, arm, mono)
### Backend Layers
```
server/src/
├── controllers/ # Route handlers (authController, monitorController, etc.)
├── service/ # Business logic
│ ├── business/ # Core monitoring logic
│ ├── infrastructure/ # Server/system utilities
│ └── system/ # App-level settings
├── db/
│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.)
│ ├── migration/ # Database migrations (run on startup)
│ └── modules/ # Database-specific modules
├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler
├── routes/v1/ # API route definitions
├── validation/ # Joi input validation schemas
└── repositories/ # Data access layer
```
### Frontend Structure
```
client/src/
├── Components/ # Reusable UI components
├── Pages/ # Page components (Auth, Uptime, Infrastructure, Incidents, etc.)
├── Features/ # Redux slices (Auth, UI)
├── Hooks/ # Custom React hooks
├── Utils/ # Utilities (NetworkService.js is main API client)
├── Validation/ # Input validation
└── locales/ # i18n translations
```
### API
- Base URL: `/api/v1`
- Documentation: `http://localhost:52345/api-docs` (Swagger UI)
- OpenAPI spec: `/server/openapi.json`
### Key Technologies
- **State Management**: Redux Toolkit + Redux-Persist
- **Data Fetching**: SWR + Axios
- **Database**: MongoDB with Mongoose ODM
- **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling)
- **i18n**: i18next + react-i18next (translations via PoEditor)
## Code Conventions
### Internationalization
All user-facing strings must use the translation function:
```javascript
t('your.key') // Never hardcode UI strings
```
### Branching
- Always branch from `develop` (not master)
- Use descriptive names: `feat/add-alerts`, `fix/login-error`
- PRs target `develop` branch
### Formatting
- **Client**: Prettier with `printWidth: 90`, tabs, double quotes
- **Server**: Prettier with `printWidth: 150`, tabs, double quotes
- Both use ESLint with strict settings
### Testing
Server tests use Mocha + Chai + Sinon:
```bash
npm test # Run all tests with coverage
npm test -- --grep "pattern" # Run specific tests
```
Test files: `server/tests/**/*.test.js`
## Database Models
Key Mongoose models in `/server/src/db/models/`:
- **Monitor** - Monitoring configuration (website, infrastructure, port, etc.)
- **Check** - Individual monitoring check results
- **Incident** - Downtime incidents
- **User** - User accounts
- **Team** - Team/workspace management
- **StatusPage** - Public status pages
- **Notification** - Alert configuration (email, Discord, Slack, webhooks)
- **MaintenanceWindow** - Scheduled maintenance periods
- **AppSettings** - Global application settings
+5 -5
View File
@@ -23,12 +23,12 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
direction="row"
position="relative"
alignItems="center"
gap={theme.spacing(5)}
gap={theme.spacing(4)}
>
<Typography
variant="h6"
component="span"
sx={{
fontWeight: 600,
fontWeight: 500,
}}
>
{title}
@@ -38,9 +38,9 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
<>
<Dot />
<Typography
variant="h6"
component="span"
sx={{
fontWeight: 600,
fontWeight: 500,
color: percentageColor,
}}
>
@@ -42,25 +42,24 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null
if (!totalCount || totalCount === 0) {
return (
<SummaryCard>
<SummaryCard title={t("incidentsPage.incidentsActivePanelTitle")}>
<Stack
direction="column"
alignItems="center"
justifyContent="center"
padding={theme.spacing(10)}
gap={theme.spacing(4)}
padding={theme.spacing(6)}
gap={theme.spacing(2)}
sx={{ flex: 1 }}
>
<Box
sx={{
color: theme.palette.success.main,
"& svg": {
width: 60,
height: 60,
width: 24,
height: 24,
"& path": { stroke: "currentColor", strokeWidth: 2 },
},
mb: theme.spacing(2),
mb: theme.spacing(1),
}}
>
<Icon
@@ -70,14 +69,12 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null
</Box>
<Typography
variant="h1"
color={theme.palette.primary.contrastTextSecondary}
sx={{
fontSize: 13,
textTransform: "uppercase",
fontWeight: 600,
fontWeight: 500,
textAlign: "center",
color: theme.palette.success.lowContrast,
letterSpacing: theme.spacing(0.4),
}}
>
{t("incidentsPage.allSystemsAreOperational")}
@@ -88,21 +85,20 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null
}
return (
<SummaryCard isHighPriority={true}>
<SummaryCard title={t("incidentsPage.incidentsActivePanelTitle")}>
<Stack
direction="column"
alignItems="center"
justifyContent="center"
spacing={theme.spacing(4)}
gap={theme.spacing(2)}
sx={{ flex: 1 }}
>
<Box
sx={{
color: theme.palette.error.lowContrast,
padding: theme.spacing(2),
"& svg": {
width: 60,
height: 60,
width: 24,
height: 24,
"& path": { stroke: "currentColor", strokeWidth: 2 },
},
}}
@@ -114,27 +110,15 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null
</Box>
<Typography
variant="h1"
sx={{
fontSize: `calc(${theme.typography.h1.fontSize} * 2.5)`,
fontWeight: 700,
fontSize: 32,
fontWeight: 600,
color: theme.palette.error.lowContrast,
lineHeight: 1,
}}
>
{totalCount}
</Typography>
<Typography
variant="h2"
sx={{
textTransform: "uppercase",
fontWeight: 700,
letterSpacing: theme.spacing(0.4),
paddingTop: theme.spacing(3),
}}
>
{t("incidentsPage.incidentsActivePanelTitle")}
</Typography>
</Stack>
</SummaryCard>
);
@@ -53,7 +53,7 @@ const IncidentsSummaryPanel = ({ updateTrigger }) => {
<>
<Grid
container
spacing={3}
spacing={"16px"}
>
<Grid
item
@@ -36,107 +36,68 @@ const IncidentItem = ({ incident }) => {
const duration = calculateDuration();
const iconWrapperStyle = {
px: theme.spacing(2),
display: "flex",
justifyContent: "center",
mx: theme.spacing(2),
color: theme.palette.primary.contrastTextTertiary,
"& svg": {
width: 18,
height: 18,
},
"& svg path": {
stroke: "currentColor",
strokeWidth: 1.5,
},
};
return (
<Box
sx={{
cursor: "pointer",
display: "grid",
gridTemplateColumns: "1fr 100px 80px",
alignItems: "center",
gap: theme.spacing(2),
width: "100%",
py: theme.spacing(0.5),
"&:hover": { opacity: 0.8 },
}}
>
<Stack
direction="column"
gap={theme.spacing(1.5)}
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(1)}
<Box sx={iconWrapperStyle}>
<Icon
name="Globe"
size={18}
/>
</Box>
<Typography
variant="body1"
fontWeight={500}
noWrap
>
<Box
sx={{
...iconWrapperStyle,
}}
>
<Icon
name="Globe"
size={20}
/>
</Box>
<Stack
direction="row"
gap={theme.spacing(4)}
alignItems="baseline"
>
<Typography variant="body1">
{t("incidentsPage.incidentItemMonitor")}:
</Typography>
<Typography
variant="body1"
fontWeight={600}
>
{incident.monitorName || t("incidentsPage.unknownMonitor")}
</Typography>
</Stack>
</Stack>
<Stack
direction="row"
alignItems="center"
width="100%"
marginTop={theme.spacing(1)}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(3)}
>
<Box sx={{ ...iconWrapperStyle }}>
<Icon
name="Wifi"
size={20}
/>
</Box>
<Stack
direction="row"
alignItems="center"
spacing={2}
>
<Typography variant="body1">
{t("incidentsPage.incidentItemStatus")}:
</Typography>
</Stack>
<StatusLabel
status={isActive ? "down" : "up"}
text={isActive ? t("incidentsPage.active") : t("incidentsPage.resolved")}
customStyles={{
textTransform: "capitalize",
}}
/>
</Stack>
<Box sx={{ flexGrow: 1 }} />
<Typography
variant="body1"
fontWeight={500}
>
{duration}
</Typography>
</Stack>
{incident.monitorName || t("incidentsPage.unknownMonitor")}
</Typography>
</Stack>
<Box sx={{ display: "flex", justifyContent: "center" }}>
<StatusLabel
status={isActive ? "down" : "up"}
text={isActive ? t("incidentsPage.active") : t("incidentsPage.resolved")}
customStyles={{
textTransform: "capitalize",
}}
/>
</Box>
<Typography
variant="body1"
fontWeight={500}
textAlign="right"
>
{duration}
</Typography>
</Box>
);
};
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
import IncidentItem from "./IncidentItem.jsx";
import SummaryCard from "../SummaryCard/index.jsx";
import Icon from "@/Components/v1/Icon";
/**
* LatestIncidentsPanel Component
@@ -51,21 +52,35 @@ const LatestIncidentsPanel = ({ incidents = [], isLoading = false, error = null
return (
<SummaryCard title={t("incidentsPage.incidentsLatestPanelTitle")}>
{!incidents || incidents.length === 0 ? (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexGrow: 1,
}}
<Stack
direction="column"
alignItems="center"
justifyContent="center"
padding={theme.spacing(6)}
gap={theme.spacing(2)}
sx={{ flex: 1 }}
>
<Box sx={{ mb: theme.spacing(1) }}>
<Icon
name="Check"
size={24}
color={theme.palette.success.main}
strokeWidth={2}
/>
</Box>
<Typography
variant="body2"
textAlign="center"
sx={{
fontSize: 13,
textTransform: "uppercase",
fontWeight: 500,
textAlign: "center",
color: theme.palette.success.lowContrast,
}}
>
{t("incidentsPage.incidentsLatestPanelEmpty")}
</Typography>
</Box>
</Stack>
) : (
<Stack gap={theme.spacing(4)}>
{incidents.map((incident, index) => (
@@ -1,9 +1,8 @@
import PropTypes from "prop-types";
import { Box, Stack, Typography } from "@mui/material";
import { Box, Stack, Typography, Divider } from "@mui/material";
import { useTheme } from "@emotion/react";
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
import { useTranslation } from "react-i18next";
import { Divider } from "@mui/material";
import Icon from "@/Components/v1/Icon";
import SummaryCard from "../SummaryCard/index.jsx";
@@ -32,18 +31,19 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
const { t } = useTranslation();
const totalResolutions =
(summary?.totalManualResolutions || 0) + (summary?.totalAutomaticResolutions || 0);
const automaticPercentage =
totalResolutions > 0
? (summary?.totalAutomaticResolutions / totalResolutions) * 100
: 0;
const iconWrapperStyle = {
display: "flex",
justifyContent: "center",
mx: theme.spacing(3),
mx: theme.spacing(2),
color: theme.palette.primary.contrastTextTertiary,
"& svg": {
width: 18,
height: 18,
},
"& svg path": {
stroke: "currentColor",
strokeWidth: 1.5,
},
};
if (isLoading) {
@@ -73,162 +73,87 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
);
}
const getMostAffectedMonitor = () => {
if (!summary.total || summary.total === 0) {
return t("incidentsPage.none");
}
return summary.topMonitor?.monitorName || t("incidentsPage.none");
};
const rowStyle = {
py: theme.spacing(0.5),
minHeight: 32,
};
return (
<SummaryCard title={t("incidentsPage.incidentsStatisticsPanelTitle")}>
<Stack gap={theme.spacing(4)}>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(3)}
>
<Box
sx={{
...iconWrapperStyle,
}}
>
<Icon
name="Bell"
size={20}
/>
</Box>
<Box>
<Typography
variant="body1"
sx={{
fontWeight: 500,
lineHeight: 1.2,
}}
>
{t("incidentsPage.totalIncidents")} : {summary.total || 0}
</Typography>
</Box>
</Stack>
<Divider />
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<Box sx={iconWrapperStyle}>
<Icon
name="AlertTriangle"
size={20}
/>
</Box>
<Box>
<Typography
variant="body1"
sx={{
fontWeight: 500,
lineHeight: 1.2,
}}
>
{t("incidentsPage.mostAffectedMonitor")} :{" "}
{summary.topMonitor?.monitorName || t("incidentsPage.unknownMonitor")}
</Typography>
</Box>
</Stack>
<Divider />
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<Box sx={iconWrapperStyle}>
<Icon
name="Wrench"
size={20}
/>
</Box>
<Box>
<Typography
variant="body1"
sx={{
fontWeight: 600,
}}
>
{t("incidentsPage.avgResolutionTime")} :{" "}
{summary.avgResolutionTimeHours || 0} {t("incidentsPage.hours")}
</Typography>
</Box>
</Stack>
<Divider />
<Box padding={theme.spacing(2)}>
<Box>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
mb={theme.spacing(1.5)}
sx={rowStyle}
>
<Box sx={iconWrapperStyle}>
<Icon
name="RefreshCw"
size={20}
name="Bell"
size={18}
/>
</Box>
<Box>
<Typography
variant="body1"
sx={{
fontWeight: 500,
lineHeight: 1.2,
}}
>
{t("incidentsPage.resolutions")}: {totalResolutions}
</Typography>
</Box>
<Typography
variant="body1"
fontWeight={500}
>
{t("incidentsPage.totalIncidents")}: {summary.total || 0}
</Typography>
</Stack>
<Box
sx={{
display: "flex",
height: 8,
borderRadius: 4,
overflow: "hidden",
bgcolor: theme.palette.accent.main,
width: "100%",
marginTop: theme.spacing(4),
}}
<Divider sx={{ mt: theme.spacing(2) }} />
</Box>
<Box>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
sx={rowStyle}
>
<Box
sx={{
width: `${automaticPercentage}%`,
bgcolor: theme.palette.warningSecondary.lowContrast,
height: "100%",
}}
<Box sx={iconWrapperStyle}>
<Icon
name="AlertTriangle"
size={18}
/>
</Box>
<Typography
variant="body1"
fontWeight={500}
>
{t("incidentsPage.mostAffectedMonitor")}: {getMostAffectedMonitor()}
</Typography>
</Stack>
<Divider sx={{ mt: theme.spacing(2) }} />
</Box>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
sx={rowStyle}
>
<Box sx={iconWrapperStyle}>
<Icon
name="Wrench"
size={18}
/>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
paddingTop: theme.spacing(4),
width: "100%",
}}
<Typography
variant="body1"
fontWeight={500}
>
{summary.totalAutomaticResolutions > 0 && (
<Typography
variant="body2"
fontWeight={500}
color={theme.palette.warningSecondary.contrastText}
>
{t("incidentsPage.automatic")} ({summary.totalAutomaticResolutions})
</Typography>
)}
{summary.totalManualResolutions > 0 && (
<Typography
variant="body2"
fontWeight={500}
color={theme.palette.accent.main}
>
{t("incidentsPage.manual")} ({summary.totalManualResolutions})
</Typography>
)}
</Box>
</Box>
{t("incidentsPage.avgResolutionTime")}:{" "}
{summary.total > 0
? `${summary.avgResolutionTimeHours || 0} ${t("incidentsPage.hours")}`
: "N/A"}
</Typography>
</Stack>
</Stack>
</SummaryCard>
);
@@ -8,17 +8,14 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }
<Paper
elevation={0}
sx={{
padding: theme.spacing(4),
borderRadius: 3,
padding: "16px",
borderRadius: theme.shape.borderRadius,
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
backgroundColor: theme.palette.primary.main,
border: isHighPriority
? `${theme.spacing(1.5)} solid ${theme.palette.error.lowContrast}`
: `${theme.spacing(1)} solid ${theme.palette.divider}`,
boxShadow: theme.palette.tertiary.cardShadow,
border: `1px solid ${theme.palette.primary.lowContrast}`,
color: theme.palette.primary.contrastTextTertiary,
fontSize: theme.typography.body1.fontSize,
...sx,
@@ -33,13 +30,12 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }
}}
>
<Typography
variant="h6"
component="h2"
sx={{
textTransform: "uppercase",
fontWeight: 700,
fontSize: theme.typography.h2.fontSize,
letterSpacing: theme.spacing(0.5),
fontWeight: 500,
fontSize: 13,
color: theme.palette.primary.contrastTextSecondary,
}}
>
{title}
@@ -49,9 +45,9 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }
</Box>
)}
<Stack
mt={theme.spacing(4)}
paddingTop={theme.spacing(5)}
gap={theme.spacing(4)}
mt={theme.spacing(2)}
paddingTop={theme.spacing(2)}
gap={theme.spacing(2)}
sx={{ height: "100%" }}
>
{children}
@@ -12,13 +12,16 @@ import { Box } from "@mui/material";
const BaseContainer = ({children}) => {
const theme = useTheme()
return(
<Box
<Box
sx={{
padding: theme.spacing(3),
borderRadius: theme.spacing(2),
border: `1px solid ${theme.palette.divider}`,
minWidth: 250,
width: "fit-content",
borderRadius: 4,
border: `1px solid ${theme.palette.primary.lowContrast}`,
minWidth: 200,
width: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
[theme.breakpoints.down("md")]: {
width: `calc(50% - (1 * ${theme.spacing(8)} / 2))`,
},
}}>
{children}
</Box>
@@ -99,7 +102,7 @@ const Gauges = ({ diagnostics, isLoading }) => {
return (
<Stack
direction="row"
spacing={theme.spacing(8)}
gap={theme.spacing(8)}
flexWrap="wrap"
>
<InfrastructureStyleGauge
+123 -49
View File
@@ -2,14 +2,17 @@ import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Select from "@/Components/v1/Inputs/Select/index.jsx";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
import DataTable from "@/Components/v1/Table/index.jsx";
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
import { useFetchLogs } from "../../../Hooks/logHooks.js";
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
const formatLog = (theme, log, idx) => {
const LevelBadge = ({ level }) => {
const theme = useTheme();
const levelColors = {
info: theme.palette.success.main,
warn: theme.palette.warning.main,
@@ -17,31 +20,38 @@ const formatLog = (theme, log, idx) => {
debug: theme.palette.accent.main,
};
const color = levelColors[log.level] || theme.palette.primary.contrastText;
const color = levelColors[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>
<Box
component="span"
sx={{
color: color,
fontWeight: 600,
textTransform: "uppercase",
fontSize: 12,
}}
>
{level}
</Box>
);
};
const Logs = () => {
// Local state
const [logLevel, setLogLevel] = useState("all");
const formatTimestamp = (timestamp) => {
if (!timestamp) return "-";
const date = new Date(timestamp);
return date.toLocaleString();
};
const Logs = () => {
const [logLevel, setLogLevel] = useState("all");
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(15);
// 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") },
@@ -49,52 +59,116 @@ const Logs = () => {
{ _id: "error", name: t("logsPage.logLevelSelect.values.error") },
{ _id: "debug", name: t("logsPage.logLevelSelect.values.debug") },
];
const headers = [
{
id: "timestamp",
content: t("logsPage.table.timestamp"),
render: (row) => (
<Typography sx={{ fontSize: 13, fontFamily: "monospace" }}>
{formatTimestamp(row.timestamp)}
</Typography>
),
},
{
id: "level",
content: t("logsPage.table.level"),
render: (row) => <LevelBadge level={row.level} />,
},
{
id: "service",
content: t("logsPage.table.service"),
render: (row) => (
<Typography sx={{ fontSize: 13 }}>{row.service || "-"}</Typography>
),
},
{
id: "method",
content: t("logsPage.table.method"),
render: (row) => (
<Typography sx={{ fontSize: 13, fontFamily: "monospace" }}>{row.method || "-"}</Typography>
),
},
{
id: "message",
content: t("logsPage.table.message"),
render: (row) => (
<Typography
sx={{
fontSize: 13,
maxWidth: 400,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{row.message || "-"}
</Typography>
),
},
];
const filteredLogs = logs
?.filter((log) => {
if (logLevel === "all") return true;
return log.level === logLevel;
})
.reverse()
.map((log, idx) => ({ ...log, id: idx }));
const paginatedLogs = filteredLogs?.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
);
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const handleLogLevelChange = (e) => {
setLogLevel(e.target.value);
setPage(0);
};
return (
<Stack gap={theme.spacing(4)}>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
sx={{
position: "sticky",
top: theme.spacing(17),
backdropFilter: "blur(10px)",
paddingTop: theme.spacing(4),
paddingLeft: theme.spacing(6),
}}
>
<Typography>{t("logsPage.logLevelSelect.title")}</Typography>
<Select
items={LOG_LEVELS}
value={logLevel}
onChange={(e) => {
setLogLevel(e.target.value);
}}
onChange={handleLogLevelChange}
/>
</Stack>
<Box
component="pre"
sx={{
fontFamily: "monospace",
color: theme.palette.primary.contrastText,
padding: 2,
borderRadius: 1,
overflowX: "auto",
whiteSpace: "pre-wrap",
wordWrap: "break-word",
<DataTable
shouldRender={!isLoading}
headers={headers}
data={paginatedLogs || []}
config={{
emptyView: t("logsPage.noLogs"),
}}
>
<code>
{logs
?.filter((log) => {
if (logLevel === "all") return true;
return log.level === logLevel;
})
.reverse()
.map((log, idx) => formatLog(theme, log, idx))}
</code>
</Box>
/>
{filteredLogs?.length > 0 && (
<Pagination
paginationLabel={t("logsPage.table.logs")}
itemCount={filteredLogs?.length || 0}
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
)}
</Stack>
);
};
+12 -2
View File
@@ -791,7 +791,16 @@
"error": "Error",
"debug": "Debug"
}
}
},
"table": {
"timestamp": "Timestamp",
"level": "Level",
"service": "Service",
"method": "Method",
"message": "Message",
"logs": "logs"
},
"noLogs": "No logs found"
},
"queuePage": {
"title": "Queue",
@@ -1195,6 +1204,7 @@
"incidentsOptionsHeaderFilterResolved": "Resolved",
"incidentsTableResolved": "Closed",
"incidentsTableActionResolveManually": "Resolve Manually",
"hours": "hours"
"hours": "hours",
"none": "None"
}
}