implementing the status page
Signed-off-by: Augustin Husson <augustin.husson@amadeus.com>
This commit is contained in:
parent
c86c6131ae
commit
d450bed6f1
|
@ -11,10 +11,12 @@
|
|||
"@emotion/react": "^11.9.3",
|
||||
"@emotion/styled": "^11.9.3",
|
||||
"@mui/material": "^5.10.14",
|
||||
"@tanstack/react-query": "^4.7.1",
|
||||
"mdi-material-ui": "^7.4.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-router-dom": "^6.3.0"
|
||||
"react-router-dom": "^6.3.0",
|
||||
"use-query-params": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.0",
|
||||
|
@ -1111,6 +1113,41 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "4.24.10",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.10.tgz",
|
||||
"integrity": "sha512-2QywqXEAGBIUoTdgn1lAB4/C8QEqwXHj2jrCLeYTk2xVGtLiPEUD8jcMoeB2noclbiW2mMt4+Fq7fZStuz3wAQ==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "4.24.10",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.24.10.tgz",
|
||||
"integrity": "sha512-FY1DixytOcNNCydPQXLxuKEV7VSST32CAuJ55BjhDNqASnMLZn+6c30yQBMrODjmWMNwzfjMZnq0Vw7C62Fwow==",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "4.24.10",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-native": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
|
@ -6827,6 +6864,11 @@
|
|||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-query-params": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-2.0.2.tgz",
|
||||
"integrity": "sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q=="
|
||||
},
|
||||
"node_modules/serve-index": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
|
||||
|
@ -7649,6 +7691,26 @@
|
|||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-query-params": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.0.tgz",
|
||||
"integrity": "sha512-MPBwXVZYzFeJEdjv0YgPNFsafUOM8WTpwBEZfNEMlyzbTsf2c+ZpOBkdM95/w4rxzk4eVO3E4DW7v33+VDbiQw==",
|
||||
"dependencies": {
|
||||
"serialize-query-params": "^2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
@ -8818,6 +8880,20 @@
|
|||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz",
|
||||
"integrity": "sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA=="
|
||||
},
|
||||
"@tanstack/query-core": {
|
||||
"version": "4.24.10",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.10.tgz",
|
||||
"integrity": "sha512-2QywqXEAGBIUoTdgn1lAB4/C8QEqwXHj2jrCLeYTk2xVGtLiPEUD8jcMoeB2noclbiW2mMt4+Fq7fZStuz3wAQ=="
|
||||
},
|
||||
"@tanstack/react-query": {
|
||||
"version": "4.24.10",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.24.10.tgz",
|
||||
"integrity": "sha512-FY1DixytOcNNCydPQXLxuKEV7VSST32CAuJ55BjhDNqASnMLZn+6c30yQBMrODjmWMNwzfjMZnq0Vw7C62Fwow==",
|
||||
"requires": {
|
||||
"@tanstack/query-core": "4.24.10",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
|
@ -13129,6 +13205,11 @@
|
|||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"serialize-query-params": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-2.0.2.tgz",
|
||||
"integrity": "sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q=="
|
||||
},
|
||||
"serve-index": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
|
||||
|
@ -13728,6 +13809,20 @@
|
|||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"use-query-params": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.0.tgz",
|
||||
"integrity": "sha512-MPBwXVZYzFeJEdjv0YgPNFsafUOM8WTpwBEZfNEMlyzbTsf2c+ZpOBkdM95/w4rxzk4eVO3E4DW7v33+VDbiQw==",
|
||||
"requires": {
|
||||
"serialize-query-params": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||
"requires": {}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
|
|
@ -13,10 +13,12 @@
|
|||
"@emotion/react": "^11.9.3",
|
||||
"@emotion/styled": "^11.9.3",
|
||||
"@mui/material": "^5.10.14",
|
||||
"@tanstack/react-query": "^4.7.1",
|
||||
"mdi-material-ui": "^7.4.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-router-dom": "^6.3.0"
|
||||
"react-router-dom": "^6.3.0",
|
||||
"use-query-params": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.0",
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { Box } from '@mui/material';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import Navbar from './components/navbar';
|
||||
import Router from './Router';
|
||||
|
||||
// Based on the MUI doc: https://mui.com/material-ui/react-app-bar/#fixed-placement
|
||||
const Offset = styled('div')(({ theme }) => theme.mixins.toolbar);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
|
@ -7,16 +11,16 @@ function App() {
|
|||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<Navbar />
|
||||
<Offset />
|
||||
<Box
|
||||
sx={{
|
||||
paddingBottom: (theme) => theme.spacing(1),
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Router />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
// Other routes are lazy-loaded for code-splitting
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
const ViewStatus = lazy(() => import('./views/ViewStatus'));
|
||||
|
||||
function Router() {
|
||||
return (
|
||||
<Suspense>
|
||||
<Routes>
|
||||
<Route path="/status" element={<ViewStatus />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export default Router;
|
|
@ -0,0 +1,41 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import buildURL from '../utils/url-builder';
|
||||
import { fetchJson } from '../utils/fetch';
|
||||
|
||||
const resource = 'status';
|
||||
|
||||
export interface AMStatusClusterPeersInfo {
|
||||
address: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AMStatusClusterInfo {
|
||||
name: string;
|
||||
peers: AMStatusClusterPeersInfo[];
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface AMStatusVersionInfo {
|
||||
branch: string;
|
||||
buildDate: string;
|
||||
buildUser: string;
|
||||
goVersion: string;
|
||||
revision: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface AMStatus {
|
||||
cluster: AMStatusClusterInfo;
|
||||
uptime: string;
|
||||
versionInfo: AMStatusVersionInfo;
|
||||
config: {
|
||||
original: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function useAMStatus() {
|
||||
return useQuery<AMStatus, Error>([], () => {
|
||||
const url = buildURL({ resource: resource });
|
||||
return fetchJson<AMStatus>(url);
|
||||
});
|
||||
}
|
|
@ -1,12 +1,15 @@
|
|||
import { AppBar, Box, Button, Toolbar, Typography } from '@mui/material';
|
||||
import { AppBar, Box, Button, Stack, Toolbar, Typography } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
|
||||
export default function Navbar(): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<AppBar position='relative'>
|
||||
<Toolbar>
|
||||
<AppBar position={'fixed'} elevation={1}>
|
||||
<Toolbar
|
||||
sx={{
|
||||
backgroundColor: 'lightgrey',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row' }} flexGrow={1}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
@ -14,16 +17,32 @@ export default function Navbar(): JSX.Element {
|
|||
}}
|
||||
>
|
||||
<Typography
|
||||
variant='h1'
|
||||
variant="h6"
|
||||
sx={(theme) => ({
|
||||
marginRight: '1rem',
|
||||
color: theme.palette.common.white,
|
||||
color: theme.palette.common.black,
|
||||
})}
|
||||
>
|
||||
AlertManager
|
||||
</Typography>
|
||||
</Button>
|
||||
<Button variant="text">Alerts</Button>
|
||||
<Button variant="text">Silences</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
navigate('/status');
|
||||
}}
|
||||
>
|
||||
Status
|
||||
</Button>
|
||||
<Button variant="text" target="_blank" href="https://prometheus.io/docs/alerting/latest/alertmanager/">
|
||||
Help
|
||||
</Button>
|
||||
</Box>
|
||||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<Button variant="outlined">New Silence</Button>
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
<meta name="description" content="AlertManager"/>
|
||||
<title>Perses</title>
|
||||
<title>AlertManager</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
|
|
@ -2,16 +2,35 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
function renderApp(container: Element| null) {
|
||||
function renderApp(container: Element | null) {
|
||||
if (container === null) {
|
||||
return;
|
||||
}
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
// react-query uses a default of 3 retries.
|
||||
// This sets the default to 0 retries.
|
||||
// If needed, the number of retries can be overridden in individual useQuery calls.
|
||||
retry: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App/>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<QueryParamProvider adapter={ReactRouter6Adapter}>
|
||||
<App />
|
||||
</QueryParamProvider>
|
||||
</QueryClientProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Calls `global.fetch`, but throws a `FetchError` for non-200 responses.
|
||||
*/
|
||||
export async function fetch(...args: Parameters<typeof global.fetch>) {
|
||||
const response = await global.fetch(...args);
|
||||
if (!response.ok) {
|
||||
throw new FetchError(response);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `global.fetch` and throws a `FetchError` on non-200 responses, but also
|
||||
* decodes the response body as JSON, casting it to type `T`. Returns the
|
||||
* decoded body.
|
||||
*/
|
||||
export async function fetchJson<T>(...args: Parameters<typeof global.fetch>) {
|
||||
const response = await fetch(...args);
|
||||
const json: T = await response.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when fetch returns a non-200 response.
|
||||
*/
|
||||
export class FetchError extends Error {
|
||||
constructor(readonly response: Response) {
|
||||
super(`${response.status} ${response.statusText}`);
|
||||
Object.setPrototypeOf(this, FetchError.prototype);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
const apiPrefix = '/api/v2';
|
||||
|
||||
export type URLParams = {
|
||||
resource: string;
|
||||
queryParams?: URLSearchParams;
|
||||
apiPrefix?: string;
|
||||
};
|
||||
|
||||
export default function buildURL(params: URLParams): string {
|
||||
let url = params.apiPrefix === undefined ? apiPrefix : params.apiPrefix;
|
||||
url = `${url}/${params.resource}`;
|
||||
|
||||
if (params.queryParams !== undefined) {
|
||||
url = `${url}?${params.queryParams.toString()}`;
|
||||
}
|
||||
return url;
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import {
|
||||
Chip,
|
||||
Container,
|
||||
SxProps,
|
||||
Table,
|
||||
TableCell,
|
||||
tableCellClasses,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
TextareaAutosize,
|
||||
Theme,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useAMStatus } from '../client/am-client';
|
||||
|
||||
const tableStyle: SxProps<Theme> = {
|
||||
[`& .${tableCellClasses.root}`]: {
|
||||
borderBottom: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
const tableHeaderStyle: SxProps<Theme> = {
|
||||
fontWeight: 'bold',
|
||||
};
|
||||
|
||||
interface tableCellProperties {
|
||||
header: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
function CustomTableCell(props: tableCellProperties) {
|
||||
const { header, content } = props;
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell variant="head" sx={tableHeaderStyle}>
|
||||
{header}
|
||||
</TableCell>
|
||||
<TableCell>{content}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ViewStatus() {
|
||||
const { data } = useAMStatus();
|
||||
if (data === undefined || data === null) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="h4">Status</Typography>
|
||||
<TableContainer>
|
||||
<Table sx={tableStyle}>
|
||||
<CustomTableCell header="Uptime" content={data.uptime} />
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Typography variant="h4">Cluster Status</Typography>
|
||||
<Table sx={tableStyle}>
|
||||
<CustomTableCell header="Name" content={data.cluster.name} />
|
||||
<TableRow>
|
||||
<TableCell variant="head" sx={tableHeaderStyle}>
|
||||
Status
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip color={data.cluster.status === 'ready' ? 'success' : 'error'} label={data.cluster.status} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
<Typography variant="h4">Version Information</Typography>
|
||||
<TableContainer>
|
||||
<Table sx={tableStyle}>
|
||||
<CustomTableCell header="Branch" content={data.versionInfo.branch} />
|
||||
<CustomTableCell header="Build Date" content={data.versionInfo.buildDate} />
|
||||
<CustomTableCell header="Build User" content={data.versionInfo.buildUser} />
|
||||
<CustomTableCell header="Go Version" content={data.versionInfo.goVersion} />
|
||||
<CustomTableCell header="Revision" content={data.versionInfo.revision} />
|
||||
<CustomTableCell header="Version" content={data.versionInfo.version} />
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Typography variant="h4">Config</Typography>
|
||||
<TextareaAutosize
|
||||
readOnly
|
||||
aria-multiline
|
||||
value={data.config.original}
|
||||
style={{ width: '100%', backgroundColor: 'lightgrey' }}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
|
@ -68,7 +68,7 @@ const devConfig: Configuration = {
|
|||
historyApiFallback: true,
|
||||
allowedHosts: 'all',
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8080',
|
||||
'/api': 'http://localhost:9093',
|
||||
},
|
||||
},
|
||||
cache: true,
|
||||
|
|
Loading…
Reference in New Issue