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:
44
ui/mantine-ui/package-lock.json
generated
44
ui/mantine-ui/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
25
ui/mantine-ui/src/components/InfoPageCard.tsx
Normal file
25
ui/mantine-ui/src/components/InfoPageCard.tsx
Normal 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;
|
||||
12
ui/mantine-ui/src/components/InfoPageStack.tsx
Normal file
12
ui/mantine-ui/src/components/InfoPageStack.tsx
Normal 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;
|
||||
97
ui/mantine-ui/src/highlightjs.css
Normal file
97
ui/mantine-ui/src/highlightjs.css
Normal 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;
|
||||
}
|
||||
}
|
||||
17
ui/mantine-ui/src/pages/Config.page.tsx
Normal file
17
ui/mantine-ui/src/pages/Config.page.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
80
ui/mantine-ui/src/pages/Status.page.tsx
Normal file
80
ui/mantine-ui/src/pages/Status.page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user