1
0
mirror of https://github.com/prometheus/alertmanager.git synced 2026-02-05 15:45:34 +01:00

Implement status pages in the mantine-ui (#4922)

* Implement status pages in the mantine-ui

Add the Status>Configuration and Status>Runtime & Build Information pages. These are nearly identical to the pages in Prometheus.

I copied the InfoPageCard and InfoPageStack files directly from Prometheus. Now that we will have a second project using some of these components, we may want to start thinking about a shared library.

Signed-off-by: Joe Adams <github@joeadams.io>

* Lint

Signed-off-by: Joe Adams <github@joeadams.io>

---------

Signed-off-by: Joe Adams <github@joeadams.io>
This commit is contained in:
Joe Adams
2026-02-01 07:22:38 -05:00
committed by GitHub
parent d403f004b9
commit 32b6b5a6e4
9 changed files with 375 additions and 42 deletions

View File

@@ -8,9 +8,11 @@
"name": "alertmanager",
"version": "0.0.0",
"dependencies": {
"@mantine/core": "8.3.12",
"@mantine/hooks": "8.3.12",
"@mantine/code-highlight": "^8.3.13",
"@mantine/core": "^8.3.13",
"@mantine/hooks": "^8.3.13",
"@tanstack/react-query": "^5.90.16",
"highlight.js": "^11.11.1",
"react": "^19.1.0",
"react-dom": "^19.2.3",
"react-router-dom": "^7.12.0"
@@ -1471,10 +1473,25 @@
"dev": true,
"license": "MIT"
},
"node_modules/@mantine/code-highlight": {
"version": "8.3.13",
"resolved": "https://registry.npmjs.org/@mantine/code-highlight/-/code-highlight-8.3.13.tgz",
"integrity": "sha512-AEFZ8TVu9A2yjllDz+Yodz5r6o5KX1p1B+9xDa97UXbYGxojJQI0V5Obh8Hyi8o1MAAZ/z1YaE7XznpYwQJjQg==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"@mantine/core": "8.3.13",
"@mantine/hooks": "8.3.13",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/core": {
"version": "8.3.12",
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.12.tgz",
"integrity": "sha512-bDEoUl4SneltfI1GeEaBk6BVDbLuB/w15YwseAmUvc8ldAbNcsVhxKxY/BdhwqUo6O3L2vhdlb3WwxR1y8741g==",
"version": "8.3.13",
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.13.tgz",
"integrity": "sha512-ZgW4vqN4meaPyIMxzAufBvsgmJRfYZdTpsrAOcS8pWy7m9e8i685E7XsAxnwJCOIHudpvpvt+7Bx7VaIjEsYEw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.27.16",
@@ -1485,15 +1502,15 @@
"type-fest": "^4.41.0"
},
"peerDependencies": {
"@mantine/hooks": "8.3.12",
"@mantine/hooks": "8.3.13",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/hooks": {
"version": "8.3.12",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.12.tgz",
"integrity": "sha512-lMMDzDewd3lUNtJCAHDj3g8On9X5aBl4q6EBwgOixKQSby9RG9ASEpK8oYHundHTm9tzo3MDeXWV/z32oSQWuw==",
"version": "8.3.13",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.13.tgz",
"integrity": "sha512-7YMbMW/tR9E8m/9DbBW01+3RNApm2mE/JbRKXf9s9+KxgbjQvq0FYGWV8Y4+Sjz48AO4vtWk2qBriUTgBMKAyg==",
"license": "MIT",
"peerDependencies": {
"react": "^18.x || ^19.x"
@@ -4473,6 +4490,15 @@
"node": ">= 0.4"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/hookified": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.13.0.tgz",

View File

@@ -17,9 +17,11 @@
"test": "npm run typecheck && npm run prettier && npm run lint && npm run vitest && npm run build"
},
"dependencies": {
"@mantine/core": "8.3.12",
"@mantine/hooks": "8.3.12",
"@mantine/code-highlight": "^8.3.13",
"@mantine/core": "^8.3.13",
"@mantine/hooks": "^8.3.13",
"@tanstack/react-query": "^5.90.16",
"highlight.js": "^11.11.1",
"react": "^19.1.0",
"react-dom": "^19.2.3",
"react-router-dom": "^7.12.0"

View File

@@ -1,48 +1,64 @@
import '@mantine/core/styles.css';
import '@mantine/code-highlight/styles.css';
import { Suspense } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import hljs from 'highlight.js/lib/core';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { CodeHighlightAdapterProvider, createHighlightJsAdapter } from '@mantine/code-highlight';
import { AppShell, Box, MantineProvider, Skeleton } from '@mantine/core';
import ErrorBoundary from './components/ErrorBoundary';
import { Header } from './components/Header';
import { AlertsPage } from './pages/Alerts.page';
import { ConfigPage } from './pages/Config.page';
import { SilencesPage } from './pages/Silences.page';
import { StatusPage } from './pages/Status.page';
import { theme } from './theme';
import './highlightjs.css';
import yamlLang from 'highlight.js/lib/languages/yaml';
hljs.registerLanguage('yaml', yamlLang);
const highlightJsAdapter = createHighlightJsAdapter(hljs);
const queryClient = new QueryClient();
export default function App() {
return (
<BrowserRouter>
<MantineProvider theme={theme}>
<QueryClientProvider client={queryClient}>
<AppShell padding="md" header={{ height: 60 }}>
<Header />
<AppShell.Main>
<ErrorBoundary key={location.pathname}>
<Suspense
fallback={
<Box mt="lg">
{Array.from(Array(10), (_, i) => (
<Skeleton key={i} height={40} mb={15} width={1000} mx="auto" />
))}
</Box>
}
>
{/* Main content will be rendered here by the Router */}
<Routes>
{/* Redirect the root path to the alerts page */}
{/* TODO(@sysadmind): This should take the fact that previous UI used /#/routeName */}
<Route path="/" element={<Navigate to="/alerts" replace />} />
<Route path="/alerts" element={<AlertsPage />} />
<Route path="/silences" element={<SilencesPage />} />
</Routes>
</Suspense>
</ErrorBoundary>
</AppShell.Main>
</AppShell>
</QueryClientProvider>
<CodeHighlightAdapterProvider adapter={highlightJsAdapter}>
<QueryClientProvider client={queryClient}>
<AppShell padding="md" header={{ height: 60 }}>
<Header />
<AppShell.Main>
<ErrorBoundary key={location.pathname}>
<Suspense
fallback={
<Box mt="lg">
{Array.from(Array(10), (_, i) => (
<Skeleton key={i} height={40} mb={15} width={1000} mx="auto" />
))}
</Box>
}
>
{/* Main content will be rendered here by the Router */}
<Routes>
{/* Redirect the root path to the alerts page */}
{/* TODO(@sysadmind): This should take the fact that previous UI used /#/routeName */}
<Route path="/" element={<Navigate to="/alerts" replace />} />
<Route path="/alerts" element={<AlertsPage />} />
<Route path="/silences" element={<SilencesPage />} />
<Route path="/status" element={<StatusPage />} />
<Route path="/config" element={<ConfigPage />} />
</Routes>
</Suspense>
</ErrorBoundary>
</AppShell.Main>
</AppShell>
</QueryClientProvider>
</CodeHighlightAdapterProvider>
</MantineProvider>
</BrowserRouter>
);

View File

@@ -1,7 +1,7 @@
import { Link, NavLink } from 'react-router-dom';
import { AppShell, Button, Group, Text } from '@mantine/core';
import { AlertsPage } from '../pages/Alerts.page';
import { SilencesPage } from '../pages/Silences.page';
import { Link, NavLink, Route, Routes } from 'react-router-dom';
import { AppShell, Button, Group, Menu, Text } from '@mantine/core';
import { AlertsPage } from '@/pages/Alerts.page';
import { SilencesPage } from '@/pages/Silences.page';
import classes from './Header.module.css';
const navLinkXPadding = 'md';
@@ -36,6 +36,64 @@ export const Header = () => {
{page.title}
</Button>
))}
<Menu>
<Routes>
<Route
path="/status"
element={
<Menu.Target>
<Button
component={NavLink}
to="/status"
className={classes.navLink}
px={navLinkXPadding}
>
Status {'>'} Runtime & Build Information
</Button>
</Menu.Target>
}
/>
<Route
path="/config"
element={
<Menu.Target>
<Button
component={NavLink}
to="/config"
className={classes.navLink}
px={navLinkXPadding}
>
Status {'>'} Configuration
</Button>
</Menu.Target>
}
/>
{/* Default menu item when no status pages are selected */}
<Route
path="*"
element={
<Menu.Target>
<Button
className={classes.navLink}
// leftSection={<IconServer style={navIconStyle} />}
// rightSection={<IconChevronDown style={navIconStyle} />}
px={navLinkXPadding}
>
Status
</Button>
</Menu.Target>
}
/>
</Routes>
<Menu.Dropdown>
<Menu.Item key="runtime" component={NavLink} to="/status">
Runtime & Build Information
</Menu.Item>
<Menu.Item key="config" component={NavLink} to="/config">
Configuration
</Menu.Item>
</Menu.Dropdown>
</Menu>
</>
);

View File

@@ -0,0 +1,25 @@
// import { IconProps } from "@tabler/icons-react";
import { FC, ReactNode } from 'react';
import { Card, em, Group } from '@mantine/core';
const infoPageCardTitleIconStyle = { width: em(17.5), height: em(17.5) };
const InfoPageCard: FC<{
children: ReactNode;
title?: string;
icon?: React.ComponentType<any>;
}> = ({ children, title, icon: Icon }) => {
return (
<Card shadow="xs" withBorder p="md">
{title && (
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs" fz="xl" fw={600}>
{Icon && <Icon style={infoPageCardTitleIconStyle} />}
{title}
</Group>
)}
{children}
</Card>
);
};
export default InfoPageCard;

View File

@@ -0,0 +1,12 @@
import { FC, ReactNode } from 'react';
import { Stack } from '@mantine/core';
const InfoPageStack: FC<{ children: ReactNode }> = ({ children }) => {
return (
<Stack gap="lg" maw={1000} mx="auto" mt="xs">
{children}
</Stack>
);
};
export default InfoPageStack;

View File

@@ -0,0 +1,97 @@
/* Adapted from Mantine 7, where highlighting was still included automatically as part of <CodeHighlight>,
see https://github.com/mantinedev/mantine/blob/v7/packages/%40mantine/code-highlight/src/CodeHighlight.theme.module.css */
.hljs {
color: var(--code-text-color);
background: var(--code-background);
@mixin where-light {
--code-text-color: var(--mantine-color-gray-7);
--code-background: var(--mantine-color-gray-0);
--code-comment-color: var(--mantine-color-gray-6);
--code-keyword-color: var(--mantine-color-violet-8);
--code-tag-color: var(--mantine-color-red-9);
--code-literal-color: var(--mantine-color-blue-6);
--code-string-color: var(--mantine-color-blue-9);
--code-variable-color: var(--mantine-color-lime-9);
--code-class-color: var(--mantine-color-orange-9);
}
@mixin where-dark {
--code-text-color: var(--mantine-color-dark-1);
--code-background: var(--mantine-color-dark-8);
--code-comment-color: var(--mantine-color-dark-3);
--code-keyword-color: var(--mantine-color-violet-3);
--code-tag-color: var(--mantine-color-yellow-4);
--code-literal-color: var(--mantine-color-blue-4);
--code-string-color: var(--mantine-color-green-6);
--code-variable-color: var(--mantine-color-blue-2);
--code-class-color: var(--mantine-color-orange-5);
}
.hljs-comment,
.hljs-quote {
font-style: italic;
color: var(--code-comment-color);
}
.hljs-doctag,
.hljs-formula,
.hljs-keyword {
color: var(--code-keyword-color);
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: var(--code-tag-color);
}
.hljs-literal {
color: var(--code-literal-color);
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: var(--code-string-color);
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: var(--code-variable-color);
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title,
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: var(--code-class-color);
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
.hljs-link {
text-decoration: underline;
}
}

View File

@@ -0,0 +1,17 @@
import { CodeHighlight } from '@mantine/code-highlight';
import { useStatus } from '@/data/status';
export function ConfigPage() {
const { data } = useStatus();
return (
<CodeHighlight
language="yaml"
code={data.config.original}
miw="50vw"
w="fit-content"
maw="calc(100vw - 75px)"
mx="auto"
mt="xs"
/>
);
}

View File

@@ -0,0 +1,80 @@
import { Table } from '@mantine/core';
import InfoPageCard from '@/components/InfoPageCard';
import InfoPageStack from '@/components/InfoPageStack';
import { useStatus } from '@/data/status';
export function StatusPage() {
const { data } = useStatus();
return (
<InfoPageStack>
<InfoPageCard title="Build information">
<Table layout="fixed">
<Table.Tbody>
<Table.Tr>
<Table.Th>Version</Table.Th>
<Table.Td>{data.versionInfo.version}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Th>Revision</Table.Th>
<Table.Td>{data.versionInfo.revision}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Th>Branch</Table.Th>
<Table.Td>{data.versionInfo.branch}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Th>Build User</Table.Th>
<Table.Td>{data.versionInfo.buildUser}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Th>Build Date</Table.Th>
<Table.Td>{data.versionInfo.buildDate}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Th>Go Version</Table.Th>
<Table.Td>{data.versionInfo.goVersion}</Table.Td>
</Table.Tr>
</Table.Tbody>
</Table>
</InfoPageCard>
<InfoPageCard title="Runtime information">
<Table layout="fixed">
<Table.Tbody>
<Table.Tr>
<Table.Th>Uptime</Table.Th>
<Table.Td>{data.uptime}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Th>Cluster Name</Table.Th>
<Table.Td>{data.cluster.name}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Th>Cluster Status</Table.Th>
<Table.Td>{data.cluster.status}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Th>Number of Peers</Table.Th>
<Table.Td>{data.cluster.peers.length}</Table.Td>
</Table.Tr>
</Table.Tbody>
</Table>
</InfoPageCard>
<InfoPageCard title="Cluster Peers">
<Table layout="fixed">
<Table.Tbody>
<Table.Tr>
<Table.Th>Peer Name</Table.Th>
<Table.Th>Address</Table.Th>
</Table.Tr>
{data.cluster.peers.map((peer, index) => (
<Table.Tr key={index}>
<Table.Td>{peer.name}</Table.Td>
<Table.Td>{peer.address}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</InfoPageCard>
</InfoPageStack>
);
}