Merge pull request #3262 from prometheus/nexucis/react-app

Initiate the React application with Status page
This commit is contained in:
Julius Volz 2023-08-28 18:02:27 +02:00 committed by GitHub
commit 6cbe2eb21f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 15056 additions and 10 deletions

View File

@ -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
View File

@ -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
View File

@ -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=

37
scripts/compress_assets.sh Executable file
View File

@ -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

83
ui/react-app/.eslintrc.js Normal file
View File

@ -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'],
};

5
ui/react-app/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/
dist/
embed.go

View File

@ -0,0 +1,2 @@
# Build output
dist/

View File

@ -0,0 +1,5 @@
{
"printWidth": 120,
"singleQuote": true,
"trailingComma": "es5"
}

View File

@ -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"

14185
ui/react-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
ui/react-app/package.json Normal file
View File

@ -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"
}
}

29
ui/react-app/src/App.tsx Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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);
});
}

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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'));

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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
}
}
}

22
ui/react-app/ui.go Normal file
View File

@ -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))

View File

@ -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,
},
},
],
},
],
},
};

View File

@ -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;

View File

@ -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;

View File

@ -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)