react updates for pathPrefix (#7979)

* dynamically determine path prefix

Signed-off-by: James Ranson <james_ranson@cable.comcast.com>

* minor changes per PR review

Signed-off-by: James Ranson <james_ranson@cable.comcast.com>

* use Context for apiPath and pathPrefix

Signed-off-by: James Ranson <james_ranson@cable.comcast.com>

* remove unhandled "/version" path

Signed-off-by: James Ranson <james_ranson@cable.comcast.com>

* only process index once instead of on every req

Signed-off-by: James Ranson <james_ranson@cable.comcast.com>

* remove unneeded tag fragment

Signed-off-by: James Ranson <james_ranson@cable.comcast.com>

* switch api path to const

Signed-off-by: James Ranson <james_ranson@cable.comcast.com>

* revert

Signed-off-by: James Ranson <james_ranson@cable.comcast.com>

* update tests

Signed-off-by: James Ranson <james_ranson@cable.comcast.com>

* linter updates

Signed-off-by: James Ranson <james_ranson@cable.comcast.com>

* simplify

Signed-off-by: James Ranson <james_ranson@cable.comcast.com>

* updates per peer review

Signed-off-by: James Ranson <james_ranson@cable.comcast.com>
This commit is contained in:
James Ranson 2020-10-22 09:22:32 -06:00 committed by GitHub
parent 74775d7324
commit 1cffda5de7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 173 additions and 117 deletions

View file

@ -10,16 +10,11 @@
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<!-- <!--
The GLOBAL_PATH_PREFIX 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.
The GLOBAL_CONSOLES_LINK placeholder magic value is replaced during serving by Prometheus The GLOBAL_CONSOLES_LINK placeholder magic value is replaced during serving by Prometheus
and set to the consoles link if it exists. It will render a "Consoles" link in the navbar when and set to the consoles link if it exists. It will render a "Consoles" link in the navbar when
it is non-empty. it is non-empty.
--> -->
<script> <script>
const GLOBAL_PATH_PREFIX='PATH_PREFIX_PLACEHOLDER';
const GLOBAL_CONSOLES_LINK='CONSOLES_LINK_PLACEHOLDER'; const GLOBAL_CONSOLES_LINK='CONSOLES_LINK_PLACEHOLDER';
</script> </script>

View file

@ -7,7 +7,7 @@ import { Router } from '@reach/router';
import { Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList } from './pages'; import { Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList } from './pages';
describe('App', () => { describe('App', () => {
const app = shallow(<App pathPrefix="/path/prefix" />); const app = shallow(<App />);
it('navigates', () => { it('navigates', () => {
expect(app.find(Navigation)).toHaveLength(1); expect(app.find(Navigation)).toHaveLength(1);
@ -16,7 +16,6 @@ describe('App', () => {
[Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList].forEach(component => { [Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList].forEach(component => {
const c = app.find(component); const c = app.find(component);
expect(c).toHaveLength(1); 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);

View file

@ -5,36 +5,62 @@ import { Container } from 'reactstrap';
import './App.css'; import './App.css';
import { Router, Redirect } from '@reach/router'; import { Router, Redirect } from '@reach/router';
import { Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList } from './pages'; import { Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList } from './pages';
import PathPrefixProps from './types/PathPrefixProps'; import { PathPrefixContext } from './contexts/PathPrefixContext';
interface AppProps { interface AppProps {
consolesLink: string | null; consolesLink: string | null;
} }
const App: FC<PathPrefixProps & AppProps> = ({ pathPrefix, consolesLink }) => { const App: FC<AppProps> = ({ consolesLink }) => {
return ( // This dynamically/generically determines the pathPrefix by stripping the first known
<> // endpoint suffix from the window location path. It works out of the box for both direct
<Navigation pathPrefix={pathPrefix} consolesLink={consolesLink} /> // hosting and reverse proxy deployments with no additional configurations required.
<Container fluid style={{ paddingTop: 70 }}> let basePath = window.location.pathname;
<Router basepath={`${pathPrefix}/new`}> const paths = [
<Redirect from="/" to={`${pathPrefix}/new/graph`} /> '/graph',
'/alerts',
'/status',
'/tsdb-status',
'/flags',
'/config',
'/rules',
'/targets',
'/service-discovery',
];
if (basePath.endsWith('/')) {
basePath = basePath.slice(0, -1);
}
if (basePath.length > 1) {
for (let i = 0; i < paths.length; i++) {
if (basePath.endsWith(paths[i])) {
basePath = basePath.slice(0, basePath.length - paths[i].length);
break;
}
}
}
return (
<PathPrefixContext.Provider value={basePath}>
<Navigation consolesLink={consolesLink} />
<Container fluid style={{ paddingTop: 70 }}>
<Router basepath={`${basePath}`}>
<Redirect from="/" to={`graph`} noThrow />
{/* {/*
NOTE: Any route added here needs to also be added to the list of NOTE: Any route added here needs to also be added to the list of
React-handled router paths ("reactRouterPaths") in /web/web.go. React-handled router paths ("reactRouterPaths") in /web/web.go.
*/} */}
<PanelList path="/graph" pathPrefix={pathPrefix} /> <PanelList path="/graph" />
<Alerts path="/alerts" pathPrefix={pathPrefix} /> <Alerts path="/alerts" />
<Config path="/config" pathPrefix={pathPrefix} /> <Config path="/config" />
<Flags path="/flags" pathPrefix={pathPrefix} /> <Flags path="/flags" />
<Rules path="/rules" pathPrefix={pathPrefix} /> <Rules path="/rules" />
<ServiceDiscovery path="/service-discovery" pathPrefix={pathPrefix} /> <ServiceDiscovery path="/service-discovery" />
<Status path="/status" pathPrefix={pathPrefix} /> <Status path="/status" />
<TSDBStatus path="/tsdb-status" pathPrefix={pathPrefix} /> <TSDBStatus path="/tsdb-status" />
<Targets path="/targets" pathPrefix={pathPrefix} /> <Targets path="/targets" />
</Router> </Router>
</Container> </Container>
</> </PathPrefixContext.Provider>
); );
}; };

View file

@ -12,19 +12,20 @@ import {
DropdownMenu, DropdownMenu,
DropdownItem, DropdownItem,
} from 'reactstrap'; } from 'reactstrap';
import PathPrefixProps from './types/PathPrefixProps'; import { usePathPrefix } from './contexts/PathPrefixContext';
interface NavbarProps { interface NavbarProps {
consolesLink: string | null; consolesLink: string | null;
} }
const Navigation: FC<PathPrefixProps & NavbarProps> = ({ pathPrefix, consolesLink }) => { const Navigation: FC<NavbarProps> = ({ consolesLink }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen); const toggle = () => setIsOpen(!isOpen);
const pathPrefix = usePathPrefix();
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={`${pathPrefix}/new/graph`}> <Link className="pt-0 navbar-brand" to={`${pathPrefix}/graph`}>
Prometheus Prometheus
</Link> </Link>
<Collapse isOpen={isOpen} navbar style={{ justifyContent: 'space-between' }}> <Collapse isOpen={isOpen} navbar style={{ justifyContent: 'space-between' }}>
@ -35,12 +36,12 @@ const Navigation: FC<PathPrefixProps & NavbarProps> = ({ pathPrefix, consolesLin
</NavItem> </NavItem>
)} )}
<NavItem> <NavItem>
<NavLink tag={Link} to={`${pathPrefix}/new/alerts`}> <NavLink tag={Link} to={`${pathPrefix}/alerts`}>
Alerts Alerts
</NavLink> </NavLink>
</NavItem> </NavItem>
<NavItem> <NavItem>
<NavLink tag={Link} to={`${pathPrefix}/new/graph`}> <NavLink tag={Link} to={`${pathPrefix}/graph`}>
Graph Graph
</NavLink> </NavLink>
</NavItem> </NavItem>
@ -49,25 +50,25 @@ const Navigation: FC<PathPrefixProps & NavbarProps> = ({ pathPrefix, consolesLin
Status Status
</DropdownToggle> </DropdownToggle>
<DropdownMenu> <DropdownMenu>
<DropdownItem tag={Link} to={`${pathPrefix}/new/status`}> <DropdownItem tag={Link} to={`${pathPrefix}/status`}>
Runtime & Build Information Runtime & Build Information
</DropdownItem> </DropdownItem>
<DropdownItem tag={Link} to={`${pathPrefix}/new/tsdb-status`}> <DropdownItem tag={Link} to={`${pathPrefix}/tsdb-status`}>
TSDB Status TSDB Status
</DropdownItem> </DropdownItem>
<DropdownItem tag={Link} to={`${pathPrefix}/new/flags`}> <DropdownItem tag={Link} to={`${pathPrefix}/flags`}>
Command-Line Flags Command-Line Flags
</DropdownItem> </DropdownItem>
<DropdownItem tag={Link} to={`${pathPrefix}/new/config`}> <DropdownItem tag={Link} to={`${pathPrefix}/config`}>
Configuration Configuration
</DropdownItem> </DropdownItem>
<DropdownItem tag={Link} to={`${pathPrefix}/new/rules`}> <DropdownItem tag={Link} to={`${pathPrefix}/rules`}>
Rules Rules
</DropdownItem> </DropdownItem>
<DropdownItem tag={Link} to={`${pathPrefix}/new/targets`}> <DropdownItem tag={Link} to={`${pathPrefix}/targets`}>
Targets Targets
</DropdownItem> </DropdownItem>
<DropdownItem tag={Link} to={`${pathPrefix}/new/service-discovery`}> <DropdownItem tag={Link} to={`${pathPrefix}/service-discovery`}>
Service Discovery Service Discovery
</DropdownItem> </DropdownItem>
</DropdownMenu> </DropdownMenu>
@ -76,7 +77,7 @@ const Navigation: FC<PathPrefixProps & NavbarProps> = ({ pathPrefix, consolesLin
<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 href={`${pathPrefix}/graph${window.location.search}`}>Classic UI</NavLink> <NavLink href={`${pathPrefix}/../graph${window.location.search}`}>Classic UI</NavLink>
</NavItem> </NavItem>
</Nav> </Nav>
</Collapse> </Collapse>

View file

@ -0,0 +1 @@
export const API_PATH = '../api/v1';

View file

@ -0,0 +1,9 @@
import React from 'react';
const PathPrefixContext = React.createContext('');
function usePathPrefix() {
return React.useContext(PathPrefixContext);
}
export { usePathPrefix, PathPrefixContext };

View file

@ -6,20 +6,10 @@ import 'bootstrap/dist/css/bootstrap.min.css';
import { isPresent } from './utils'; import { isPresent } from './utils';
// Declared/defined in public/index.html, value replaced by Prometheus when serving bundle. // Declared/defined in public/index.html, value replaced by Prometheus when serving bundle.
declare const GLOBAL_PATH_PREFIX: string;
declare const GLOBAL_CONSOLES_LINK: string; declare const GLOBAL_CONSOLES_LINK: string;
let prefix = GLOBAL_PATH_PREFIX;
let consolesLink: string | null = GLOBAL_CONSOLES_LINK; let consolesLink: string | null = GLOBAL_CONSOLES_LINK;
if (GLOBAL_PATH_PREFIX === 'PATH_PREFIX_PLACEHOLDER' || GLOBAL_PATH_PREFIX === '/' || !isPresent(GLOBAL_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 = '';
}
if ( if (
GLOBAL_CONSOLES_LINK === 'CONSOLES_LINK_PLACEHOLDER' || GLOBAL_CONSOLES_LINK === 'CONSOLES_LINK_PLACEHOLDER' ||
GLOBAL_CONSOLES_LINK === '' || GLOBAL_CONSOLES_LINK === '' ||
@ -28,4 +18,4 @@ if (
consolesLink = null; consolesLink = null;
} }
ReactDOM.render(<App pathPrefix={prefix} consolesLink={consolesLink} />, document.getElementById('root')); ReactDOM.render(<App consolesLink={consolesLink} />, document.getElementById('root'));

View file

@ -1,14 +1,16 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
import PathPrefixProps from '../../types/PathPrefixProps';
import { useFetch } from '../../hooks/useFetch'; import { useFetch } from '../../hooks/useFetch';
import { withStatusIndicator } from '../../components/withStatusIndicator'; import { withStatusIndicator } from '../../components/withStatusIndicator';
import AlertsContent, { RuleStatus, AlertsProps } from './AlertContents'; import AlertsContent, { RuleStatus, AlertsProps } from './AlertContents';
import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { API_PATH } from '../../constants/constants';
const AlertsWithStatusIndicator = withStatusIndicator(AlertsContent); const AlertsWithStatusIndicator = withStatusIndicator(AlertsContent);
const Alerts: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' }) => { const Alerts: FC<RouteComponentProps> = () => {
const { response, error, isLoading } = useFetch<AlertsProps>(`${pathPrefix}/api/v1/rules?type=alert`); const pathPrefix = usePathPrefix();
const { response, error, isLoading } = useFetch<AlertsProps>(`${pathPrefix}/${API_PATH}/rules?type=alert`);
const ruleStatsCount: RuleStatus<number> = { const ruleStatsCount: RuleStatus<number> = {
inactive: 0, inactive: 0,

View file

@ -2,11 +2,12 @@ import React, { useState, FC } from 'react';
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
import { Button } from 'reactstrap'; import { Button } from 'reactstrap';
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import PathPrefixProps from '../../types/PathPrefixProps';
import './Config.css'; import './Config.css';
import { withStatusIndicator } from '../../components/withStatusIndicator'; import { withStatusIndicator } from '../../components/withStatusIndicator';
import { useFetch } from '../../hooks/useFetch'; import { useFetch } from '../../hooks/useFetch';
import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { API_PATH } from '../../constants/constants';
type YamlConfig = { yaml?: string }; type YamlConfig = { yaml?: string };
@ -44,8 +45,9 @@ export const ConfigContent: FC<ConfigContentProps> = ({ error, data }) => {
); );
}; };
const Config: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => { const Config: FC<RouteComponentProps> = () => {
const { response, error } = useFetch<YamlConfig>(`${pathPrefix}/api/v1/status/config`); const pathPrefix = usePathPrefix();
const { response, error } = useFetch<YamlConfig>(`${pathPrefix}/${API_PATH}/status/config`);
return <ConfigContent error={error} data={response.data} />; return <ConfigContent error={error} data={response.data} />;
}; };

View file

@ -3,7 +3,8 @@ import { RouteComponentProps } from '@reach/router';
import { Table } from 'reactstrap'; import { Table } from 'reactstrap';
import { withStatusIndicator } from '../../components/withStatusIndicator'; import { withStatusIndicator } from '../../components/withStatusIndicator';
import { useFetch } from '../../hooks/useFetch'; import { useFetch } from '../../hooks/useFetch';
import PathPrefixProps from '../../types/PathPrefixProps'; import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { API_PATH } from '../../constants/constants';
interface FlagMap { interface FlagMap {
[key: string]: string; [key: string]: string;
@ -34,8 +35,9 @@ const FlagsWithStatusIndicator = withStatusIndicator(FlagsContent);
FlagsContent.displayName = 'Flags'; FlagsContent.displayName = 'Flags';
const Flags: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' }) => { const Flags: FC<RouteComponentProps> = () => {
const { response, error, isLoading } = useFetch<FlagMap>(`${pathPrefix}/api/v1/status/flags`); const pathPrefix = usePathPrefix();
const { response, error, isLoading } = useFetch<FlagMap>(`${pathPrefix}/${API_PATH}/status/flags`);
return <FlagsWithStatusIndicator data={response.data} error={error} isLoading={isLoading} />; return <FlagsWithStatusIndicator data={response.data} error={error} isLoading={isLoading} />;
}; };

View file

@ -10,8 +10,8 @@ import { GraphTabContent } from './GraphTabContent';
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 '../../types/PathPrefixProps';
import { QueryParams } from '../../types/types'; import { QueryParams } from '../../types/types';
import { API_PATH } from '../../constants/constants';
interface PanelProps { interface PanelProps {
options: PanelOptions; options: PanelOptions;
@ -21,6 +21,7 @@ interface PanelProps {
metricNames: string[]; metricNames: string[];
removePanel: () => void; removePanel: () => void;
onExecuteQuery: (query: string) => void; onExecuteQuery: (query: string) => void;
pathPrefix: string;
} }
interface PanelState { interface PanelState {
@ -55,7 +56,7 @@ export const PanelDefaultOptions: PanelOptions = {
stacked: false, stacked: false,
}; };
class Panel extends Component<PanelProps & PathPrefixProps, PanelState> { class Panel extends Component<PanelProps, PanelState> {
private abortInFlightFetch: (() => void) | null = null; private abortInFlightFetch: (() => void) | null = null;
constructor(props: PanelProps) { constructor(props: PanelProps) {
@ -117,21 +118,20 @@ class Panel extends Component<PanelProps & PathPrefixProps, 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 = '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.
break; break;
case 'table': case 'table':
path = '/api/v1/query'; path = '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(`${this.props.pathPrefix}${path}?${params}`, { fetch(`${this.props.pathPrefix}/${API_PATH}/${path}?${params}`, {
cache: 'no-store', cache: 'no-store',
credentials: 'same-origin', credentials: 'same-origin',
signal: abortController.signal, signal: abortController.signal,

View file

@ -4,10 +4,11 @@ import { Alert, Button } from 'reactstrap';
import Panel, { PanelOptions, PanelDefaultOptions } from './Panel'; import Panel, { PanelOptions, PanelDefaultOptions } from './Panel';
import Checkbox from '../../components/Checkbox'; import Checkbox from '../../components/Checkbox';
import PathPrefixProps from '../../types/PathPrefixProps';
import { generateID, decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString, callAll } from '../../utils'; import { generateID, decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString, callAll } from '../../utils';
import { useFetch } from '../../hooks/useFetch'; import { useFetch } from '../../hooks/useFetch';
import { useLocalStorage } from '../../hooks/useLocalStorage'; import { useLocalStorage } from '../../hooks/useLocalStorage';
import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { API_PATH } from '../../constants/constants';
export type PanelMeta = { key: string; options: PanelOptions; id: string }; export type PanelMeta = { key: string; options: PanelOptions; id: string };
@ -16,20 +17,14 @@ export const updateURL = (nextPanels: PanelMeta[]) => {
window.history.pushState({}, '', query); window.history.pushState({}, '', query);
}; };
interface PanelListProps extends PathPrefixProps, RouteComponentProps { interface PanelListProps extends RouteComponentProps {
panels: PanelMeta[]; panels: PanelMeta[];
metrics: string[]; metrics: string[];
useLocalTime: boolean; useLocalTime: boolean;
queryHistoryEnabled: boolean; queryHistoryEnabled: boolean;
} }
export const PanelListContent: FC<PanelListProps> = ({ export const PanelListContent: FC<PanelListProps> = ({ metrics = [], useLocalTime, queryHistoryEnabled, ...rest }) => {
metrics = [],
useLocalTime,
pathPrefix,
queryHistoryEnabled,
...rest
}) => {
const [panels, setPanels] = useState(rest.panels); const [panels, setPanels] = useState(rest.panels);
const [historyItems, setLocalStorageHistoryItems] = useLocalStorage<string[]>('history', []); const [historyItems, setLocalStorageHistoryItems] = useLocalStorage<string[]>('history', []);
@ -73,10 +68,13 @@ export const PanelListContent: FC<PanelListProps> = ({
]); ]);
}; };
const pathPrefix = usePathPrefix();
return ( return (
<> <>
{panels.map(({ id, options }) => ( {panels.map(({ id, options }) => (
<Panel <Panel
pathPrefix={pathPrefix}
onExecuteQuery={handleExecuteQuery} onExecuteQuery={handleExecuteQuery}
key={id} key={id}
options={options} options={options}
@ -97,7 +95,6 @@ export const PanelListContent: FC<PanelListProps> = ({
useLocalTime={useLocalTime} useLocalTime={useLocalTime}
metricNames={metrics} metricNames={metrics}
pastQueries={queryHistoryEnabled ? historyItems : []} pastQueries={queryHistoryEnabled ? historyItems : []}
pathPrefix={pathPrefix}
/> />
))} ))}
<Button className="mb-3" color="primary" onClick={addPanel}> <Button className="mb-3" color="primary" onClick={addPanel}>
@ -107,15 +104,18 @@ export const PanelListContent: FC<PanelListProps> = ({
); );
}; };
const PanelList: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' }) => { const PanelList: FC<RouteComponentProps> = () => {
const [delta, setDelta] = useState(0); const [delta, setDelta] = useState(0);
const [useLocalTime, setUseLocalTime] = useLocalStorage('use-local-time', false); const [useLocalTime, setUseLocalTime] = useLocalStorage('use-local-time', false);
const [enableQueryHistory, setEnableQueryHistory] = useLocalStorage('enable-query-history', false); const [enableQueryHistory, setEnableQueryHistory] = useLocalStorage('enable-query-history', false);
const { response: metricsRes, error: metricsErr } = useFetch<string[]>(`${pathPrefix}/api/v1/label/__name__/values`); const pathPrefix = usePathPrefix();
const { response: metricsRes, error: metricsErr } = useFetch<string[]>(`${pathPrefix}/${API_PATH}/label/__name__/values`);
const browserTime = new Date().getTime() / 1000; const browserTime = new Date().getTime() / 1000;
const { response: timeRes, error: timeErr } = useFetch<{ result: number[] }>(`${pathPrefix}/api/v1/query?query=time()`); const { response: timeRes, error: timeErr } = useFetch<{ result: number[] }>(
`${pathPrefix}/${API_PATH}/query?query=time()`
);
useEffect(() => { useEffect(() => {
if (timeRes.data) { if (timeRes.data) {
@ -164,7 +164,6 @@ const PanelList: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = ''
)} )}
<PanelListContent <PanelListContent
panels={decodePanelOptionsFromQueryString(window.location.search)} panels={decodePanelOptionsFromQueryString(window.location.search)}
pathPrefix={pathPrefix}
useLocalTime={useLocalTime} useLocalTime={useLocalTime}
metrics={metricsRes.data} metrics={metricsRes.data}
queryHistoryEnabled={enableQueryHistory} queryHistoryEnabled={enableQueryHistory}

View file

@ -1,14 +1,16 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
import PathPrefixProps from '../../types/PathPrefixProps';
import { useFetch } from '../../hooks/useFetch'; import { useFetch } from '../../hooks/useFetch';
import { withStatusIndicator } from '../../components/withStatusIndicator'; import { withStatusIndicator } from '../../components/withStatusIndicator';
import { RulesMap, RulesContent } from './RulesContent'; import { RulesMap, RulesContent } from './RulesContent';
import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { API_PATH } from '../../constants/constants';
const RulesWithStatusIndicator = withStatusIndicator(RulesContent); const RulesWithStatusIndicator = withStatusIndicator(RulesContent);
const Rules: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => { const Rules: FC<RouteComponentProps> = () => {
const { response, error, isLoading } = useFetch<RulesMap>(`${pathPrefix}/api/v1/rules`); const pathPrefix = usePathPrefix();
const { response, error, isLoading } = useFetch<RulesMap>(`${pathPrefix}/${API_PATH}/rules`);
return <RulesWithStatusIndicator response={response} error={error} isLoading={isLoading} />; return <RulesWithStatusIndicator response={response} error={error} isLoading={isLoading} />;
}; };

View file

@ -1,12 +1,13 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
import PathPrefixProps from '../../types/PathPrefixProps';
import { useFetch } from '../../hooks/useFetch'; import { useFetch } from '../../hooks/useFetch';
import { LabelsTable } from './LabelsTable'; import { LabelsTable } from './LabelsTable';
import { Target, Labels, DroppedTarget } from '../targets/target'; import { Target, Labels, DroppedTarget } from '../targets/target';
import { withStatusIndicator } from '../../components/withStatusIndicator'; import { withStatusIndicator } from '../../components/withStatusIndicator';
import { mapObjEntries } from '../../utils'; import { mapObjEntries } from '../../utils';
import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { API_PATH } from '../../constants/constants';
interface ServiceMap { interface ServiceMap {
activeTargets: Target[]; activeTargets: Target[];
@ -105,8 +106,9 @@ ServiceDiscoveryContent.displayName = 'ServiceDiscoveryContent';
const ServicesWithStatusIndicator = withStatusIndicator(ServiceDiscoveryContent); const ServicesWithStatusIndicator = withStatusIndicator(ServiceDiscoveryContent);
const ServiceDiscovery: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => { const ServiceDiscovery: FC<RouteComponentProps> = () => {
const { response, error, isLoading } = useFetch<ServiceMap>(`${pathPrefix}/api/v1/targets`); const pathPrefix = usePathPrefix();
const { response, error, isLoading } = useFetch<ServiceMap>(`${pathPrefix}/${API_PATH}/targets`);
return ( return (
<ServicesWithStatusIndicator <ServicesWithStatusIndicator
{...response.data} {...response.data}

View file

@ -3,7 +3,8 @@ import { RouteComponentProps } from '@reach/router';
import { Table } from 'reactstrap'; import { Table } from 'reactstrap';
import { withStatusIndicator } from '../../components/withStatusIndicator'; import { withStatusIndicator } from '../../components/withStatusIndicator';
import { useFetch } from '../../hooks/useFetch'; import { useFetch } from '../../hooks/useFetch';
import PathPrefixProps from '../../types/PathPrefixProps'; import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { API_PATH } from '../../constants/constants';
interface StatusPageProps { interface StatusPageProps {
data: Record<string, string>; data: Record<string, string>;
@ -82,8 +83,9 @@ const StatusWithStatusIndicator = withStatusIndicator(StatusContent);
StatusContent.displayName = 'Status'; StatusContent.displayName = 'Status';
const Status: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' }) => { const Status: FC<RouteComponentProps> = () => {
const path = `${pathPrefix}/api/v1`; const pathPrefix = usePathPrefix();
const path = `${pathPrefix}/${API_PATH}`;
return ( return (
<> <>

View file

@ -7,11 +7,11 @@ import ScrapePoolList from './ScrapePoolList';
import ScrapePoolPanel from './ScrapePoolPanel'; import ScrapePoolPanel from './ScrapePoolPanel';
import { Target } from './target'; import { Target } from './target';
import { FetchMock } from 'jest-fetch-mock/types'; import { FetchMock } from 'jest-fetch-mock/types';
import { PathPrefixContext } from '../../contexts/PathPrefixContext';
describe('ScrapePoolList', () => { describe('ScrapePoolList', () => {
const defaultProps = { const defaultProps = {
filter: { showHealthy: true, showUnhealthy: true }, filter: { showHealthy: true, showUnhealthy: true },
pathPrefix: '..',
}; };
beforeEach(() => { beforeEach(() => {
@ -36,10 +36,17 @@ describe('ScrapePoolList', () => {
it('renders a table', async () => { it('renders a table', async () => {
await act(async () => { await act(async () => {
scrapePoolList = mount(<ScrapePoolList {...defaultProps} />); scrapePoolList = mount(
<PathPrefixContext.Provider value="/path/prefix">
<ScrapePoolList {...defaultProps} />
</PathPrefixContext.Provider>
);
}); });
scrapePoolList.update(); scrapePoolList.update();
expect(mock).toHaveBeenCalledWith('../api/v1/targets?state=active', { cache: 'no-store', credentials: 'same-origin' }); expect(mock).toHaveBeenCalledWith('/path/prefix/../api/v1/targets?state=active', {
cache: 'no-store',
credentials: 'same-origin',
});
const panels = scrapePoolList.find(ScrapePoolPanel); const panels = scrapePoolList.find(ScrapePoolPanel);
expect(panels).toHaveLength(3); expect(panels).toHaveLength(3);
const activeTargets: Target[] = sampleApiResponse.data.activeTargets as Target[]; const activeTargets: Target[] = sampleApiResponse.data.activeTargets as Target[];
@ -55,10 +62,17 @@ describe('ScrapePoolList', () => {
filter: { showHealthy: false, showUnhealthy: true }, filter: { showHealthy: false, showUnhealthy: true },
}; };
await act(async () => { await act(async () => {
scrapePoolList = mount(<ScrapePoolList {...props} />); scrapePoolList = mount(
<PathPrefixContext.Provider value="/path/prefix">
<ScrapePoolList {...props} />
</PathPrefixContext.Provider>
);
}); });
scrapePoolList.update(); scrapePoolList.update();
expect(mock).toHaveBeenCalledWith('../api/v1/targets?state=active', { cache: 'no-store', credentials: 'same-origin' }); expect(mock).toHaveBeenCalledWith('/path/prefix/../api/v1/targets?state=active', {
cache: 'no-store',
credentials: 'same-origin',
});
const panels = scrapePoolList.find(ScrapePoolPanel); const panels = scrapePoolList.find(ScrapePoolPanel);
expect(panels).toHaveLength(0); expect(panels).toHaveLength(0);
}); });
@ -70,11 +84,18 @@ describe('ScrapePoolList', () => {
let scrapePoolList: any; let scrapePoolList: any;
await act(async () => { await act(async () => {
scrapePoolList = mount(<ScrapePoolList {...defaultProps} />); scrapePoolList = mount(
<PathPrefixContext.Provider value="/path/prefix">
<ScrapePoolList {...defaultProps} />
</PathPrefixContext.Provider>
);
}); });
scrapePoolList.update(); scrapePoolList.update();
expect(mock).toHaveBeenCalledWith('../api/v1/targets?state=active', { cache: 'no-store', credentials: 'same-origin' }); expect(mock).toHaveBeenCalledWith('/path/prefix/../api/v1/targets?state=active', {
cache: 'no-store',
credentials: 'same-origin',
});
const alert = scrapePoolList.find(Alert); const alert = scrapePoolList.find(Alert);
expect(alert.prop('color')).toBe('danger'); expect(alert.prop('color')).toBe('danger');
expect(alert.text()).toContain('Error fetching targets'); expect(alert.text()).toContain('Error fetching targets');

View file

@ -3,8 +3,9 @@ import { FilterData } from './Filter';
import { useFetch } from '../../hooks/useFetch'; import { useFetch } from '../../hooks/useFetch';
import { groupTargets, Target } from './target'; import { groupTargets, Target } from './target';
import ScrapePoolPanel from './ScrapePoolPanel'; import ScrapePoolPanel from './ScrapePoolPanel';
import PathPrefixProps from '../../types/PathPrefixProps';
import { withStatusIndicator } from '../../components/withStatusIndicator'; import { withStatusIndicator } from '../../components/withStatusIndicator';
import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { API_PATH } from '../../constants/constants';
interface ScrapePoolListProps { interface ScrapePoolListProps {
filter: FilterData; filter: FilterData;
@ -30,8 +31,9 @@ ScrapePoolContent.displayName = 'ScrapePoolContent';
const ScrapePoolListWithStatusIndicator = withStatusIndicator(ScrapePoolContent); const ScrapePoolListWithStatusIndicator = withStatusIndicator(ScrapePoolContent);
const ScrapePoolList: FC<{ filter: FilterData } & PathPrefixProps> = ({ pathPrefix, filter }) => { const ScrapePoolList: FC<{ filter: FilterData }> = ({ filter }) => {
const { response, error, isLoading } = useFetch<ScrapePoolListProps>(`${pathPrefix}/api/v1/targets?state=active`); const pathPrefix = usePathPrefix();
const { response, error, isLoading } = useFetch<ScrapePoolListProps>(`${pathPrefix}/${API_PATH}/targets?state=active`);
const { status: responseStatus } = response; const { status: responseStatus } = response;
const badResponse = responseStatus !== 'success' && responseStatus !== 'start fetching'; const badResponse = responseStatus !== 'success' && responseStatus !== 'start fetching';
return ( return (

View file

@ -28,6 +28,5 @@ describe('Targets', () => {
const scrapePoolList = targets.find(ScrapePoolList); const scrapePoolList = targets.find(ScrapePoolList);
expect(scrapePoolList).toHaveLength(1); expect(scrapePoolList).toHaveLength(1);
expect(scrapePoolList.prop('filter')).toEqual({ showHealthy: true, showUnhealthy: true }); expect(scrapePoolList.prop('filter')).toEqual({ showHealthy: true, showUnhealthy: true });
expect(scrapePoolList.prop('pathPrefix')).toEqual(defaultProps.pathPrefix);
}); });
}); });

View file

@ -2,13 +2,15 @@ import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
import Filter from './Filter'; import Filter from './Filter';
import ScrapePoolList from './ScrapePoolList'; import ScrapePoolList from './ScrapePoolList';
import PathPrefixProps from '../../types/PathPrefixProps';
import { useLocalStorage } from '../../hooks/useLocalStorage'; import { useLocalStorage } from '../../hooks/useLocalStorage';
import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { API_PATH } from '../../constants/constants';
const Targets: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => { const Targets: FC<RouteComponentProps> = () => {
const pathPrefix = usePathPrefix();
const [filter, setFilter] = useLocalStorage('targets-page-filter', { showHealthy: true, showUnhealthy: true }); const [filter, setFilter] = useLocalStorage('targets-page-filter', { showHealthy: true, showUnhealthy: true });
const filterProps = { filter, setFilter }; const filterProps = { filter, setFilter };
const scrapePoolListProps = { filter, pathPrefix }; const scrapePoolListProps = { filter, pathPrefix, API_PATH };
return ( return (
<> <>

View file

@ -5,6 +5,7 @@ import { Table } from 'reactstrap';
import TSDBStatus from './TSDBStatus'; import TSDBStatus from './TSDBStatus';
import { TSDBMap } from './TSDBStatus'; import { TSDBMap } from './TSDBStatus';
import { PathPrefixContext } from '../../contexts/PathPrefixContext';
const fakeTSDBStatusResponse: { const fakeTSDBStatusResponse: {
status: string; status: string;
@ -66,11 +67,15 @@ describe('TSDB Stats', () => {
const mock = fetchMock.mockResponse(JSON.stringify(fakeTSDBStatusResponse)); const mock = fetchMock.mockResponse(JSON.stringify(fakeTSDBStatusResponse));
let page: any; let page: any;
await act(async () => { await act(async () => {
page = mount(<TSDBStatus pathPrefix="/path/prefix" />); page = mount(
<PathPrefixContext.Provider value="/path/prefix">
<TSDBStatus />
</PathPrefixContext.Provider>
);
}); });
page.update(); page.update();
expect(mock).toHaveBeenCalledWith('/path/prefix/api/v1/status/tsdb', { expect(mock).toHaveBeenCalledWith('/path/prefix/../api/v1/status/tsdb', {
cache: 'no-store', cache: 'no-store',
credentials: 'same-origin', credentials: 'same-origin',
}); });

View file

@ -3,8 +3,9 @@ import { RouteComponentProps } from '@reach/router';
import { Table } from 'reactstrap'; import { Table } from 'reactstrap';
import { useFetch } from '../../hooks/useFetch'; import { useFetch } from '../../hooks/useFetch';
import PathPrefixProps from '../../types/PathPrefixProps';
import { withStatusIndicator } from '../../components/withStatusIndicator'; import { withStatusIndicator } from '../../components/withStatusIndicator';
import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { API_PATH } from '../../constants/constants';
interface Stats { interface Stats {
name: string; name: string;
@ -101,8 +102,9 @@ TSDBStatusContent.displayName = 'TSDBStatusContent';
const TSDBStatusContentWithStatusIndicator = withStatusIndicator(TSDBStatusContent); const TSDBStatusContentWithStatusIndicator = withStatusIndicator(TSDBStatusContent);
const TSDBStatus: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => { const TSDBStatus: FC<RouteComponentProps> = () => {
const { response, error, isLoading } = useFetch<TSDBMap>(`${pathPrefix}/api/v1/status/tsdb`); const pathPrefix = usePathPrefix();
const { response, error, isLoading } = useFetch<TSDBMap>(`${pathPrefix}/${API_PATH}/status/tsdb`);
return ( return (
<TSDBStatusContentWithStatusIndicator <TSDBStatusContentWithStatusIndicator

View file

@ -1,5 +0,0 @@
interface PathPrefixProps {
pathPrefix?: string;
}
export default PathPrefixProps;

View file

@ -79,7 +79,6 @@ var reactRouterPaths = []string{
"/status", "/status",
"/targets", "/targets",
"/tsdb-status", "/tsdb-status",
"/version",
} }
// withStackTrace logs the stack trace in case the request panics. The function // withStackTrace logs the stack trace in case the request panics. The function
@ -393,8 +392,7 @@ func New(logger log.Logger, o *Options) *Handler {
fmt.Fprintf(w, "Error reading React index.html: %v", err) fmt.Fprintf(w, "Error reading React index.html: %v", err)
return return
} }
replacedIdx := bytes.ReplaceAll(idx, []byte("PATH_PREFIX_PLACEHOLDER"), []byte(o.ExternalURL.Path)) replacedIdx := bytes.ReplaceAll(idx, []byte("CONSOLES_LINK_PLACEHOLDER"), []byte(h.consolesPath()))
replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("CONSOLES_LINK_PLACEHOLDER"), []byte(h.consolesPath()))
replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("TITLE_PLACEHOLDER"), []byte(h.options.PageTitle)) replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("TITLE_PLACEHOLDER"), []byte(h.options.PageTitle))
w.Write(replacedIdx) w.Write(replacedIdx)
return return