Merge pull request #3262 from prometheus/nexucis/react-app
Initiate the React application with Status page
This commit is contained in:
commit
6cbe2eb21f
15
Makefile
15
Makefile
|
@ -26,6 +26,21 @@ STATICCHECK_IGNORE =
|
|||
# Will build both the front-end as well as the back-end
|
||||
build-all: assets apiv2 build
|
||||
|
||||
.PHONY: build
|
||||
build: build-react-app assets-compress common-build
|
||||
|
||||
.PHONY: lint
|
||||
lint: assets-compress common-lint
|
||||
|
||||
.PHONY: build-react-app
|
||||
build-react-app:
|
||||
cd ui/react-app && npm install && npm run build
|
||||
|
||||
.PHONY: assets-compress
|
||||
assets-compress:
|
||||
@echo '>> compressing assets'
|
||||
scripts/compress_assets.sh
|
||||
|
||||
.PHONY: assets
|
||||
assets: asset/assets_vfsdata.go
|
||||
|
||||
|
|
1
go.mod
1
go.mod
|
@ -31,6 +31,7 @@ require (
|
|||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.15.1
|
||||
github.com/prometheus/common v0.44.0
|
||||
github.com/prometheus/common/assets v0.2.0
|
||||
github.com/prometheus/common/sigv4 v0.1.0
|
||||
github.com/prometheus/exporter-toolkit v0.10.0
|
||||
github.com/rs/cors v1.9.0
|
||||
|
|
2
go.sum
2
go.sum
|
@ -490,6 +490,8 @@ github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9
|
|||
github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM=
|
||||
github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI=
|
||||
github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4=
|
||||
github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI=
|
||||
github.com/prometheus/exporter-toolkit v0.10.0 h1:yOAzZTi4M22ZzVxD+fhy1URTuNRj/36uQJJ5S8IPza8=
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# compress static assets
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd ui/react-app
|
||||
cp embed.go.tmpl embed.go
|
||||
|
||||
GZIP_OPTS="-fk"
|
||||
# gzip option '-k' may not always exist in the latest gzip available on different distros.
|
||||
if ! gzip -k -h &>/dev/null; then GZIP_OPTS="-f"; fi
|
||||
|
||||
dist="dist"
|
||||
|
||||
if ! [[ -d "${dist}" ]]; then
|
||||
mkdir -p ${dist}
|
||||
echo "<!doctype html>
|
||||
<html lang=\"en\">
|
||||
<head>
|
||||
<meta charset=\"utf-8\">
|
||||
<title>Node</title>
|
||||
<base href=\"/\">
|
||||
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
|
||||
<link rel=\"icon\" type=\"image/x-icon\" href=\"favicon.ico\">
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p> This is the default index, looks like you forget to generate the react app before generating the golang endpoint.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>" > ${dist}/index.html
|
||||
fi
|
||||
|
||||
find dist -type f -name '*.gz' -delete
|
||||
find dist -type f -exec gzip $GZIP_OPTS '{}' \; -print0 | xargs -0 -I % echo %.gz | xargs echo //go:embed >> embed.go
|
||||
echo var embedFS embed.FS >> embed.go
|
|
@ -0,0 +1,83 @@
|
|||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
|
||||
plugins: ['import'],
|
||||
|
||||
env: {
|
||||
commonjs: true,
|
||||
es6: true,
|
||||
jest: true,
|
||||
node: true,
|
||||
browser: true,
|
||||
},
|
||||
|
||||
parser: '@typescript-eslint/parser',
|
||||
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/array-type': [
|
||||
'warn',
|
||||
{
|
||||
default: 'array-simple',
|
||||
},
|
||||
],
|
||||
'import/order': 'warn',
|
||||
// you must disable the base rule as it can report incorrect errors
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error'],
|
||||
|
||||
'react/prop-types': 'off',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
// Not necessary in React 17
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
/**
|
||||
* This library is gigantic and named imports end up slowing down builds/blowing out bundle sizes,
|
||||
* so this prevents that style of import.
|
||||
*/
|
||||
group: ['mdi-material-ui', '!mdi-material-ui/'],
|
||||
message: `
|
||||
Please use the default import from the icon file directly rather than using a named import.
|
||||
|
||||
Good:
|
||||
import IconName from 'mdi-material-ui/IconName';
|
||||
|
||||
Bad:
|
||||
import { IconName } from 'mdi-material-ui';
|
||||
`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
ignorePatterns: ['**/dist'],
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
|
||||
dist/
|
||||
|
||||
embed.go
|
|
@ -0,0 +1,2 @@
|
|||
# Build output
|
||||
dist/
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2023 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package reactApp
|
||||
|
||||
import "embed"
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"name": "@prometheus-io/alertmanager",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rimraf dist/",
|
||||
"start": "webpack serve --config webpack.dev.ts",
|
||||
"build": "webpack --config webpack.prod.ts",
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"lint:fix": "eslint --fix src --ext .ts,.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"use-query-params": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
||||
"@typescript-eslint/parser": "^5.30.7",
|
||||
"css-loader": "^6.7.1",
|
||||
"dotenv-defaults": "^5.0.2",
|
||||
"esbuild-loader": "^2.20.0",
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-webpack-plugin": "^3.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^7.3.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-loader": "^9.3.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.7.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.7.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1",
|
||||
"webpack-merge": "^5.8.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
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 (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Navbar />
|
||||
<Offset />
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Router />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -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="/react-app/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);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import { AppBar, Box, Button, Stack, Toolbar, Typography } from '@mui/material';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function Navbar(): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
return (
|
||||
<AppBar position={'fixed'} elevation={1}>
|
||||
<Toolbar
|
||||
sx={{
|
||||
backgroundColor: 'aliceblue',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row' }} flexGrow={1}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate('/react-app');
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={(theme) => ({
|
||||
marginRight: '1rem',
|
||||
color: theme.palette.common.black,
|
||||
})}
|
||||
>
|
||||
AlertManager
|
||||
</Typography>
|
||||
</Button>
|
||||
<Button variant="text">Alerts</Button>
|
||||
<Button variant="text">Silences</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
navigate('/react-app/status');
|
||||
}}
|
||||
disabled={location.pathname === '/react-app/status'}
|
||||
>
|
||||
Status
|
||||
</Button>
|
||||
<Button variant="text">Settings</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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
<meta name="description" content="AlertManager"/>
|
||||
<title>AlertManager</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,39 @@
|
|||
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) {
|
||||
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>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<QueryParamProvider adapter={ReactRouter6Adapter}>
|
||||
<App />
|
||||
</QueryParamProvider>
|
||||
</QueryClientProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
renderApp(document.getElementById('root'));
|
|
@ -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,15 @@
|
|||
const API_PREFIX = '/api/v2';
|
||||
|
||||
export type URLParams = {
|
||||
resource: string;
|
||||
queryParams?: URLSearchParams;
|
||||
apiPrefix?: string;
|
||||
};
|
||||
|
||||
export default function buildURL({ apiPrefix = API_PREFIX, resource, queryParams }: URLParams): string {
|
||||
let url = `${apiPrefix}/${resource}`;
|
||||
if (queryParams !== undefined) {
|
||||
url = `${url}?${queryParams.toString()}`;
|
||||
}
|
||||
return url;
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
import {
|
||||
Chip,
|
||||
Container,
|
||||
List,
|
||||
ListItem,
|
||||
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 null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="h4">Status</Typography>
|
||||
<TableContainer>
|
||||
<Table size="small" sx={tableStyle}>
|
||||
<CustomTableCell header="Uptime" content={data.uptime} />
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Typography variant="h4">Cluster Status</Typography>
|
||||
<Table size="small" 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>
|
||||
<TableRow>
|
||||
<TableCell variant="head" sx={tableHeaderStyle}>
|
||||
Peers
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<List>
|
||||
{data.cluster.peers.map((peer, i) => {
|
||||
return (
|
||||
<ListItem disablePadding sx={{ display: 'list-item' }} key={i}>
|
||||
<p>
|
||||
Name: {peer.name}
|
||||
<br />
|
||||
Address: {peer.address}
|
||||
</p>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
<Typography variant="h4">Version Information</Typography>
|
||||
<TableContainer>
|
||||
<Table size="small" 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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// Base config for all typescript packages
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"module": "esnext",
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"pretty": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"],
|
||||
// For builds that use ts-node to compile the configs (e.g. webpack, jest)
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2023 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package reactApp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/common/assets"
|
||||
)
|
||||
|
||||
var Assets = http.FS(assets.New(embedFS))
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2023 The Perses Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import path from 'path';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import { Configuration } from 'webpack';
|
||||
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
|
||||
import ESLintWebpackPlugin from 'eslint-webpack-plugin';
|
||||
|
||||
export const commonConfig: Configuration = {
|
||||
entry: path.resolve(__dirname, './src/index.tsx'),
|
||||
output: {
|
||||
path: path.resolve(__dirname, './dist'),
|
||||
publicPath: '/',
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', 'jsx', '.json'],
|
||||
},
|
||||
plugins: [
|
||||
// Generates HTML index page with bundle injected
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(__dirname, './src/index.html'),
|
||||
templateParameters: {},
|
||||
}),
|
||||
// Does TS type-checking in a separate process
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
typescript: {
|
||||
configFile: path.resolve(__dirname, './tsconfig.json'),
|
||||
},
|
||||
}),
|
||||
new ESLintWebpackPlugin({
|
||||
threads: true,
|
||||
files: '../*/src/**/*.{ts,tsx,js,jsx}',
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
// Type-checking happens in separate plugin process
|
||||
transpileOnly: true,
|
||||
projectReferences: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|woff|woff2)$/,
|
||||
type: 'asset/resource',
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif)$/,
|
||||
type: 'asset',
|
||||
},
|
||||
// SVG as React components
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: [
|
||||
{
|
||||
loader: '@svgr/webpack',
|
||||
options: {
|
||||
// Generated React components will support a 'title' prop to render
|
||||
// a <title> inside the <svg>
|
||||
titleProp: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2023 The Perses Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import fs from 'fs';
|
||||
import { Configuration } from 'webpack';
|
||||
import { Configuration as DevServerConfig, ServerConfiguration } from 'webpack-dev-server';
|
||||
import { merge } from 'webpack-merge';
|
||||
import { commonConfig } from './webpack.common';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require('dotenv-defaults').config();
|
||||
|
||||
declare module 'webpack' {
|
||||
interface Configuration {
|
||||
devServer?: DevServerConfig | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Get dev server HTTP options (note: HTTP2 is not currently supported by webpack since we're on Node 16)
|
||||
function getServerConfig(): ServerConfiguration | undefined {
|
||||
// Just use regular HTTP by default
|
||||
if (process.env.HTTPS !== 'true') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Support the same HTTPS options as Creact React App if HTTPS is set
|
||||
if (process.env.SSL_KEY_FILE === undefined || process.env.SSL_CRT_FILE === undefined) {
|
||||
// Use the default self-signed cert
|
||||
return { type: 'https' };
|
||||
}
|
||||
|
||||
// Use a custom cert
|
||||
return {
|
||||
type: 'https',
|
||||
options: {
|
||||
key: fs.readFileSync(process.env.SSL_KEY_FILE),
|
||||
cert: fs.readFileSync(process.env.SSL_CRT_FILE),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Webpack configuration in dev
|
||||
const devConfig: Configuration = {
|
||||
mode: 'development',
|
||||
devtool: 'cheap-module-source-map',
|
||||
|
||||
output: {
|
||||
pathinfo: true,
|
||||
},
|
||||
|
||||
watchOptions: {
|
||||
aggregateTimeout: 300,
|
||||
},
|
||||
|
||||
devServer: {
|
||||
port: parseInt(process.env.PORT ?? '3000'),
|
||||
open: true,
|
||||
server: getServerConfig(),
|
||||
historyApiFallback: true,
|
||||
allowedHosts: 'all',
|
||||
proxy: {
|
||||
'/api': 'http://localhost:9093',
|
||||
},
|
||||
},
|
||||
cache: true,
|
||||
};
|
||||
|
||||
const merged = merge(commonConfig, devConfig);
|
||||
export default merged;
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright 2023 The Perses Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Configuration } from 'webpack';
|
||||
import { merge } from 'webpack-merge';
|
||||
import { ESBuildMinifyPlugin } from 'esbuild-loader';
|
||||
import { commonConfig } from './webpack.common';
|
||||
import path from 'path';
|
||||
|
||||
const prodConfig: Configuration = {
|
||||
output: {
|
||||
path: path.resolve(__dirname, './dist'),
|
||||
publicPath: '/react-app/',
|
||||
},
|
||||
mode: 'production',
|
||||
bail: true,
|
||||
devtool: 'source-map',
|
||||
optimization: {
|
||||
// TODO: Could this also be replaced with swc minifier?
|
||||
minimizer: [new ESBuildMinifyPlugin({ target: 'es2018' })],
|
||||
},
|
||||
};
|
||||
|
||||
const merged = merge(commonConfig, prodConfig);
|
||||
export default merged;
|
59
ui/web.go
59
ui/web.go
|
@ -15,6 +15,7 @@ package ui
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
_ "net/http/pprof" // Comment this line to disable pprof endpoint.
|
||||
"path"
|
||||
|
@ -22,10 +23,17 @@ import (
|
|||
"github.com/go-kit/log"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/prometheus/common/route"
|
||||
"github.com/prometheus/common/server"
|
||||
|
||||
"github.com/prometheus/alertmanager/asset"
|
||||
reactApp "github.com/prometheus/alertmanager/ui/react-app"
|
||||
)
|
||||
|
||||
var reactRouterPaths = []string{
|
||||
"/",
|
||||
"/status",
|
||||
}
|
||||
|
||||
// Register registers handlers to serve files for the web interface.
|
||||
func Register(r *route.Router, reloadCh chan<- chan error, logger log.Logger) {
|
||||
r.Get("/metrics", promhttp.Handler().ServeHTTP)
|
||||
|
@ -62,7 +70,38 @@ func Register(r *route.Router, reloadCh chan<- chan error, logger log.Logger) {
|
|||
fs.ServeHTTP(w, req)
|
||||
})
|
||||
|
||||
r.Post("/-/reload", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
serveReactApp := func(w http.ResponseWriter, r *http.Request) {
|
||||
f, err := reactApp.Assets.Open("/dist/index.html")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Error opening React index.html: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
idx, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Error reading React index.html: %v", err)
|
||||
return
|
||||
}
|
||||
w.Write(idx)
|
||||
}
|
||||
|
||||
// Static files required by the React app.
|
||||
r.Get("/react-app/*filepath", func(w http.ResponseWriter, r *http.Request) {
|
||||
for _, rt := range reactRouterPaths {
|
||||
if r.URL.Path != "/react-app"+rt {
|
||||
continue
|
||||
}
|
||||
serveReactApp(w, r)
|
||||
return
|
||||
}
|
||||
r.URL.Path = path.Join("/dist", route.Param(r.Context(), "filepath"))
|
||||
fs := server.StaticFileServer(reactApp.Assets)
|
||||
fs.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
r.Post("/-/reload", func(w http.ResponseWriter, req *http.Request) {
|
||||
errc := make(chan error)
|
||||
defer close(errc)
|
||||
|
||||
|
@ -70,22 +109,22 @@ func Register(r *route.Router, reloadCh chan<- chan error, logger log.Logger) {
|
|||
if err := <-errc; err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to reload config: %s", err), http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
r.Get("/-/healthy", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
r.Get("/-/healthy", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
r.Head("/-/healthy", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
})
|
||||
r.Head("/-/healthy", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
r.Get("/-/ready", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
})
|
||||
r.Get("/-/ready", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "OK")
|
||||
}))
|
||||
r.Head("/-/ready", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
})
|
||||
r.Head("/-/ready", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
})
|
||||
|
||||
r.Get("/debug/*subpath", http.DefaultServeMux.ServeHTTP)
|
||||
r.Post("/debug/*subpath", http.DefaultServeMux.ServeHTTP)
|
||||
|
|
Loading…
Reference in New Issue