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" />
<!--
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
and set to the consoles link if it exists. It will render a "Consoles" link in the navbar when
it is non-empty.
-->
<script>
const GLOBAL_PATH_PREFIX='PATH_PREFIX_PLACEHOLDER';
const GLOBAL_CONSOLES_LINK='CONSOLES_LINK_PLACEHOLDER';
</script>

View File

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

View File

@ -5,36 +5,62 @@ import { Container } from 'reactstrap';
import './App.css';
import { Router, Redirect } from '@reach/router';
import { Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList } from './pages';
import PathPrefixProps from './types/PathPrefixProps';
import { PathPrefixContext } from './contexts/PathPrefixContext';
interface AppProps {
consolesLink: string | null;
}
const App: FC<PathPrefixProps & AppProps> = ({ pathPrefix, consolesLink }) => {
return (
<>
<Navigation pathPrefix={pathPrefix} consolesLink={consolesLink} />
<Container fluid style={{ paddingTop: 70 }}>
<Router basepath={`${pathPrefix}/new`}>
<Redirect from="/" to={`${pathPrefix}/new/graph`} />
const App: FC<AppProps> = ({ consolesLink }) => {
// 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
// hosting and reverse proxy deployments with no additional configurations required.
let basePath = window.location.pathname;
const paths = [
'/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
React-handled router paths ("reactRouterPaths") in /web/web.go.
*/}
<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} />
<ServiceDiscovery path="/service-discovery" pathPrefix={pathPrefix} />
<Status path="/status" pathPrefix={pathPrefix} />
<TSDBStatus path="/tsdb-status" pathPrefix={pathPrefix} />
<Targets path="/targets" pathPrefix={pathPrefix} />
NOTE: Any route added here needs to also be added to the list of
React-handled router paths ("reactRouterPaths") in /web/web.go.
*/}
<PanelList path="/graph" />
<Alerts path="/alerts" />
<Config path="/config" />
<Flags path="/flags" />
<Rules path="/rules" />
<ServiceDiscovery path="/service-discovery" />
<Status path="/status" />
<TSDBStatus path="/tsdb-status" />
<Targets path="/targets" />
</Router>
</Container>
</>
</PathPrefixContext.Provider>
);
};

View File

@ -12,19 +12,20 @@ import {
DropdownMenu,
DropdownItem,
} from 'reactstrap';
import PathPrefixProps from './types/PathPrefixProps';
import { usePathPrefix } from './contexts/PathPrefixContext';
interface NavbarProps {
consolesLink: string | null;
}
const Navigation: FC<PathPrefixProps & NavbarProps> = ({ pathPrefix, consolesLink }) => {
const Navigation: FC<NavbarProps> = ({ consolesLink }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
const pathPrefix = usePathPrefix();
return (
<Navbar className="mb-3" dark color="dark" expand="md" fixed="top">
<NavbarToggler onClick={toggle} />
<Link className="pt-0 navbar-brand" to={`${pathPrefix}/new/graph`}>
<Link className="pt-0 navbar-brand" to={`${pathPrefix}/graph`}>
Prometheus
</Link>
<Collapse isOpen={isOpen} navbar style={{ justifyContent: 'space-between' }}>
@ -35,12 +36,12 @@ const Navigation: FC<PathPrefixProps & NavbarProps> = ({ pathPrefix, consolesLin
</NavItem>
)}
<NavItem>
<NavLink tag={Link} to={`${pathPrefix}/new/alerts`}>
<NavLink tag={Link} to={`${pathPrefix}/alerts`}>
Alerts
</NavLink>
</NavItem>
<NavItem>
<NavLink tag={Link} to={`${pathPrefix}/new/graph`}>
<NavLink tag={Link} to={`${pathPrefix}/graph`}>
Graph
</NavLink>
</NavItem>
@ -49,25 +50,25 @@ const Navigation: FC<PathPrefixProps & NavbarProps> = ({ pathPrefix, consolesLin
Status
</DropdownToggle>
<DropdownMenu>
<DropdownItem tag={Link} to={`${pathPrefix}/new/status`}>
<DropdownItem tag={Link} to={`${pathPrefix}/status`}>
Runtime & Build Information
</DropdownItem>
<DropdownItem tag={Link} to={`${pathPrefix}/new/tsdb-status`}>
<DropdownItem tag={Link} to={`${pathPrefix}/tsdb-status`}>
TSDB Status
</DropdownItem>
<DropdownItem tag={Link} to={`${pathPrefix}/new/flags`}>
<DropdownItem tag={Link} to={`${pathPrefix}/flags`}>
Command-Line Flags
</DropdownItem>
<DropdownItem tag={Link} to={`${pathPrefix}/new/config`}>
<DropdownItem tag={Link} to={`${pathPrefix}/config`}>
Configuration
</DropdownItem>
<DropdownItem tag={Link} to={`${pathPrefix}/new/rules`}>
<DropdownItem tag={Link} to={`${pathPrefix}/rules`}>
Rules
</DropdownItem>
<DropdownItem tag={Link} to={`${pathPrefix}/new/targets`}>
<DropdownItem tag={Link} to={`${pathPrefix}/targets`}>
Targets
</DropdownItem>
<DropdownItem tag={Link} to={`${pathPrefix}/new/service-discovery`}>
<DropdownItem tag={Link} to={`${pathPrefix}/service-discovery`}>
Service Discovery
</DropdownItem>
</DropdownMenu>
@ -76,7 +77,7 @@ const Navigation: FC<PathPrefixProps & NavbarProps> = ({ pathPrefix, consolesLin
<NavLink href="https://prometheus.io/docs/prometheus/latest/getting_started/">Help</NavLink>
</NavItem>
<NavItem>
<NavLink href={`${pathPrefix}/graph${window.location.search}`}>Classic UI</NavLink>
<NavLink href={`${pathPrefix}/../graph${window.location.search}`}>Classic UI</NavLink>
</NavItem>
</Nav>
</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';
// 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;
let prefix = GLOBAL_PATH_PREFIX;
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 (
GLOBAL_CONSOLES_LINK === 'CONSOLES_LINK_PLACEHOLDER' ||
GLOBAL_CONSOLES_LINK === '' ||
@ -28,4 +18,4 @@ if (
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 { RouteComponentProps } from '@reach/router';
import PathPrefixProps from '../../types/PathPrefixProps';
import { useFetch } from '../../hooks/useFetch';
import { withStatusIndicator } from '../../components/withStatusIndicator';
import AlertsContent, { RuleStatus, AlertsProps } from './AlertContents';
import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { API_PATH } from '../../constants/constants';
const AlertsWithStatusIndicator = withStatusIndicator(AlertsContent);
const Alerts: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' }) => {
const { response, error, isLoading } = useFetch<AlertsProps>(`${pathPrefix}/api/v1/rules?type=alert`);
const Alerts: FC<RouteComponentProps> = () => {
const pathPrefix = usePathPrefix();
const { response, error, isLoading } = useFetch<AlertsProps>(`${pathPrefix}/${API_PATH}/rules?type=alert`);
const ruleStatsCount: RuleStatus<number> = {
inactive: 0,

View File

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

View File

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

View File

@ -10,8 +10,8 @@ import { GraphTabContent } from './GraphTabContent';
import DataTable from './DataTable';
import TimeInput from './TimeInput';
import QueryStatsView, { QueryStats } from './QueryStatsView';
import PathPrefixProps from '../../types/PathPrefixProps';
import { QueryParams } from '../../types/types';
import { API_PATH } from '../../constants/constants';
interface PanelProps {
options: PanelOptions;
@ -21,6 +21,7 @@ interface PanelProps {
metricNames: string[];
removePanel: () => void;
onExecuteQuery: (query: string) => void;
pathPrefix: string;
}
interface PanelState {
@ -55,7 +56,7 @@ export const PanelDefaultOptions: PanelOptions = {
stacked: false,
};
class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
class Panel extends Component<PanelProps, PanelState> {
private abortInFlightFetch: (() => void) | null = null;
constructor(props: PanelProps) {
@ -117,21 +118,20 @@ class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
let path: string;
switch (this.props.options.type) {
case 'graph':
path = '/api/v1/query_range';
path = '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 = 'query';
params.append('time', endTime.toString());
break;
default:
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',
credentials: 'same-origin',
signal: abortController.signal,

View File

@ -4,10 +4,11 @@ import { Alert, Button } from 'reactstrap';
import Panel, { PanelOptions, PanelDefaultOptions } from './Panel';
import Checkbox from '../../components/Checkbox';
import PathPrefixProps from '../../types/PathPrefixProps';
import { generateID, decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString, callAll } from '../../utils';
import { useFetch } from '../../hooks/useFetch';
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 };
@ -16,20 +17,14 @@ export const updateURL = (nextPanels: PanelMeta[]) => {
window.history.pushState({}, '', query);
};
interface PanelListProps extends PathPrefixProps, RouteComponentProps {
interface PanelListProps extends RouteComponentProps {
panels: PanelMeta[];
metrics: string[];
useLocalTime: boolean;
queryHistoryEnabled: boolean;
}
export const PanelListContent: FC<PanelListProps> = ({
metrics = [],
useLocalTime,
pathPrefix,
queryHistoryEnabled,
...rest
}) => {
export const PanelListContent: FC<PanelListProps> = ({ metrics = [], useLocalTime, queryHistoryEnabled, ...rest }) => {
const [panels, setPanels] = useState(rest.panels);
const [historyItems, setLocalStorageHistoryItems] = useLocalStorage<string[]>('history', []);
@ -73,10 +68,13 @@ export const PanelListContent: FC<PanelListProps> = ({
]);
};
const pathPrefix = usePathPrefix();
return (
<>
{panels.map(({ id, options }) => (
<Panel
pathPrefix={pathPrefix}
onExecuteQuery={handleExecuteQuery}
key={id}
options={options}
@ -97,7 +95,6 @@ export const PanelListContent: FC<PanelListProps> = ({
useLocalTime={useLocalTime}
metricNames={metrics}
pastQueries={queryHistoryEnabled ? historyItems : []}
pathPrefix={pathPrefix}
/>
))}
<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 [useLocalTime, setUseLocalTime] = useLocalStorage('use-local-time', 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 { 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(() => {
if (timeRes.data) {
@ -164,7 +164,6 @@ const PanelList: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = ''
)}
<PanelListContent
panels={decodePanelOptionsFromQueryString(window.location.search)}
pathPrefix={pathPrefix}
useLocalTime={useLocalTime}
metrics={metricsRes.data}
queryHistoryEnabled={enableQueryHistory}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,5 @@ describe('Targets', () => {
const scrapePoolList = targets.find(ScrapePoolList);
expect(scrapePoolList).toHaveLength(1);
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 Filter from './Filter';
import ScrapePoolList from './ScrapePoolList';
import PathPrefixProps from '../../types/PathPrefixProps';
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 filterProps = { filter, setFilter };
const scrapePoolListProps = { filter, pathPrefix };
const scrapePoolListProps = { filter, pathPrefix, API_PATH };
return (
<>

View File

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

View File

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

View File

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

View File

@ -79,7 +79,6 @@ var reactRouterPaths = []string{
"/status",
"/targets",
"/tsdb-status",
"/version",
}
// 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)
return
}
replacedIdx := bytes.ReplaceAll(idx, []byte("PATH_PREFIX_PLACEHOLDER"), []byte(o.ExternalURL.Path))
replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("CONSOLES_LINK_PLACEHOLDER"), []byte(h.consolesPath()))
replacedIdx := bytes.ReplaceAll(idx, []byte("CONSOLES_LINK_PLACEHOLDER"), []byte(h.consolesPath()))
replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("TITLE_PLACEHOLDER"), []byte(h.options.PageTitle))
w.Write(replacedIdx)
return