mirror of https://github.com/prometheus/prometheus
React UI: Support custom path prefixes (#6264)
* React UI: Support custom path prefixes The challenge was that the path prefix can be set dynamically as a flag on Prometheus, but the React app bundle is statically compiled in to expect a given path prefix. By adding a placeholder value to the React app's index.html and replacing it in Prometheus with the right path prefix during serving, this injects Prometheus's path prefix into the React app via a global const. Threading the path prefix into the different React components could have been done with React's Contexts (https://reactjs.org/docs/context.html), but I found the consumer side of context values to be a bit cumbersome (wrapping entire components in context consumers), so I ended up preferring direct threading of the path prefix values to components that needed them. Also, using contexts in tests is more verbose than just passing in path prefix values directly. Fixes https://github.com/prometheus/prometheus/issues/6163 Signed-off-by: Julius Volz <julius.volz@gmail.com> * Review feedback Signed-off-by: Julius Volz <julius.volz@gmail.com>pull/6275/head
parent
5bc935337a
commit
95554074d8
|
@ -8,6 +8,14 @@
|
|||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<!--
|
||||
This constant's placeholder magic value is replaced during serving by Prometheus
|
||||
and set to Prometheus's external URL path. It gets prepended to all links back
|
||||
to Prometheus, both for asset loading as well as API accesses.
|
||||
-->
|
||||
<script>const PATH_PREFIX='PATH_PREFIX_PLACEHOLDER';</script>
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
|
|
|
@ -7,15 +7,17 @@ import { Router } from '@reach/router';
|
|||
import { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList } from './pages';
|
||||
|
||||
describe('App', () => {
|
||||
const app = shallow(<App />);
|
||||
const app = shallow(<App pathPrefix="/path/prefix" />);
|
||||
|
||||
it('navigates', () => {
|
||||
expect(app.find(Navigation)).toHaveLength(1);
|
||||
});
|
||||
it('routes', () => {
|
||||
[Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList].forEach(component =>
|
||||
expect(app.find(component)).toHaveLength(1)
|
||||
);
|
||||
[Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList].forEach(component => {
|
||||
const c = app.find(component);
|
||||
expect(c).toHaveLength(1);
|
||||
expect(c.prop('pathPrefix')).toBe('/path/prefix');
|
||||
});
|
||||
expect(app.find(Router)).toHaveLength(1);
|
||||
expect(app.find(Container)).toHaveLength(1);
|
||||
});
|
||||
|
|
|
@ -1,31 +1,32 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import Navigation from './Navbar';
|
||||
import { Container } from 'reactstrap';
|
||||
|
||||
import './App.css';
|
||||
import { Router } from '@reach/router';
|
||||
import { Router, Redirect } from '@reach/router';
|
||||
import { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList } from './pages';
|
||||
import PathPrefixProps from './PathPrefixProps';
|
||||
|
||||
class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Navigation />
|
||||
<Container fluid style={{ paddingTop: 70 }}>
|
||||
<Router basepath="/new">
|
||||
<PanelList path="/graph" />
|
||||
<Alerts path="/alerts" />
|
||||
<Config path="/config" />
|
||||
<Flags path="/flags" />
|
||||
<Rules path="/rules" />
|
||||
<Services path="/service-discovery" />
|
||||
<Status path="/status" />
|
||||
<Targets path="/targets" />
|
||||
</Router>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
const App: FC<PathPrefixProps> = ({ pathPrefix }) => {
|
||||
return (
|
||||
<>
|
||||
<Navigation pathPrefix={pathPrefix} />
|
||||
<Container fluid style={{ paddingTop: 70 }}>
|
||||
<Router basepath={`${pathPrefix}/new`}>
|
||||
<Redirect from="/" to={`${pathPrefix}/new/graph`} />
|
||||
|
||||
<PanelList path="/graph" pathPrefix={pathPrefix} />
|
||||
<Alerts path="/alerts" pathPrefix={pathPrefix} />
|
||||
<Config path="/config" pathPrefix={pathPrefix} />
|
||||
<Flags path="/flags" pathPrefix={pathPrefix} />
|
||||
<Rules path="/rules" pathPrefix={pathPrefix} />
|
||||
<Services path="/service-discovery" pathPrefix={pathPrefix} />
|
||||
<Status path="/status" pathPrefix={pathPrefix} />
|
||||
<Targets path="/targets" pathPrefix={pathPrefix} />
|
||||
</Router>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { Link } from '@reach/router';
|
||||
import {
|
||||
Collapse,
|
||||
|
@ -12,25 +12,26 @@ import {
|
|||
DropdownMenu,
|
||||
DropdownItem,
|
||||
} from 'reactstrap';
|
||||
import PathPrefixProps from './PathPrefixProps';
|
||||
|
||||
const Navigation = () => {
|
||||
const Navigation: FC<PathPrefixProps> = ({ pathPrefix }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const toggle = () => setIsOpen(!isOpen);
|
||||
return (
|
||||
<Navbar className="mb-3" dark color="dark" expand="md" fixed="top">
|
||||
<NavbarToggler onClick={toggle} />
|
||||
<Link className="pt-0 navbar-brand" to="/new/graph">
|
||||
<Link className="pt-0 navbar-brand" to={`${pathPrefix}/new/graph`}>
|
||||
Prometheus
|
||||
</Link>
|
||||
<Collapse isOpen={isOpen} navbar style={{ justifyContent: 'space-between' }}>
|
||||
<Nav className="ml-0" navbar>
|
||||
<NavItem>
|
||||
<NavLink tag={Link} to="/new/alerts">
|
||||
<NavLink tag={Link} to={`${pathPrefix}/new/alerts`}>
|
||||
Alerts
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink tag={Link} to="/new/graph">
|
||||
<NavLink tag={Link} to={`${pathPrefix}/new/graph`}>
|
||||
Graph
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
|
@ -39,22 +40,22 @@ const Navigation = () => {
|
|||
Status
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
<DropdownItem tag={Link} to="/new/status">
|
||||
<DropdownItem tag={Link} to={`${pathPrefix}/new/status`}>
|
||||
Runtime & Build Information
|
||||
</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/flags">
|
||||
<DropdownItem tag={Link} to={`${pathPrefix}/new/flags`}>
|
||||
Command-Line Flags
|
||||
</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/config">
|
||||
<DropdownItem tag={Link} to={`${pathPrefix}/new/config`}>
|
||||
Configuration
|
||||
</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/rules">
|
||||
<DropdownItem tag={Link} to={`${pathPrefix}/new/rules`}>
|
||||
Rules
|
||||
</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/targets">
|
||||
<DropdownItem tag={Link} to={`${pathPrefix}/new/targets`}>
|
||||
Targets
|
||||
</DropdownItem>
|
||||
<DropdownItem tag={Link} to="/new/service-discovery">
|
||||
<DropdownItem tag={Link} to={`${pathPrefix}/new/service-discovery`}>
|
||||
Service Discovery
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
|
@ -63,7 +64,7 @@ const Navigation = () => {
|
|||
<NavLink href="https://prometheus.io/docs/prometheus/latest/getting_started/">Help</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink tag={Link} to="../../graph">
|
||||
<NavLink tag={Link} to={pathPrefix}>
|
||||
Classic UI
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
|
|
|
@ -10,6 +10,7 @@ import Graph from './Graph';
|
|||
import DataTable from './DataTable';
|
||||
import TimeInput from './TimeInput';
|
||||
import QueryStatsView, { QueryStats } from './QueryStatsView';
|
||||
import PathPrefixProps from './PathPrefixProps';
|
||||
|
||||
interface PanelProps {
|
||||
options: PanelOptions;
|
||||
|
@ -56,7 +57,7 @@ export const PanelDefaultOptions: PanelOptions = {
|
|||
stacked: false,
|
||||
};
|
||||
|
||||
class Panel extends Component<PanelProps, PanelState> {
|
||||
class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
|
||||
private abortInFlightFetch: (() => void) | null = null;
|
||||
|
||||
constructor(props: PanelProps) {
|
||||
|
@ -123,21 +124,21 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
let path: string;
|
||||
switch (this.props.options.type) {
|
||||
case 'graph':
|
||||
path = '../../api/v1/query_range';
|
||||
path = '/api/v1/query_range';
|
||||
params.append('start', startTime.toString());
|
||||
params.append('end', endTime.toString());
|
||||
params.append('step', resolution.toString());
|
||||
// TODO path prefix here and elsewhere.
|
||||
break;
|
||||
case 'table':
|
||||
path = '../../api/v1/query';
|
||||
path = '/api/v1/query';
|
||||
params.append('time', endTime.toString());
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid panel type "' + this.props.options.type + '"');
|
||||
}
|
||||
|
||||
fetch(`${path}?${params}`, { cache: 'no-store', signal: abortController.signal })
|
||||
fetch(`${this.props.pathPrefix}${path}?${params}`, { cache: 'no-store', signal: abortController.signal })
|
||||
.then(resp => resp.json())
|
||||
.then(json => {
|
||||
if (json.status !== 'success') {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
interface PathPrefixProps {
|
||||
pathPrefix?: string;
|
||||
}
|
||||
|
||||
export default PathPrefixProps;
|
|
@ -4,4 +4,16 @@ import ReactDOM from 'react-dom';
|
|||
import App from './App';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
// Declared/defined in public/index.html, value replaced by Prometheus when serving bundle.
|
||||
declare const PATH_PREFIX: string;
|
||||
|
||||
let prefix = PATH_PREFIX;
|
||||
if (PATH_PREFIX === 'PATH_PREFIX_PLACEHOLDER' || PATH_PREFIX === '/') {
|
||||
// Either we are running the app outside of Prometheus, so the placeholder value in
|
||||
// the index.html didn't get replaced, or we have a '/' prefix, which we also need to
|
||||
// normalize to '' to make concatenations work (prefixes like '/foo/bar/' already get
|
||||
// their trailing slash stripped by Prometheus).
|
||||
prefix = '';
|
||||
}
|
||||
|
||||
ReactDOM.render(<App pathPrefix={prefix} />, document.getElementById('root'));
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { FC } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import PathPrefixProps from '../PathPrefixProps';
|
||||
|
||||
const Alerts: FC<RouteComponentProps> = props => <div>Alerts page</div>;
|
||||
const Alerts: FC<RouteComponentProps & PathPrefixProps> = props => <div>Alerts page</div>;
|
||||
|
||||
export default Alerts;
|
||||
|
|
|
@ -5,11 +5,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import { useFetch } from '../utils/useFetch';
|
||||
import PathPrefixProps from '../PathPrefixProps';
|
||||
|
||||
import './Config.css';
|
||||
|
||||
const Config: FC<RouteComponentProps> = () => {
|
||||
const { response, error } = useFetch('../api/v1/status/config');
|
||||
const Config: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
|
||||
const { response, error } = useFetch(`${pathPrefix}/api/v1/status/config`);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const config = response && response.data.yaml;
|
||||
|
|
|
@ -73,11 +73,11 @@ describe('Flags', () => {
|
|||
|
||||
let flags: ReactWrapper;
|
||||
await act(async () => {
|
||||
flags = mount(<Flags />);
|
||||
flags = mount(<Flags pathPrefix="/path/prefix" />);
|
||||
});
|
||||
flags.update();
|
||||
|
||||
expect(mock).toHaveBeenCalledWith('../api/v1/status/flags', undefined);
|
||||
expect(mock).toHaveBeenCalledWith('/path/prefix/api/v1/status/flags', undefined);
|
||||
const table = flags.find(Table);
|
||||
expect(table.prop('striped')).toBe(true);
|
||||
|
||||
|
@ -98,11 +98,11 @@ describe('Flags', () => {
|
|||
|
||||
let flags: ReactWrapper;
|
||||
await act(async () => {
|
||||
flags = mount(<Flags />);
|
||||
flags = mount(<Flags pathPrefix="/path/prefix" />);
|
||||
});
|
||||
flags.update();
|
||||
|
||||
expect(mock).toHaveBeenCalledWith('../api/v1/status/flags', undefined);
|
||||
expect(mock).toHaveBeenCalledWith('/path/prefix/api/v1/status/flags', undefined);
|
||||
const alert = flags.find(Alert);
|
||||
expect(alert.prop('color')).toBe('danger');
|
||||
expect(alert.text()).toContain('error loading flags');
|
||||
|
|
|
@ -4,13 +4,14 @@ import { Alert, Table } from 'reactstrap';
|
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useFetch } from '../utils/useFetch';
|
||||
import PathPrefixProps from '../PathPrefixProps';
|
||||
|
||||
export interface FlagMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const Flags: FC<RouteComponentProps> = () => {
|
||||
const { response, error } = useFetch('../api/v1/status/flags');
|
||||
const Flags: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
|
||||
const { response, error } = useFetch(`${pathPrefix}/api/v1/status/flags`);
|
||||
|
||||
const body = () => {
|
||||
const flags: FlagMap = response && response.data;
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React, { Component, ChangeEvent } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
|
||||
import { Alert, Button, Col, Row } from 'reactstrap';
|
||||
|
||||
import Panel, { PanelOptions, PanelDefaultOptions } from '../Panel';
|
||||
import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString } from '../utils/urlParams';
|
||||
import Checkbox from '../Checkbox';
|
||||
import PathPrefixProps from '../PathPrefixProps';
|
||||
|
||||
export type MetricGroup = { title: string; items: string[] };
|
||||
|
||||
|
@ -19,9 +21,9 @@ interface PanelListState {
|
|||
timeDriftError: string | null;
|
||||
}
|
||||
|
||||
class PanelList extends Component<any, PanelListState> {
|
||||
class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelListState> {
|
||||
private key = 0;
|
||||
constructor(props: any) {
|
||||
constructor(props: PathPrefixProps) {
|
||||
super(props);
|
||||
|
||||
const urlPanels = decodePanelOptionsFromQueryString(window.location.search);
|
||||
|
@ -44,7 +46,7 @@ class PanelList extends Component<any, PanelListState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetch('../../api/v1/label/__name__/values', { cache: 'no-store' })
|
||||
fetch(`${this.props.pathPrefix}/api/v1/label/__name__/values`, { cache: 'no-store' })
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
|
@ -58,7 +60,7 @@ class PanelList extends Component<any, PanelListState> {
|
|||
.catch(error => this.setState({ fetchMetricsError: error.message }));
|
||||
|
||||
const browserTime = new Date().getTime() / 1000;
|
||||
fetch('../../api/v1/query?query=time()', { cache: 'no-store' })
|
||||
fetch(`${this.props.pathPrefix}/api/v1/query?query=time()`, { cache: 'no-store' })
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
|
@ -161,6 +163,7 @@ class PanelList extends Component<any, PanelListState> {
|
|||
|
||||
render() {
|
||||
const { metricNames, pastQueries, timeDriftError, fetchMetricsError } = this.state;
|
||||
const { pathPrefix } = this.props;
|
||||
return (
|
||||
<>
|
||||
<Row className="mb-2">
|
||||
|
@ -200,6 +203,7 @@ class PanelList extends Component<any, PanelListState> {
|
|||
removePanel={() => this.removePanel(p.key)}
|
||||
metricNames={metricNames}
|
||||
pastQueries={pastQueries}
|
||||
pathPrefix={pathPrefix}
|
||||
/>
|
||||
))}
|
||||
<Button color="primary" className="add-panel-btn" onClick={this.addPanel}>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { FC } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import PathPrefixProps from '../PathPrefixProps';
|
||||
|
||||
const Rules: FC<RouteComponentProps> = () => <div>Rules page</div>;
|
||||
const Rules: FC<RouteComponentProps & PathPrefixProps> = () => <div>Rules page</div>;
|
||||
|
||||
export default Rules;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { FC } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import PathPrefixProps from '../PathPrefixProps';
|
||||
|
||||
const Services: FC<RouteComponentProps> = () => <div>Services page</div>;
|
||||
const Services: FC<RouteComponentProps & PathPrefixProps> = () => <div>Services page</div>;
|
||||
|
||||
export default Services;
|
||||
|
|
|
@ -19,11 +19,11 @@ describe('Status', () => {
|
|||
});
|
||||
it('should fetch proper API endpoints', () => {
|
||||
const useFetchSpy = jest.spyOn(useFetch, 'default');
|
||||
shallow(<Status />);
|
||||
shallow(<Status pathPrefix="/path/prefix" />);
|
||||
expect(useFetchSpy).toHaveBeenCalledWith([
|
||||
'../api/v1/status/runtimeinfo',
|
||||
'../api/v1/status/buildinfo',
|
||||
'../api/v1/alertmanagers',
|
||||
'/path/prefix/api/v1/status/runtimeinfo',
|
||||
'/path/prefix/api/v1/status/buildinfo',
|
||||
'/path/prefix/api/v1/alertmanagers',
|
||||
]);
|
||||
});
|
||||
describe('Snapshot testing', () => {
|
||||
|
|
|
@ -5,8 +5,9 @@ import useFetches from '../hooks/useFetches';
|
|||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import PathPrefixProps from '../PathPrefixProps';
|
||||
|
||||
const ENDPOINTS = ['../api/v1/status/runtimeinfo', '../api/v1/status/buildinfo', '../api/v1/alertmanagers'];
|
||||
const ENDPOINTS = ['/api/v1/status/runtimeinfo', '/api/v1/status/buildinfo', '/api/v1/alertmanagers'];
|
||||
const sectionTitles = ['Runtime Information', 'Build Information', 'Alertmanagers'];
|
||||
|
||||
interface StatusConfig {
|
||||
|
@ -54,8 +55,19 @@ export const statusConfig: StatusConfig = {
|
|||
droppedAlertmanagers: { skip: true },
|
||||
};
|
||||
|
||||
const Status = () => {
|
||||
const { response: data, error, isLoading } = useFetches<StatusPageState[]>(ENDPOINTS);
|
||||
const endpointsMemo: { [prefix: string]: string[] } = {};
|
||||
|
||||
const Status: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' }) => {
|
||||
if (!endpointsMemo[pathPrefix]) {
|
||||
// TODO: Come up with a nicer solution for this?
|
||||
//
|
||||
// The problem is that there's an infinite reload loop if the endpoints array is
|
||||
// reconstructed on every render, as the dependency checking in useFetches()
|
||||
// then thinks that something has changed... the whole useFetches() should
|
||||
// probably removed and solved differently (within the component?) somehow.
|
||||
endpointsMemo[pathPrefix] = ENDPOINTS.map(ep => `${pathPrefix}${ep}`);
|
||||
}
|
||||
const { response: data, error, isLoading } = useFetches<StatusPageState[]>(endpointsMemo[pathPrefix]);
|
||||
if (error) {
|
||||
return (
|
||||
<Alert color="danger">
|
||||
|
@ -73,8 +85,9 @@ const Status = () => {
|
|||
/>
|
||||
);
|
||||
}
|
||||
return data
|
||||
? data.map((statuses, i) => {
|
||||
return data ? (
|
||||
<>
|
||||
{data.map((statuses, i) => {
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<h2>{sectionTitles[i]}</h2>
|
||||
|
@ -101,8 +114,9 @@ const Status = () => {
|
|||
</Table>
|
||||
</Fragment>
|
||||
);
|
||||
})
|
||||
: null;
|
||||
})}
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default Status as FC<RouteComponentProps>;
|
||||
export default Status;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { FC } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import PathPrefixProps from '../PathPrefixProps';
|
||||
|
||||
const Targets: FC<RouteComponentProps> = () => <div>Targets page</div>;
|
||||
const Targets: FC<RouteComponentProps & PathPrefixProps> = () => <div>Targets page</div>;
|
||||
|
||||
export default Targets;
|
||||
|
|
|
@ -1,465 +1,453 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Status Snapshot testing should match table snapshot 1`] = `
|
||||
Array [
|
||||
<Fragment
|
||||
key="0"
|
||||
<Fragment>
|
||||
<h2>
|
||||
Runtime Information
|
||||
</h2>
|
||||
<Table
|
||||
bordered={true}
|
||||
className="h-auto"
|
||||
responsiveTag="div"
|
||||
size="sm"
|
||||
striped={true}
|
||||
tag="table"
|
||||
>
|
||||
<h2>
|
||||
Runtime Information
|
||||
</h2>
|
||||
<Table
|
||||
bordered={true}
|
||||
className="h-auto"
|
||||
responsiveTag="div"
|
||||
size="sm"
|
||||
striped={true}
|
||||
tag="table"
|
||||
>
|
||||
<tbody>
|
||||
<tr
|
||||
key="startTime"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
<tbody>
|
||||
<tr
|
||||
key="startTime"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
>
|
||||
Start time
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
Wed, 30 Oct 2019 20:03:23 GMT
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="CWD"
|
||||
}
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
}
|
||||
>
|
||||
Working directory
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
/home/boyskila/Desktop/prometheus
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="reloadConfigSuccess"
|
||||
Start time
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
Wed, 30 Oct 2019 20:03:23 GMT
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="CWD"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
>
|
||||
Configuration reload
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
Successful
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="lastConfigTime"
|
||||
}
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
}
|
||||
>
|
||||
Last successful configuration reload
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
2019-10-30T22:03:23+02:00
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="chunkCount"
|
||||
Working directory
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
/home/boyskila/Desktop/prometheus
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="reloadConfigSuccess"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
>
|
||||
Head chunks
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
1383
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="timeSeriesCount"
|
||||
}
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
}
|
||||
>
|
||||
Head time series
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
461
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="corruptionCount"
|
||||
Configuration reload
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
Successful
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="lastConfigTime"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
>
|
||||
WAL corruptions
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
0
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="goroutineCount"
|
||||
}
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
}
|
||||
>
|
||||
Goroutines
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
37
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="GOMAXPROCS"
|
||||
Last successful configuration reload
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
2019-10-30T22:03:23+02:00
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="chunkCount"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
>
|
||||
GOMAXPROCS
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
4
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="GOGC"
|
||||
}
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
}
|
||||
>
|
||||
GOGC
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="GODEBUG"
|
||||
Head chunks
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
1383
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="timeSeriesCount"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
>
|
||||
GODEBUG
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="storageRetention"
|
||||
}
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
Head time series
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
461
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="corruptionCount"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
>
|
||||
Storage retention
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
15d
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Fragment>,
|
||||
<Fragment
|
||||
key="1"
|
||||
}
|
||||
>
|
||||
WAL corruptions
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
0
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="goroutineCount"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
}
|
||||
>
|
||||
Goroutines
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
37
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="GOMAXPROCS"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
}
|
||||
>
|
||||
GOMAXPROCS
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
4
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="GOGC"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
}
|
||||
>
|
||||
GOGC
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="GODEBUG"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
}
|
||||
>
|
||||
GODEBUG
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="storageRetention"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
}
|
||||
>
|
||||
Storage retention
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
15d
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
<h2>
|
||||
Build Information
|
||||
</h2>
|
||||
<Table
|
||||
bordered={true}
|
||||
className="h-auto"
|
||||
responsiveTag="div"
|
||||
size="sm"
|
||||
striped={true}
|
||||
tag="table"
|
||||
>
|
||||
<h2>
|
||||
Build Information
|
||||
</h2>
|
||||
<Table
|
||||
bordered={true}
|
||||
className="h-auto"
|
||||
responsiveTag="div"
|
||||
size="sm"
|
||||
striped={true}
|
||||
tag="table"
|
||||
>
|
||||
<tbody>
|
||||
<tr
|
||||
key="version"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
<tbody>
|
||||
<tr
|
||||
key="version"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
>
|
||||
version
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="revision"
|
||||
}
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
version
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="revision"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
>
|
||||
revision
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="branch"
|
||||
}
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
revision
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="branch"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
>
|
||||
branch
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="buildUser"
|
||||
}
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
branch
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="buildUser"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
>
|
||||
buildUser
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="buildDate"
|
||||
}
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
buildUser
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="buildDate"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
>
|
||||
buildDate
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="goVersion"
|
||||
}
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
buildDate
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
key="goVersion"
|
||||
>
|
||||
<th
|
||||
className="capitalize-title"
|
||||
style={
|
||||
Object {
|
||||
"width": "35%",
|
||||
}
|
||||
>
|
||||
goVersion
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
go1.13.3
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Fragment>,
|
||||
<Fragment
|
||||
key="2"
|
||||
}
|
||||
>
|
||||
goVersion
|
||||
</th>
|
||||
<td
|
||||
className="text-break"
|
||||
>
|
||||
go1.13.3
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
<h2>
|
||||
Alertmanagers
|
||||
</h2>
|
||||
<Table
|
||||
bordered={true}
|
||||
className="h-auto"
|
||||
responsiveTag="div"
|
||||
size="sm"
|
||||
striped={true}
|
||||
tag="table"
|
||||
>
|
||||
<h2>
|
||||
Alertmanagers
|
||||
</h2>
|
||||
<Table
|
||||
bordered={true}
|
||||
className="h-auto"
|
||||
responsiveTag="div"
|
||||
size="sm"
|
||||
striped={true}
|
||||
tag="table"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
Endpoint
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
key="https://1.2.3.4:9093/api/v1/alerts"
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
href="https://1.2.3.4:9093/api/v1/alerts"
|
||||
>
|
||||
https://1.2.3.4:9093
|
||||
</a>
|
||||
/api/v1/alerts
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="https://1.2.3.5:9093/api/v1/alerts"
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
href="https://1.2.3.5:9093/api/v1/alerts"
|
||||
>
|
||||
https://1.2.3.5:9093
|
||||
</a>
|
||||
/api/v1/alerts
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="https://1.2.3.6:9093/api/v1/alerts"
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
href="https://1.2.3.6:9093/api/v1/alerts"
|
||||
>
|
||||
https://1.2.3.6:9093
|
||||
</a>
|
||||
/api/v1/alerts
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="https://1.2.3.7:9093/api/v1/alerts"
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
href="https://1.2.3.7:9093/api/v1/alerts"
|
||||
>
|
||||
https://1.2.3.7:9093
|
||||
</a>
|
||||
/api/v1/alerts
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="https://1.2.3.8:9093/api/v1/alerts"
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
href="https://1.2.3.8:9093/api/v1/alerts"
|
||||
>
|
||||
https://1.2.3.8:9093
|
||||
</a>
|
||||
/api/v1/alerts
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="https://1.2.3.9:9093/api/v1/alerts"
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
href="https://1.2.3.9:9093/api/v1/alerts"
|
||||
>
|
||||
https://1.2.3.9:9093
|
||||
</a>
|
||||
/api/v1/alerts
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Fragment>,
|
||||
]
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
Endpoint
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
key="https://1.2.3.4:9093/api/v1/alerts"
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
href="https://1.2.3.4:9093/api/v1/alerts"
|
||||
>
|
||||
https://1.2.3.4:9093
|
||||
</a>
|
||||
/api/v1/alerts
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="https://1.2.3.5:9093/api/v1/alerts"
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
href="https://1.2.3.5:9093/api/v1/alerts"
|
||||
>
|
||||
https://1.2.3.5:9093
|
||||
</a>
|
||||
/api/v1/alerts
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="https://1.2.3.6:9093/api/v1/alerts"
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
href="https://1.2.3.6:9093/api/v1/alerts"
|
||||
>
|
||||
https://1.2.3.6:9093
|
||||
</a>
|
||||
/api/v1/alerts
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="https://1.2.3.7:9093/api/v1/alerts"
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
href="https://1.2.3.7:9093/api/v1/alerts"
|
||||
>
|
||||
https://1.2.3.7:9093
|
||||
</a>
|
||||
/api/v1/alerts
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="https://1.2.3.8:9093/api/v1/alerts"
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
href="https://1.2.3.8:9093/api/v1/alerts"
|
||||
>
|
||||
https://1.2.3.8:9093
|
||||
</a>
|
||||
/api/v1/alerts
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="https://1.2.3.9:9093/api/v1/alerts"
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
href="https://1.2.3.9:9093/api/v1/alerts"
|
||||
>
|
||||
https://1.2.3.9:9093
|
||||
</a>
|
||||
/api/v1/alerts
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Fragment>
|
||||
`;
|
||||
|
|
31
web/web.go
31
web/web.go
|
@ -14,6 +14,7 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
@ -72,7 +73,7 @@ var (
|
|||
localhostRepresentations = []string{"127.0.0.1", "localhost"}
|
||||
|
||||
// Paths that are handled by the React / Reach router that should all be served the main React app's index.html.
|
||||
reactAppPaths = []string{
|
||||
reactRouterPaths = []string{
|
||||
"/",
|
||||
"/alerts",
|
||||
"/config",
|
||||
|
@ -347,15 +348,33 @@ func New(logger log.Logger, o *Options) *Handler {
|
|||
|
||||
router.Get("/new/*filepath", func(w http.ResponseWriter, r *http.Request) {
|
||||
p := route.Param(r.Context(), "filepath")
|
||||
r.URL.Path = path.Join("/static/react/", p)
|
||||
|
||||
for _, rp := range reactAppPaths {
|
||||
if p == rp {
|
||||
r.URL.Path = "/static/react/"
|
||||
break
|
||||
// For paths that the React/Reach router handles, we want to serve the
|
||||
// index.html, but with replaced path prefix placeholder.
|
||||
for _, rp := range reactRouterPaths {
|
||||
if p != rp {
|
||||
continue
|
||||
}
|
||||
|
||||
f, err := ui.Assets.Open("/static/react/index.html")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Error opening React index.html: %v", err)
|
||||
return
|
||||
}
|
||||
idx, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Error reading React index.html: %v", err)
|
||||
return
|
||||
}
|
||||
prefixedIdx := bytes.ReplaceAll(idx, []byte("PATH_PREFIX_PLACEHOLDER"), []byte(o.ExternalURL.Path))
|
||||
w.Write(prefixedIdx)
|
||||
return
|
||||
}
|
||||
|
||||
// For all other paths, serve auxiliary assets.
|
||||
r.URL.Path = path.Join("/static/react/", p)
|
||||
fs := server.StaticFileServer(ui.Assets)
|
||||
fs.ServeHTTP(w, r)
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue