Improve UI consistency for Logs and Incidents pages

## Changes

### Logs page
- Convert server logs from monospace text to DataTable with pagination
- Add table columns: Timestamp, Level, Service, Method, Message
- Add colored level badges (info=green, warn=yellow, error=red, debug=blue)
- Add pagination with configurable rows per page
- Align diagnostics gauge cards with 16px gap and responsive widths

### Incidents page
- Redesign summary cards with consistent styling
- Remove shadows and use 1px border matching other sections
- Reduce icon sizes to 18-24px (sidebar-style)
- Reduce font sizes (13px titles, 18-32px values)
- Add titles to all three summary cards
- Fix "Most Affected Monitor" showing "Unknown Monitor" when no incidents
- Show "N/A" for avg resolution time when no incidents
- Remove cluttered "Resolutions" progress bar section
- Remove horizontal dividers between statistics rows
- Standardize card padding and gap to 16px

### Uptime page
- Fix oversized monitor names in table (removed h6 variant)
This commit is contained in:
gorkem-bwl
2026-01-15 20:44:51 -05:00
parent 2f2ce34373
commit 1ab31d19f3
9 changed files with 223 additions and 203 deletions
+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,
}}
>
@@ -43,39 +43,36 @@ 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),
}}
>
<CheckIcon />
</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")}
@@ -86,21 +83,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 },
},
}}
@@ -109,27 +105,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
@@ -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 CheckIcon from "@/assets/icons/check-icon.svg?react";
/**
* LatestIncidentsPanel Component
@@ -51,21 +52,40 @@ 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={{
color: theme.palette.success.main,
"& svg": {
width: 24,
height: 24,
"& path": { stroke: "currentColor", strokeWidth: 2 },
},
mb: theme.spacing(1),
}}
>
<CheckIcon />
</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) => (
@@ -3,10 +3,8 @@ import { Box, Stack, Typography } 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 Clock from "@/assets/icons/maintenance.svg?react";
import Incidents from "@/assets/icons/incidents.svg?react";
import ResolutionItem from "@/assets/icons/interval-check.svg?react";
import NotificationIcon from "@/assets/icons/notifications.svg?react";
import SummaryCard from "../SummaryCard/index.jsx";
@@ -43,10 +41,15 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
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) {
@@ -76,6 +79,13 @@ 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");
};
return (
<SummaryCard title={t("incidentsPage.incidentsStatisticsPanelTitle")}>
<Stack gap={theme.spacing(4)}>
@@ -99,11 +109,10 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
lineHeight: 1.2,
}}
>
{t("incidentsPage.totalIncidents")} : {summary.total || 0}
{t("incidentsPage.totalIncidents")}: {summary.total || 0}
</Typography>
</Box>
</Stack>
<Divider />
<Stack
direction="row"
alignItems="center"
@@ -120,12 +129,10 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
lineHeight: 1.2,
}}
>
{t("incidentsPage.mostAffectedMonitor")} :{" "}
{summary.topMonitor?.monitorName || t("incidentsPage.unknownMonitor")}
{t("incidentsPage.mostAffectedMonitor")}: {getMostAffectedMonitor()}
</Typography>
</Box>
</Stack>
<Divider />
<Stack
direction="row"
alignItems="center"
@@ -138,88 +145,14 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
<Typography
variant="body1"
sx={{
fontWeight: 600,
fontWeight: 500,
}}
>
{t("incidentsPage.avgResolutionTime")} :{" "}
{summary.avgResolutionTimeHours || 0} {t("incidentsPage.hours")}
{t("incidentsPage.avgResolutionTime")}:{" "}
{summary.total > 0 ? `${summary.avgResolutionTimeHours || 0} ${t("incidentsPage.hours")}` : "N/A"}
</Typography>
</Box>
</Stack>
<Divider />
<Box padding={theme.spacing(2)}>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
mb={theme.spacing(1.5)}
>
<Box sx={iconWrapperStyle}>
<ResolutionItem />
</Box>
<Box>
<Typography
variant="body1"
sx={{
fontWeight: 500,
lineHeight: 1.2,
}}
>
{t("incidentsPage.resolutions")}: {totalResolutions}
</Typography>
</Box>
</Stack>
<Box
sx={{
display: "flex",
height: 8,
borderRadius: 4,
overflow: "hidden",
bgcolor: theme.palette.accent.main,
width: "100%",
marginTop: theme.spacing(4),
}}
>
<Box
sx={{
width: `${automaticPercentage}%`,
bgcolor: theme.palette.warningSecondary.lowContrast,
height: "100%",
}}
/>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
paddingTop: theme.spacing(4),
width: "100%",
}}
>
{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>
</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"
}
}