未验证 提交 95554074 编写于 作者: J Julius Volz 提交者: GitHub

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/6163Signed-off-by: NJulius Volz <julius.volz@gmail.com>

* Review feedback
Signed-off-by: NJulius Volz <julius.volz@gmail.com>
上级 5bc93533
...@@ -8,6 +8,14 @@ ...@@ -8,6 +8,14 @@
content="width=device-width, initial-scale=1, shrink-to-fit=no" content="width=device-width, initial-scale=1, shrink-to-fit=no"
/> />
<meta name="theme-color" content="#000000" /> <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 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/ homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/
......
...@@ -7,15 +7,17 @@ import { Router } from '@reach/router'; ...@@ -7,15 +7,17 @@ import { Router } from '@reach/router';
import { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList } from './pages'; import { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList } from './pages';
describe('App', () => { describe('App', () => {
const app = shallow(<App />); const app = shallow(<App pathPrefix="/path/prefix" />);
it('navigates', () => { it('navigates', () => {
expect(app.find(Navigation)).toHaveLength(1); expect(app.find(Navigation)).toHaveLength(1);
}); });
it('routes', () => { it('routes', () => {
[Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList].forEach(component => [Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList].forEach(component => {
expect(app.find(component)).toHaveLength(1) 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(Router)).toHaveLength(1);
expect(app.find(Container)).toHaveLength(1); expect(app.find(Container)).toHaveLength(1);
}); });
......
import React, { Component } from 'react'; import React, { FC } from 'react';
import Navigation from './Navbar'; import Navigation from './Navbar';
import { Container } from 'reactstrap'; import { Container } from 'reactstrap';
import './App.css'; 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 { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList } from './pages';
import PathPrefixProps from './PathPrefixProps';
class App extends Component { const App: FC<PathPrefixProps> = ({ pathPrefix }) => {
render() { return (
return ( <>
<> <Navigation pathPrefix={pathPrefix} />
<Navigation /> <Container fluid style={{ paddingTop: 70 }}>
<Container fluid style={{ paddingTop: 70 }}> <Router basepath={`${pathPrefix}/new`}>
<Router basepath="/new"> <Redirect from="/" to={`${pathPrefix}/new/graph`} />
<PanelList path="/graph" />
<Alerts path="/alerts" /> <PanelList path="/graph" pathPrefix={pathPrefix} />
<Config path="/config" /> <Alerts path="/alerts" pathPrefix={pathPrefix} />
<Flags path="/flags" /> <Config path="/config" pathPrefix={pathPrefix} />
<Rules path="/rules" /> <Flags path="/flags" pathPrefix={pathPrefix} />
<Services path="/service-discovery" /> <Rules path="/rules" pathPrefix={pathPrefix} />
<Status path="/status" /> <Services path="/service-discovery" pathPrefix={pathPrefix} />
<Targets path="/targets" /> <Status path="/status" pathPrefix={pathPrefix} />
</Router> <Targets path="/targets" pathPrefix={pathPrefix} />
</Container> </Router>
</> </Container>
); </>
} );
} };
export default App; export default App;
import React, { useState } from 'react'; import React, { FC, useState } from 'react';
import { Link } from '@reach/router'; import { Link } from '@reach/router';
import { import {
Collapse, Collapse,
...@@ -12,25 +12,26 @@ import { ...@@ -12,25 +12,26 @@ import {
DropdownMenu, DropdownMenu,
DropdownItem, DropdownItem,
} from 'reactstrap'; } from 'reactstrap';
import PathPrefixProps from './PathPrefixProps';
const Navigation = () => { const Navigation: FC<PathPrefixProps> = ({ pathPrefix }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen); const toggle = () => setIsOpen(!isOpen);
return ( return (
<Navbar className="mb-3" dark color="dark" expand="md" fixed="top"> <Navbar className="mb-3" dark color="dark" expand="md" fixed="top">
<NavbarToggler onClick={toggle} /> <NavbarToggler onClick={toggle} />
<Link className="pt-0 navbar-brand" to="/new/graph"> <Link className="pt-0 navbar-brand" to={`${pathPrefix}/new/graph`}>
Prometheus Prometheus
</Link> </Link>
<Collapse isOpen={isOpen} navbar style={{ justifyContent: 'space-between' }}> <Collapse isOpen={isOpen} navbar style={{ justifyContent: 'space-between' }}>
<Nav className="ml-0" navbar> <Nav className="ml-0" navbar>
<NavItem> <NavItem>
<NavLink tag={Link} to="/new/alerts"> <NavLink tag={Link} to={`${pathPrefix}/new/alerts`}>
Alerts Alerts
</NavLink> </NavLink>
</NavItem> </NavItem>
<NavItem> <NavItem>
<NavLink tag={Link} to="/new/graph"> <NavLink tag={Link} to={`${pathPrefix}/new/graph`}>
Graph Graph
</NavLink> </NavLink>
</NavItem> </NavItem>
...@@ -39,22 +40,22 @@ const Navigation = () => { ...@@ -39,22 +40,22 @@ const Navigation = () => {
Status Status
</DropdownToggle> </DropdownToggle>
<DropdownMenu> <DropdownMenu>
<DropdownItem tag={Link} to="/new/status"> <DropdownItem tag={Link} to={`${pathPrefix}/new/status`}>
Runtime & Build Information Runtime & Build Information
</DropdownItem> </DropdownItem>
<DropdownItem tag={Link} to="/new/flags"> <DropdownItem tag={Link} to={`${pathPrefix}/new/flags`}>
Command-Line Flags Command-Line Flags
</DropdownItem> </DropdownItem>
<DropdownItem tag={Link} to="/new/config"> <DropdownItem tag={Link} to={`${pathPrefix}/new/config`}>
Configuration Configuration
</DropdownItem> </DropdownItem>
<DropdownItem tag={Link} to="/new/rules"> <DropdownItem tag={Link} to={`${pathPrefix}/new/rules`}>
Rules Rules
</DropdownItem> </DropdownItem>
<DropdownItem tag={Link} to="/new/targets"> <DropdownItem tag={Link} to={`${pathPrefix}/new/targets`}>
Targets Targets
</DropdownItem> </DropdownItem>
<DropdownItem tag={Link} to="/new/service-discovery"> <DropdownItem tag={Link} to={`${pathPrefix}/new/service-discovery`}>
Service Discovery Service Discovery
</DropdownItem> </DropdownItem>
</DropdownMenu> </DropdownMenu>
...@@ -63,7 +64,7 @@ const Navigation = () => { ...@@ -63,7 +64,7 @@ const Navigation = () => {
<NavLink href="https://prometheus.io/docs/prometheus/latest/getting_started/">Help</NavLink> <NavLink href="https://prometheus.io/docs/prometheus/latest/getting_started/">Help</NavLink>
</NavItem> </NavItem>
<NavItem> <NavItem>
<NavLink tag={Link} to="../../graph"> <NavLink tag={Link} to={pathPrefix}>
Classic UI Classic UI
</NavLink> </NavLink>
</NavItem> </NavItem>
......
...@@ -10,6 +10,7 @@ import Graph from './Graph'; ...@@ -10,6 +10,7 @@ import Graph from './Graph';
import DataTable from './DataTable'; import DataTable from './DataTable';
import TimeInput from './TimeInput'; import TimeInput from './TimeInput';
import QueryStatsView, { QueryStats } from './QueryStatsView'; import QueryStatsView, { QueryStats } from './QueryStatsView';
import PathPrefixProps from './PathPrefixProps';
interface PanelProps { interface PanelProps {
options: PanelOptions; options: PanelOptions;
...@@ -56,7 +57,7 @@ export const PanelDefaultOptions: PanelOptions = { ...@@ -56,7 +57,7 @@ export const PanelDefaultOptions: PanelOptions = {
stacked: false, stacked: false,
}; };
class Panel extends Component<PanelProps, PanelState> { class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
private abortInFlightFetch: (() => void) | null = null; private abortInFlightFetch: (() => void) | null = null;
constructor(props: PanelProps) { constructor(props: PanelProps) {
...@@ -123,21 +124,21 @@ class Panel extends Component<PanelProps, PanelState> { ...@@ -123,21 +124,21 @@ class Panel extends Component<PanelProps, PanelState> {
let path: string; let path: string;
switch (this.props.options.type) { switch (this.props.options.type) {
case 'graph': case 'graph':
path = '../../api/v1/query_range'; path = '/api/v1/query_range';
params.append('start', startTime.toString()); params.append('start', startTime.toString());
params.append('end', endTime.toString()); params.append('end', endTime.toString());
params.append('step', resolution.toString()); params.append('step', resolution.toString());
// TODO path prefix here and elsewhere. // TODO path prefix here and elsewhere.
break; break;
case 'table': case 'table':
path = '../../api/v1/query'; path = '/api/v1/query';
params.append('time', endTime.toString()); params.append('time', endTime.toString());
break; break;
default: default:
throw new Error('Invalid panel type "' + this.props.options.type + '"'); 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(resp => resp.json())
.then(json => { .then(json => {
if (json.status !== 'success') { if (json.status !== 'success') {
......
interface PathPrefixProps {
pathPrefix?: string;
}
export default PathPrefixProps;
...@@ -4,4 +4,16 @@ import ReactDOM from 'react-dom'; ...@@ -4,4 +4,16 @@ import ReactDOM from 'react-dom';
import App from './App'; import App from './App';
import 'bootstrap/dist/css/bootstrap.min.css'; 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'));
import React, { FC } from 'react'; import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; 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; export default Alerts;
...@@ -5,11 +5,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; ...@@ -5,11 +5,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import { useFetch } from '../utils/useFetch'; import { useFetch } from '../utils/useFetch';
import PathPrefixProps from '../PathPrefixProps';
import './Config.css'; import './Config.css';
const Config: FC<RouteComponentProps> = () => { const Config: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
const { response, error } = useFetch('../api/v1/status/config'); const { response, error } = useFetch(`${pathPrefix}/api/v1/status/config`);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const config = response && response.data.yaml; const config = response && response.data.yaml;
......
...@@ -73,11 +73,11 @@ describe('Flags', () => { ...@@ -73,11 +73,11 @@ describe('Flags', () => {
let flags: ReactWrapper; let flags: ReactWrapper;
await act(async () => { await act(async () => {
flags = mount(<Flags />); flags = mount(<Flags pathPrefix="/path/prefix" />);
}); });
flags.update(); 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); const table = flags.find(Table);
expect(table.prop('striped')).toBe(true); expect(table.prop('striped')).toBe(true);
...@@ -98,11 +98,11 @@ describe('Flags', () => { ...@@ -98,11 +98,11 @@ describe('Flags', () => {
let flags: ReactWrapper; let flags: ReactWrapper;
await act(async () => { await act(async () => {
flags = mount(<Flags />); flags = mount(<Flags pathPrefix="/path/prefix" />);
}); });
flags.update(); 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); const alert = flags.find(Alert);
expect(alert.prop('color')).toBe('danger'); expect(alert.prop('color')).toBe('danger');
expect(alert.text()).toContain('error loading flags'); expect(alert.text()).toContain('error loading flags');
......
...@@ -4,13 +4,14 @@ import { Alert, Table } from 'reactstrap'; ...@@ -4,13 +4,14 @@ import { Alert, Table } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import { useFetch } from '../utils/useFetch'; import { useFetch } from '../utils/useFetch';
import PathPrefixProps from '../PathPrefixProps';
export interface FlagMap { export interface FlagMap {
[key: string]: string; [key: string]: string;
} }
const Flags: FC<RouteComponentProps> = () => { const Flags: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
const { response, error } = useFetch('../api/v1/status/flags'); const { response, error } = useFetch(`${pathPrefix}/api/v1/status/flags`);
const body = () => { const body = () => {
const flags: FlagMap = response && response.data; const flags: FlagMap = response && response.data;
......
import React, { Component, ChangeEvent } from 'react'; import React, { Component, ChangeEvent } from 'react';
import { RouteComponentProps } from '@reach/router';
import { Alert, Button, Col, Row } from 'reactstrap'; import { Alert, Button, Col, Row } from 'reactstrap';
import Panel, { PanelOptions, PanelDefaultOptions } from '../Panel'; import Panel, { PanelOptions, PanelDefaultOptions } from '../Panel';
import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString } from '../utils/urlParams'; import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString } from '../utils/urlParams';
import Checkbox from '../Checkbox'; import Checkbox from '../Checkbox';
import PathPrefixProps from '../PathPrefixProps';
export type MetricGroup = { title: string; items: string[] }; export type MetricGroup = { title: string; items: string[] };
...@@ -19,9 +21,9 @@ interface PanelListState { ...@@ -19,9 +21,9 @@ interface PanelListState {
timeDriftError: string | null; timeDriftError: string | null;
} }
class PanelList extends Component<any, PanelListState> { class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelListState> {
private key = 0; private key = 0;
constructor(props: any) { constructor(props: PathPrefixProps) {
super(props); super(props);
const urlPanels = decodePanelOptionsFromQueryString(window.location.search); const urlPanels = decodePanelOptionsFromQueryString(window.location.search);
...@@ -44,7 +46,7 @@ class PanelList extends Component<any, PanelListState> { ...@@ -44,7 +46,7 @@ class PanelList extends Component<any, PanelListState> {
} }
componentDidMount() { componentDidMount() {
fetch('../../api/v1/label/__name__/values', { cache: 'no-store' }) fetch(`${this.props.pathPrefix}/api/v1/label/__name__/values`, { cache: 'no-store' })
.then(resp => { .then(resp => {
if (resp.ok) { if (resp.ok) {
return resp.json(); return resp.json();
...@@ -58,7 +60,7 @@ class PanelList extends Component<any, PanelListState> { ...@@ -58,7 +60,7 @@ class PanelList extends Component<any, PanelListState> {
.catch(error => this.setState({ fetchMetricsError: error.message })); .catch(error => this.setState({ fetchMetricsError: error.message }));
const browserTime = new Date().getTime() / 1000; 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 => { .then(resp => {
if (resp.ok) { if (resp.ok) {
return resp.json(); return resp.json();
...@@ -161,6 +163,7 @@ class PanelList extends Component<any, PanelListState> { ...@@ -161,6 +163,7 @@ class PanelList extends Component<any, PanelListState> {
render() { render() {
const { metricNames, pastQueries, timeDriftError, fetchMetricsError } = this.state; const { metricNames, pastQueries, timeDriftError, fetchMetricsError } = this.state;
const { pathPrefix } = this.props;
return ( return (
<> <>
<Row className="mb-2"> <Row className="mb-2">
...@@ -200,6 +203,7 @@ class PanelList extends Component<any, PanelListState> { ...@@ -200,6 +203,7 @@ class PanelList extends Component<any, PanelListState> {
removePanel={() => this.removePanel(p.key)} removePanel={() => this.removePanel(p.key)}
metricNames={metricNames} metricNames={metricNames}
pastQueries={pastQueries} pastQueries={pastQueries}
pathPrefix={pathPrefix}
/> />
))} ))}
<Button color="primary" className="add-panel-btn" onClick={this.addPanel}> <Button color="primary" className="add-panel-btn" onClick={this.addPanel}>
......
import React, { FC } from 'react'; import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; 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; export default Rules;
import React, { FC } from 'react'; import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; 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; export default Services;
...@@ -19,11 +19,11 @@ describe('Status', () => { ...@@ -19,11 +19,11 @@ describe('Status', () => {
}); });
it('should fetch proper API endpoints', () => { it('should fetch proper API endpoints', () => {
const useFetchSpy = jest.spyOn(useFetch, 'default'); const useFetchSpy = jest.spyOn(useFetch, 'default');
shallow(<Status />); shallow(<Status pathPrefix="/path/prefix" />);
expect(useFetchSpy).toHaveBeenCalledWith([ expect(useFetchSpy).toHaveBeenCalledWith([
'../api/v1/status/runtimeinfo', '/path/prefix/api/v1/status/runtimeinfo',
'../api/v1/status/buildinfo', '/path/prefix/api/v1/status/buildinfo',
'../api/v1/alertmanagers', '/path/prefix/api/v1/alertmanagers',
]); ]);
}); });
describe('Snapshot testing', () => { describe('Snapshot testing', () => {
......
...@@ -5,8 +5,9 @@ import useFetches from '../hooks/useFetches'; ...@@ -5,8 +5,9 @@ import useFetches from '../hooks/useFetches';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons'; 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']; const sectionTitles = ['Runtime Information', 'Build Information', 'Alertmanagers'];
interface StatusConfig { interface StatusConfig {
...@@ -54,8 +55,19 @@ export const statusConfig: StatusConfig = { ...@@ -54,8 +55,19 @@ export const statusConfig: StatusConfig = {
droppedAlertmanagers: { skip: true }, droppedAlertmanagers: { skip: true },
}; };
const Status = () => { const endpointsMemo: { [prefix: string]: string[] } = {};
const { response: data, error, isLoading } = useFetches<StatusPageState[]>(ENDPOINTS);
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) { if (error) {
return ( return (
<Alert color="danger"> <Alert color="danger">
...@@ -73,8 +85,9 @@ const Status = () => { ...@@ -73,8 +85,9 @@ const Status = () => {
/> />
); );
} }
return data return data ? (
? data.map((statuses, i) => { <>
{data.map((statuses, i) => {
return ( return (
<Fragment key={i}> <Fragment key={i}>
<h2>{sectionTitles[i]}</h2> <h2>{sectionTitles[i]}</h2>
...@@ -101,8 +114,9 @@ const Status = () => { ...@@ -101,8 +114,9 @@ const Status = () => {
</Table> </Table>
</Fragment> </Fragment>
); );
}) })}
: null; </>
) : null;
}; };
export default Status as FC<RouteComponentProps>; export default Status;
import React, { FC } from 'react'; import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; 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; export default Targets;
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Status Snapshot testing should match table snapshot 1`] = ` exports[`Status Snapshot testing should match table snapshot 1`] = `
Array [ <Fragment>
<Fragment <h2>
key="0" Runtime Information
</h2>
<Table
bordered={true}
className="h-auto"
responsiveTag="div"
size="sm"
striped={true}
tag="table"
> >
<h2> <tbody>
Runtime Information <tr
</h2> key="startTime"
<Table >
bordered={true} <th
className="h-auto" className="capitalize-title"
responsiveTag="div" style={
size="sm" Object {
striped={true} "width": "35%",
tag="table"
>
<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 Start time
className="capitalize-title" </th>
style={ <td
Object { className="text-break"
"width": "35%",
}
}
>
Working directory
</th>
<td
className="text-break"
>
/home/boyskila/Desktop/prometheus
</td>
</tr>
<tr
key="reloadConfigSuccess"
> >
<th Wed, 30 Oct 2019 20:03:23 GMT
className="capitalize-title" </td>
style={ </tr>
Object { <tr
"width": "35%", key="CWD"
} >
<th
className="capitalize-title"
style={
Object {
"width": "35%",
} }
> }
Configuration reload
</th>
<td
className="text-break"
>
Successful
</td>
</tr>
<tr
key="lastConfigTime"
> >
<th Working directory
className="capitalize-title" </th>
style={ <td
Object { className="text-break"
"width": "35%",
}
}
>
Last successful configuration reload
</th>
<td
className="text-break"
>
2019-10-30T22:03:23+02:00
</td>
</tr>
<tr
key="chunkCount"
> >
<th /home/boyskila/Desktop/prometheus
className="capitalize-title" </td>
style={ </tr>
Object { <tr
"width": "35%", key="reloadConfigSuccess"
} >
<th
className="capitalize-title"
style={
Object {
"width": "35%",
} }
> }
Head chunks
</th>
<td
className="text-break"
>
1383
</td>
</tr>
<tr
key="timeSeriesCount"
> >
<th Configuration reload
className="capitalize-title" </th>
style={ <td
Object { className="text-break"
"width": "35%",
}
}
>
Head time series
</th>
<td
className="text-break"
>
461
</td>
</tr>
<tr
key="corruptionCount"
> >
<th Successful
className="capitalize-title" </td>
style={ </tr>
Object { <tr
"width": "35%", key="lastConfigTime"
} >
<th
className="capitalize-title"
style={
Object {
"width": "35%",
} }
> }
WAL corruptions
</th>
<td
className="text-break"
>
0
</td>
</tr>
<tr
key="goroutineCount"
> >
<th Last successful configuration reload
className="capitalize-title" </th>
style={ <td
Object { className="text-break"
"width": "35%",
}
}
>
Goroutines
</th>
<td
className="text-break"
>
37
</td>
</tr>
<tr
key="GOMAXPROCS"
> >
<th 2019-10-30T22:03:23+02:00
className="capitalize-title" </td>
style={ </tr>
Object { <tr
"width": "35%", key="chunkCount"
} >
<th
className="capitalize-title"
style={
Object {
"width": "35%",
} }
> }
GOMAXPROCS >
</th> Head chunks
<td </th>
className="text-break" <td
> className="text-break"
4
</td>
</tr>
<tr
key="GOGC"
> >
<th 1383
className="capitalize-title" </td>
style={ </tr>
Object { <tr
"width": "35%", key="timeSeriesCount"
} >
<th
className="capitalize-title"
style={
Object {
"width": "35%",
} }
> }
GOGC >
</th> Head time series
<td </th>
className="text-break" <td
/> className="text-break"
</tr>
<tr
key="GODEBUG"
> >
<th 461
className="capitalize-title" </td>
style={ </tr>
Object { <tr
"width": "35%", key="corruptionCount"
} >
<th
className="capitalize-title"
style={
Object {
"width": "35%",
} }
> }
GODEBUG
</th>
<td
className="text-break"
/>
</tr>
<tr
key="storageRetention"
> >
<th WAL corruptions
className="capitalize-title" </th>
style={ <td
Object { className="text-break"
"width": "35%", >
} 0
</td>
</tr>
<tr
key="goroutineCount"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
} }
> }
Storage retention
</th>
<td
className="text-break"
>
15d
</td>
</tr>
</tbody>
</Table>
</Fragment>,
<Fragment
key="1"
>
<h2>
Build Information
</h2>
<Table
bordered={true}
className="h-auto"
responsiveTag="div"
size="sm"
striped={true}
tag="table"
>
<tbody>
<tr
key="version"
> >
<th Goroutines
className="capitalize-title" </th>
style={ <td
Object { className="text-break"
"width": "35%", >
} 37
</td>
</tr>
<tr
key="GOMAXPROCS"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
} }
> }
version >
</th> GOMAXPROCS
<td </th>
className="text-break" <td
/> className="text-break"
</tr>
<tr
key="revision"
> >
<th 4
className="capitalize-title" </td>
style={ </tr>
Object { <tr
"width": "35%", key="GOGC"
} >
<th
className="capitalize-title"
style={
Object {
"width": "35%",
} }
> }
revision
</th>
<td
className="text-break"
/>
</tr>
<tr
key="branch"
> >
<th GOGC
className="capitalize-title" </th>
style={ <td
Object { className="text-break"
"width": "35%", />
} </tr>
<tr
key="GODEBUG"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
} }
> }
branch
</th>
<td
className="text-break"
/>
</tr>
<tr
key="buildUser"
> >
<th GODEBUG
className="capitalize-title" </th>
style={ <td
Object { className="text-break"
"width": "35%", />
} </tr>
<tr
key="storageRetention"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
} }
> }
buildUser >
</th> Storage retention
<td </th>
className="text-break" <td
/> className="text-break"
</tr>
<tr
key="buildDate"
> >
<th 15d
className="capitalize-title" </td>
style={ </tr>
Object { </tbody>
"width": "35%", </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%",
} }
> }
buildDate
</th>
<td
className="text-break"
/>
</tr>
<tr
key="goVersion"
> >
<th version
className="capitalize-title" </th>
style={ <td
Object { className="text-break"
"width": "35%", />
} </tr>
<tr
key="revision"
>
<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"
>
<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> revision
<a </th>
href="https://1.2.3.4:9093/api/v1/alerts" <td
> className="text-break"
https://1.2.3.4:9093 />
</a> </tr>
/api/v1/alerts <tr
</td> key="branch"
</tr> >
<tr <th
key="https://1.2.3.5:9093/api/v1/alerts" className="capitalize-title"
style={
Object {
"width": "35%",
}
}
> >
<td> branch
<a </th>
href="https://1.2.3.5:9093/api/v1/alerts" <td
> className="text-break"
https://1.2.3.5:9093 />
</a> </tr>
/api/v1/alerts <tr
</td> key="buildUser"
</tr> >
<tr <th
key="https://1.2.3.6:9093/api/v1/alerts" className="capitalize-title"
style={
Object {
"width": "35%",
}
}
> >
<td> buildUser
<a </th>
href="https://1.2.3.6:9093/api/v1/alerts" <td
> className="text-break"
https://1.2.3.6:9093 />
</a> </tr>
/api/v1/alerts <tr
</td> key="buildDate"
</tr> >
<tr <th
key="https://1.2.3.7:9093/api/v1/alerts" className="capitalize-title"
style={
Object {
"width": "35%",
}
}
> >
<td> buildDate
<a </th>
href="https://1.2.3.7:9093/api/v1/alerts" <td
> className="text-break"
https://1.2.3.7:9093 />
</a> </tr>
/api/v1/alerts <tr
</td> key="goVersion"
</tr> >
<tr <th
key="https://1.2.3.8:9093/api/v1/alerts" className="capitalize-title"
style={
Object {
"width": "35%",
}
}
> >
<td> goVersion
<a </th>
href="https://1.2.3.8:9093/api/v1/alerts" <td
> className="text-break"
https://1.2.3.8:9093
</a>
/api/v1/alerts
</td>
</tr>
<tr
key="https://1.2.3.9:9093/api/v1/alerts"
> >
<td> go1.13.3
<a </td>
href="https://1.2.3.9:9093/api/v1/alerts" </tr>
> </tbody>
https://1.2.3.9:9093 </Table>
</a> <h2>
/api/v1/alerts Alertmanagers
</td> </h2>
</tr> <Table
</tbody> bordered={true}
</Table> className="h-auto"
</Fragment>, 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>
`; `;
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
package web package web
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
...@@ -72,7 +73,7 @@ var ( ...@@ -72,7 +73,7 @@ var (
localhostRepresentations = []string{"127.0.0.1", "localhost"} 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. // 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", "/alerts",
"/config", "/config",
...@@ -347,15 +348,33 @@ func New(logger log.Logger, o *Options) *Handler { ...@@ -347,15 +348,33 @@ func New(logger log.Logger, o *Options) *Handler {
router.Get("/new/*filepath", func(w http.ResponseWriter, r *http.Request) { router.Get("/new/*filepath", func(w http.ResponseWriter, r *http.Request) {
p := route.Param(r.Context(), "filepath") p := route.Param(r.Context(), "filepath")
r.URL.Path = path.Join("/static/react/", p)
for _, rp := range reactAppPaths { // For paths that the React/Reach router handles, we want to serve the
if p == rp { // index.html, but with replaced path prefix placeholder.
r.URL.Path = "/static/react/" for _, rp := range reactRouterPaths {
break 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 := server.StaticFileServer(ui.Assets)
fs.ServeHTTP(w, r) fs.ServeHTTP(w, r)
}) })
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册