From 1cffda5de775668f7438578c90a05e8bfa58eb91 Mon Sep 17 00:00:00 2001 From: James Ranson Date: Thu, 22 Oct 2020 09:22:32 -0600 Subject: [PATCH] react updates for pathPrefix (#7979) * dynamically determine path prefix Signed-off-by: James Ranson * minor changes per PR review Signed-off-by: James Ranson * use Context for apiPath and pathPrefix Signed-off-by: James Ranson * remove unhandled "/version" path Signed-off-by: James Ranson * only process index once instead of on every req Signed-off-by: James Ranson * remove unneeded tag fragment Signed-off-by: James Ranson * switch api path to const Signed-off-by: James Ranson * revert Signed-off-by: James Ranson * update tests Signed-off-by: James Ranson * linter updates Signed-off-by: James Ranson * simplify Signed-off-by: James Ranson * updates per peer review Signed-off-by: James Ranson --- web/ui/react-app/public/index.html | 5 -- web/ui/react-app/src/App.test.tsx | 3 +- web/ui/react-app/src/App.tsx | 68 +++++++++++++------ web/ui/react-app/src/Navbar.tsx | 27 ++++---- web/ui/react-app/src/constants/constants.tsx | 1 + .../src/contexts/PathPrefixContext.tsx | 9 +++ web/ui/react-app/src/index.tsx | 12 +--- web/ui/react-app/src/pages/alerts/Alerts.tsx | 8 ++- web/ui/react-app/src/pages/config/Config.tsx | 8 ++- web/ui/react-app/src/pages/flags/Flags.tsx | 8 ++- web/ui/react-app/src/pages/graph/Panel.tsx | 12 ++-- .../react-app/src/pages/graph/PanelList.tsx | 27 ++++---- web/ui/react-app/src/pages/rules/Rules.tsx | 8 ++- .../src/pages/serviceDiscovery/Services.tsx | 8 ++- web/ui/react-app/src/pages/status/Status.tsx | 8 ++- .../src/pages/targets/ScrapePoolList.test.tsx | 35 ++++++++-- .../src/pages/targets/ScrapePoolList.tsx | 8 ++- .../src/pages/targets/Targets.test.tsx | 1 - .../react-app/src/pages/targets/Targets.tsx | 8 ++- .../src/pages/tsdbStatus/TSDBStatus.test.tsx | 9 ++- .../src/pages/tsdbStatus/TSDBStatus.tsx | 8 ++- web/ui/react-app/src/types/PathPrefixProps.ts | 5 -- web/web.go | 4 +- 23 files changed, 173 insertions(+), 117 deletions(-) create mode 100644 web/ui/react-app/src/constants/constants.tsx create mode 100644 web/ui/react-app/src/contexts/PathPrefixContext.tsx delete mode 100644 web/ui/react-app/src/types/PathPrefixProps.ts diff --git a/web/ui/react-app/public/index.html b/web/ui/react-app/public/index.html index cc3336fa25..c19bb569bf 100755 --- a/web/ui/react-app/public/index.html +++ b/web/ui/react-app/public/index.html @@ -10,16 +10,11 @@ diff --git a/web/ui/react-app/src/App.test.tsx b/web/ui/react-app/src/App.test.tsx index 705e0bc6b8..e96bbf7a40 100755 --- a/web/ui/react-app/src/App.test.tsx +++ b/web/ui/react-app/src/App.test.tsx @@ -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(); + const app = shallow(); 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); diff --git a/web/ui/react-app/src/App.tsx b/web/ui/react-app/src/App.tsx index d172773e0d..29adb54b6f 100755 --- a/web/ui/react-app/src/App.tsx +++ b/web/ui/react-app/src/App.tsx @@ -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 = ({ pathPrefix, consolesLink }) => { - return ( - <> - - - - +const App: FC = ({ 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 ( + + + + + {/* - NOTE: Any route added here needs to also be added to the list of - React-handled router paths ("reactRouterPaths") in /web/web.go. - */} - - - - - - - - - + NOTE: Any route added here needs to also be added to the list of + React-handled router paths ("reactRouterPaths") in /web/web.go. + */} + + + + + + + + + - + ); }; diff --git a/web/ui/react-app/src/Navbar.tsx b/web/ui/react-app/src/Navbar.tsx index 51c072add4..a425c33cb2 100644 --- a/web/ui/react-app/src/Navbar.tsx +++ b/web/ui/react-app/src/Navbar.tsx @@ -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 = ({ pathPrefix, consolesLink }) => { +const Navigation: FC = ({ consolesLink }) => { const [isOpen, setIsOpen] = useState(false); const toggle = () => setIsOpen(!isOpen); + const pathPrefix = usePathPrefix(); return ( - + Prometheus @@ -35,12 +36,12 @@ const Navigation: FC = ({ pathPrefix, consolesLin )} - + Alerts - + Graph @@ -49,25 +50,25 @@ const Navigation: FC = ({ pathPrefix, consolesLin Status - + Runtime & Build Information - + TSDB Status - + Command-Line Flags - + Configuration - + Rules - + Targets - + Service Discovery @@ -76,7 +77,7 @@ const Navigation: FC = ({ pathPrefix, consolesLin Help - Classic UI + Classic UI diff --git a/web/ui/react-app/src/constants/constants.tsx b/web/ui/react-app/src/constants/constants.tsx new file mode 100644 index 0000000000..5f0d8b9b4a --- /dev/null +++ b/web/ui/react-app/src/constants/constants.tsx @@ -0,0 +1 @@ +export const API_PATH = '../api/v1'; diff --git a/web/ui/react-app/src/contexts/PathPrefixContext.tsx b/web/ui/react-app/src/contexts/PathPrefixContext.tsx new file mode 100644 index 0000000000..38e4eec096 --- /dev/null +++ b/web/ui/react-app/src/contexts/PathPrefixContext.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +const PathPrefixContext = React.createContext(''); + +function usePathPrefix() { + return React.useContext(PathPrefixContext); +} + +export { usePathPrefix, PathPrefixContext }; diff --git a/web/ui/react-app/src/index.tsx b/web/ui/react-app/src/index.tsx index f6e672829a..9398bc8983 100755 --- a/web/ui/react-app/src/index.tsx +++ b/web/ui/react-app/src/index.tsx @@ -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(, document.getElementById('root')); +ReactDOM.render(, document.getElementById('root')); diff --git a/web/ui/react-app/src/pages/alerts/Alerts.tsx b/web/ui/react-app/src/pages/alerts/Alerts.tsx index c6fef5b767..a1624d7a87 100644 --- a/web/ui/react-app/src/pages/alerts/Alerts.tsx +++ b/web/ui/react-app/src/pages/alerts/Alerts.tsx @@ -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 = ({ pathPrefix = '' }) => { - const { response, error, isLoading } = useFetch(`${pathPrefix}/api/v1/rules?type=alert`); +const Alerts: FC = () => { + const pathPrefix = usePathPrefix(); + const { response, error, isLoading } = useFetch(`${pathPrefix}/${API_PATH}/rules?type=alert`); const ruleStatsCount: RuleStatus = { inactive: 0, diff --git a/web/ui/react-app/src/pages/config/Config.tsx b/web/ui/react-app/src/pages/config/Config.tsx index 1636668e6c..00bb56d19c 100644 --- a/web/ui/react-app/src/pages/config/Config.tsx +++ b/web/ui/react-app/src/pages/config/Config.tsx @@ -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 = ({ error, data }) => { ); }; -const Config: FC = ({ pathPrefix }) => { - const { response, error } = useFetch(`${pathPrefix}/api/v1/status/config`); +const Config: FC = () => { + const pathPrefix = usePathPrefix(); + const { response, error } = useFetch(`${pathPrefix}/${API_PATH}/status/config`); return ; }; diff --git a/web/ui/react-app/src/pages/flags/Flags.tsx b/web/ui/react-app/src/pages/flags/Flags.tsx index 5f0a409650..60b67ae771 100644 --- a/web/ui/react-app/src/pages/flags/Flags.tsx +++ b/web/ui/react-app/src/pages/flags/Flags.tsx @@ -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 = ({ pathPrefix = '' }) => { - const { response, error, isLoading } = useFetch(`${pathPrefix}/api/v1/status/flags`); +const Flags: FC = () => { + const pathPrefix = usePathPrefix(); + const { response, error, isLoading } = useFetch(`${pathPrefix}/${API_PATH}/status/flags`); return ; }; diff --git a/web/ui/react-app/src/pages/graph/Panel.tsx b/web/ui/react-app/src/pages/graph/Panel.tsx index 29dbbc4dc7..bd103de4d2 100644 --- a/web/ui/react-app/src/pages/graph/Panel.tsx +++ b/web/ui/react-app/src/pages/graph/Panel.tsx @@ -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 { +class Panel extends Component { private abortInFlightFetch: (() => void) | null = null; constructor(props: PanelProps) { @@ -117,21 +118,20 @@ class Panel extends Component { 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, diff --git a/web/ui/react-app/src/pages/graph/PanelList.tsx b/web/ui/react-app/src/pages/graph/PanelList.tsx index 8444b09872..b8e29bcb0f 100644 --- a/web/ui/react-app/src/pages/graph/PanelList.tsx +++ b/web/ui/react-app/src/pages/graph/PanelList.tsx @@ -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 = ({ - metrics = [], - useLocalTime, - pathPrefix, - queryHistoryEnabled, - ...rest -}) => { +export const PanelListContent: FC = ({ metrics = [], useLocalTime, queryHistoryEnabled, ...rest }) => { const [panels, setPanels] = useState(rest.panels); const [historyItems, setLocalStorageHistoryItems] = useLocalStorage('history', []); @@ -73,10 +68,13 @@ export const PanelListContent: FC = ({ ]); }; + const pathPrefix = usePathPrefix(); + return ( <> {panels.map(({ id, options }) => ( = ({ useLocalTime={useLocalTime} metricNames={metrics} pastQueries={queryHistoryEnabled ? historyItems : []} - pathPrefix={pathPrefix} /> ))}