Skip to content

Commit 38c07ff

Browse files
committed
Added metrics view and better welcome screen to web UI
1 parent 18f9ce0 commit 38c07ff

File tree

11 files changed

+677
-49
lines changed

11 files changed

+677
-49
lines changed

web/package-lock.json

Lines changed: 355 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,28 @@
1212
"dependencies": {
1313
"@emotion/react": "^11.14.0",
1414
"@emotion/styled": "^11.14.0",
15-
"@fontsource/roboto": "^5.1.0",
16-
"@mui/icons-material": "^6.3.0",
17-
"@mui/material": "^6.3.0",
15+
"@fontsource/roboto": "^5.1.1",
16+
"@mui/icons-material": "^6.4.0",
17+
"@mui/material": "^6.4.0",
18+
"@mui/x-charts": "^7.24.0",
1819
"@reduxjs/toolkit": "^2.5.0",
1920
"react": "^18.3.1",
2021
"react-dom": "^18.3.1",
2122
"react-redux": "^9.2.0",
22-
"react-router": "^7.1.1"
23+
"react-router": "^7.1.3"
2324
},
2425
"devDependencies": {
25-
"@eslint/js": "^9.17.0",
26+
"@eslint/js": "^9.18.0",
2627
"@types/react": "^18.3.18",
2728
"@types/react-dom": "^18.3.5",
2829
"@vitejs/plugin-react": "^4.3.4",
29-
"eslint": "^9.17.0",
30-
"eslint-plugin-react-hooks": "^5.0.0",
31-
"eslint-plugin-react-refresh": "^0.4.16",
30+
"eslint": "^9.18.0",
31+
"eslint-plugin-react-hooks": "^5.1.0",
32+
"eslint-plugin-react-refresh": "^0.4.18",
3233
"globals": "^15.14.0",
3334
"prettier": "3.4.2",
34-
"typescript": "~5.6.2",
35-
"typescript-eslint": "^8.18.2",
36-
"vite": "^6.0.5"
35+
"typescript": "~5.7.3",
36+
"typescript-eslint": "^8.20.0",
37+
"vite": "^6.0.7"
3738
}
3839
}

web/src/Routes.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Queries from "./components/Queries.tsx";
99
import Query from "./components/Query.tsx";
1010
import Module from "./components/Module.tsx";
1111
import Settings from "./components/Settings.tsx";
12+
import Metrics from "./components/Metrics.tsx";
1213

1314
export default function Router() {
1415
const { isAuthenticated } = useAuthentication();
@@ -36,6 +37,7 @@ export default function Router() {
3637
<Route path={":id"} element={<Query />} />
3738
<Route index={true} element={<Queries />} />
3839
</Route>
40+
<Route path={"metrics"} element={<Metrics />} />
3941
</Route>
4042
</Routes>
4143
</BrowserRouter>

web/src/api/api.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,23 +74,28 @@ interface Info {
7474
version: string;
7575
version_url: string;
7676
}
77+
7778
interface Version {
7879
version: string;
7980
}
81+
8082
export interface LogRecord {
8183
date: string;
8284
file: string;
8385
level: "critical" | "error" | "warning" | "info" | "success" | "debug" | "unknown";
8486
line: number;
8587
message: string;
8688
}
89+
8790
export interface LogStatus {
8891
errors: number;
8992
last_error: string;
9093
}
94+
9195
interface ModulesQuery {
9296
all: boolean;
9397
}
98+
9499
export interface ModuleListItem {
95100
id: string;
96101
name: string;
@@ -140,10 +145,12 @@ interface Query {
140145
execute_nagios_url: string;
141146
execute_url: string;
142147
}
148+
143149
export interface ExecuteQueryArgs {
144150
query: string;
145151
args: string[];
146152
}
153+
147154
export interface QueryExecutionResultLinePerf {
148155
critical: number;
149156
maximum: number;
@@ -159,6 +166,7 @@ export interface QueryExecutionResultLine {
159166
[key: string]: QueryExecutionResultLinePerf;
160167
};
161168
}
169+
162170
export interface QueryExecutionResult {
163171
command: string;
164172
lines: QueryExecutionResultLine[];
@@ -171,11 +179,13 @@ export interface SettingsStatus {
171179
type: string;
172180
has_changed: boolean;
173181
}
182+
174183
export interface Settings {
175184
key: string;
176185
path: string;
177186
value: string;
178187
}
188+
179189
export interface SettingsCommand {
180190
command: "load" | "save" | "reload";
181191
}
@@ -196,6 +206,8 @@ export interface SettingsDescription {
196206
value: string;
197207
}
198208

209+
export type Metrics = { [key: string]: string | number };
210+
199211
const baseQuery = fetchBaseQuery({
200212
baseUrl: "/api",
201213
prepareHeaders: (headers, { getState }) => {
@@ -248,6 +260,7 @@ export const nsclientApi = createApi({
248260
"SettingsStatus",
249261
"SettingsDescriptions",
250262
"LogStatus",
263+
"Metrics",
251264
],
252265
endpoints: (builder) => ({
253266
getEndpoints: builder.query<EndpointList, void>({
@@ -438,6 +451,12 @@ export const nsclientApi = createApi({
438451
},
439452
}),
440453
}),
454+
getMetrics: builder.query<Metrics, void>({
455+
query: () => ({
456+
url: "/v2/metrics",
457+
}),
458+
providesTags: ["Metrics"],
459+
}),
441460
}),
442461
});
443462

@@ -465,4 +484,5 @@ export const {
465484
useGetLogStatusQuery,
466485
useResetLogStatusMutation,
467486
useLoginMutation,
487+
useGetMetricsQuery,
468488
} = nsclientApi;

web/src/components/AppNavbar.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import Typography from "@mui/material/Typography";
33
import { Box, IconButton, Toolbar } from "@mui/material";
44
import MenuIcon from "@mui/icons-material/Menu";
55
import LogStatusIcon from "./LogStatusIcon.tsx";
6+
import { useGetInfoQuery } from "../api/api.ts";
67

78
interface Props {
89
handleDrawerToggle: () => void;
910
}
1011

1112
export default function AppNavbar({ handleDrawerToggle }: Props) {
13+
const { data: info } = useGetInfoQuery();
14+
const version = info?.version || "unknown";
1215
return (
1316
<Box sx={{ flexGrow: 1 }}>
1417
<AppBar
@@ -27,9 +30,12 @@ export default function AppNavbar({ handleDrawerToggle }: Props) {
2730
>
2831
<MenuIcon />
2932
</IconButton>
30-
<Typography variant="h4" component="div" sx={{ flexGrow: 1 }}>
33+
<Typography variant="h4" component="div" sx={{ paddingRight: 3 }}>
3134
NSClient++
3235
</Typography>
36+
<Typography variant="h5" color="textDisabled" component="div" sx={{ flexGrow: 1 }}>
37+
{version}
38+
</Typography>
3339
<LogStatusIcon />
3440
</Toolbar>
3541
</AppBar>

web/src/components/LogStatusIcon.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CircularProgress, IconButton, Menu, MenuItem, Tooltip } from "@mui/material";
1+
import { IconButton, Menu, MenuItem, Tooltip } from "@mui/material";
22
import { nsclientApi, useGetLogStatusQuery, useResetLogStatusMutation } from "../api/api.ts";
33
import DoneIcon from "@mui/icons-material/Done";
44
import ErrorIcon from "@mui/icons-material/Error";
@@ -8,7 +8,7 @@ import { useNavigate } from "react-router";
88
import { useAppDispatch } from "../store/store.ts";
99

1010
export default function LogStatusIcon() {
11-
const { data: logStatus, isFetching } = useGetLogStatusQuery();
11+
const { data: logStatus } = useGetLogStatusQuery();
1212
const [resetStatus] = useResetLogStatusMutation();
1313
const dispatch = useAppDispatch();
1414
const navigate = useNavigate();
@@ -42,18 +42,14 @@ export default function LogStatusIcon() {
4242
resetStatus();
4343
};
4444

45-
const icon = isFetching ? (
46-
<CircularProgress size="2em" />
47-
) : errors === 0 ? (
48-
<DoneIcon color="success" />
49-
) : (
50-
<ErrorIcon color="error" />
51-
);
45+
const icon = errors === 0 ? <DoneIcon color="success" /> : <ErrorIcon color="error" />;
5246

5347
return (
5448
<>
5549
<Tooltip disableFocusListener title={lastError}>
56-
<IconButton onClick={handleOpenUserMenu}>{icon}</IconButton>
50+
<IconButton sx={{ backgroundColor: "white" }} onClick={handleOpenUserMenu}>
51+
{icon}
52+
</IconButton>
5753
</Tooltip>
5854
<Menu
5955
sx={{ mt: "45px" }}

web/src/components/Logs.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Spacing } from "./atoms/Spacing.tsx";
2222
import { useState } from "react";
2323
import CloseIcon from "@mui/icons-material/Close";
2424
import Button from "@mui/material/Button";
25+
import Typography from "@mui/material/Typography";
2526

2627
const ICONS = {
2728
critical: <ErrorIcon color="error" />,
@@ -58,6 +59,7 @@ export default function Logs() {
5859
<Box sx={{ p: { sm: 3 } }}>
5960
<Stack direction="column">
6061
<Toolbar>
62+
<Typography variant="body2">Filter:</Typography>
6163
<ToggleButtonGroup
6264
value={level}
6365
exclusive
@@ -73,10 +75,10 @@ export default function Logs() {
7375
</ToggleButton>
7476
</ToggleButtonGroup>
7577

78+
<Spacing />
7679
<Button size="small" onClick={clear}>
77-
Clear
80+
Clear log
7881
</Button>
79-
<Spacing />
8082
<RefreshButton onRefresh={onRefresh} />
8183
</Toolbar>
8284
{logs?.count === 0 && <p>No logs found</p>}

web/src/components/Metrics.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import Stack from "@mui/material/Stack";
2+
import { nsclientApi, useGetMetricsQuery } from "../api/api.ts";
3+
import {
4+
Table,
5+
TableBody,
6+
TableCell,
7+
TableContainer,
8+
TableHead,
9+
TableRow,
10+
TextField,
11+
ToggleButton,
12+
ToggleButtonGroup,
13+
} from "@mui/material";
14+
import { Toolbar } from "./atoms/Toolbar.tsx";
15+
import { Spacing } from "./atoms/Spacing.tsx";
16+
import { RefreshButton } from "./atoms/RefreshButton.tsx";
17+
import { useAppDispatch } from "../store/store.ts";
18+
import { useState } from "react";
19+
import CloseIcon from "@mui/icons-material/Close";
20+
import { parseMetrics } from "../metric_parser.ts";
21+
22+
export default function Metrics() {
23+
const dispatch = useAppDispatch();
24+
const [filter, setFilter] = useState<string>("");
25+
const { data: metrics } = useGetMetricsQuery();
26+
27+
const result = parseMetrics(metrics);
28+
const filteredMetrics = result.metrics.filter((m) => filter === "" || m.key.includes(filter));
29+
30+
const onRefresh = () => {
31+
dispatch(nsclientApi.util.invalidateTags(["Metrics"]));
32+
};
33+
34+
const sortedMetrics = filteredMetrics.sort((a, b) => a.key.localeCompare(b.key));
35+
36+
return (
37+
<Stack direction="column">
38+
<Toolbar>
39+
<TextField
40+
label="Filter"
41+
variant="standard"
42+
size="small"
43+
value={filter}
44+
onChange={(e) => setFilter(e.target.value)}
45+
/>
46+
<ToggleButtonGroup value={filter} exclusive onChange={(_e, f) => setFilter(f)} aria-label="text alignment">
47+
{result.modules.map((m) => (
48+
<ToggleButton value={m}>{m}</ToggleButton>
49+
))}
50+
<ToggleButton value="">
51+
<CloseIcon />
52+
</ToggleButton>
53+
</ToggleButtonGroup>
54+
<Spacing />
55+
<RefreshButton onRefresh={onRefresh} />
56+
</Toolbar>
57+
<TableContainer sx={{ width: "100%" }}>
58+
<Table>
59+
<TableHead>
60+
<TableRow>
61+
<TableCell>Module</TableCell>
62+
<TableCell>Type</TableCell>
63+
<TableCell>Instance</TableCell>
64+
<TableCell>Metric</TableCell>
65+
<TableCell align="right">Value</TableCell>
66+
</TableRow>
67+
</TableHead>
68+
<TableBody>
69+
{sortedMetrics.map((m) => (
70+
<TableRow hover key={m.key} sx={{ "&:last-child td, &:last-child th": { border: 0 } }}>
71+
<TableCell>{m.module}</TableCell>
72+
<TableCell>{m.type}</TableCell>
73+
<TableCell>{m.instance}</TableCell>
74+
<TableCell>{m.metric}</TableCell>
75+
<TableCell align="right">{m.value}</TableCell>
76+
</TableRow>
77+
))}
78+
</TableBody>
79+
</Table>
80+
</TableContainer>
81+
</Stack>
82+
);
83+
}

web/src/components/SideMenu.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import AppsIcon from "@mui/icons-material/Apps";
55
import TaskAltIcon from "@mui/icons-material/TaskAlt";
66
import MessageIcon from "@mui/icons-material/Message";
77
import SettingsIcon from "@mui/icons-material/Settings";
8+
import ShowChartIcon from "@mui/icons-material/ShowChart";
89
import { useNavigate } from "react-router";
910

1011
export default function SideMenu() {
@@ -51,6 +52,15 @@ export default function SideMenu() {
5152
<ListItemText primary="Queries" />
5253
</ListItemButton>
5354
</ListItem>
55+
<Divider />
56+
<ListItem disablePadding>
57+
<ListItemButton onClick={() => navigate("/metrics")}>
58+
<ListItemIcon>
59+
<ShowChartIcon />
60+
</ListItemIcon>
61+
<ListItemText primary="Metrics" />
62+
</ListItemButton>
63+
</ListItem>
5464
<ListItem disablePadding>
5565
<ListItemButton onClick={() => navigate("/logs")}>
5666
<ListItemIcon>

0 commit comments

Comments
 (0)