From a7657268f1c4a3ae671b231678696acc4f3c6671 Mon Sep 17 00:00:00 2001 From: aptmac Date: Fri, 17 Jan 2025 13:00:37 -0500 Subject: [PATCH] feat(routes): allow setting a basepath via an environment variable (#1534) --- src/app/AppLayout/AppLayout.tsx | 4 +- src/app/AppLayout/SslErrorModal.tsx | 4 +- src/app/CreateRecording/CreateRecording.tsx | 3 +- .../Charts/jfr/JFRMetricsChartCard.tsx | 3 +- src/app/Dashboard/Dashboard.tsx | 3 +- src/app/Dashboard/DashboardSolo.tsx | 5 +- src/app/Events/EventTemplates.tsx | 4 +- src/app/Rules/CreateRule.tsx | 4 +- src/app/Topology/Actions/CreateTarget.tsx | 4 +- .../Actions/quicksearches/custom-target.tsx | 3 +- src/app/Topology/Actions/utils.tsx | 9 ++-- src/app/routes.tsx | 47 ++++++++----------- src/app/utils/utils.ts | 14 ++++++ src/test/utils.tsx | 3 +- webpack.dev.js | 3 +- webpack.prod.js | 3 +- 16 files changed, 65 insertions(+), 51 deletions(-) diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index 1f5a6c078..7b6a9895a 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -33,7 +33,7 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { useTheme } from '@app/utils/hooks/useTheme'; import { saveToLocalStorage } from '@app/utils/LocalStorage'; -import { cleanDataId, isAssetNew, openTabForUrl, portalRoot } from '@app/utils/utils'; +import { cleanDataId, isAssetNew, openTabForUrl, portalRoot, toPath } from '@app/utils/utils'; import { useCryostatTranslation } from '@i18n/i18nextUtil'; import { Alert, @@ -272,7 +272,7 @@ export const AppLayout: React.FC = ({ children }) => { if (location.pathname === '/settings') { selectTab(SettingTab.GENERAL); } else { - navigate(`/settings?${new URLSearchParams({ tab: tabAsParam(SettingTab.GENERAL) })}`); + navigate(toPath(`/settings?${new URLSearchParams({ tab: tabAsParam(SettingTab.GENERAL) })}`)); } }, [location, navigate]); diff --git a/src/app/AppLayout/SslErrorModal.tsx b/src/app/AppLayout/SslErrorModal.tsx index 60444de44..c07d02f4b 100644 --- a/src/app/AppLayout/SslErrorModal.tsx +++ b/src/app/AppLayout/SslErrorModal.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { portalRoot } from '@app/utils/utils'; +import { portalRoot, toPath } from '@app/utils/utils'; import { Button, Modal, ModalVariant, Text } from '@patternfly/react-core'; import * as React from 'react'; import { useNavigate } from 'react-router-dom-v5-compat'; @@ -27,7 +27,7 @@ export const SslErrorModal: React.FC = ({ visible, onDismiss const navigate = useNavigate(); const handleClick = React.useCallback(() => { - navigate('/security'); + navigate(toPath('/security')); onDismiss(); }, [navigate, onDismiss]); diff --git a/src/app/CreateRecording/CreateRecording.tsx b/src/app/CreateRecording/CreateRecording.tsx index 0fe1ed173..c4e1a16b3 100644 --- a/src/app/CreateRecording/CreateRecording.tsx +++ b/src/app/CreateRecording/CreateRecording.tsx @@ -15,6 +15,7 @@ */ import { TargetView } from '@app/TargetView/TargetView'; +import { toPath } from '@app/utils/utils'; import { Card, CardBody, Tab, Tabs, TabTitleText } from '@patternfly/react-core'; import * as React from 'react'; import { CustomRecordingForm } from './CustomRecordingForm'; @@ -29,7 +30,7 @@ export const CreateRecording: React.FC = () => { ); return ( - + diff --git a/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx b/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx index c2ce24808..64555b92d 100644 --- a/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx +++ b/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx @@ -25,6 +25,7 @@ import { FeatureLevel } from '@app/Shared/Services/service.types'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { useTheme } from '@app/utils/hooks/useTheme'; +import { toPath } from '@app/utils/utils'; import { useCryostatTranslation } from '@i18n/i18nextUtil'; import { Bullseye, @@ -213,7 +214,7 @@ export const JFRMetricsChartCard: DashboardCardFC = (p }, [props.actions, props.chartKind, props.duration, props.period, t, controllerState, actions]); const handleCreateRecording = React.useCallback(() => { - navigate('/recordings/create', { + navigate(toPath('/recordings/create'), { state: { name: RECORDING_NAME, template: { diff --git a/src/app/Dashboard/Dashboard.tsx b/src/app/Dashboard/Dashboard.tsx index 1f6697268..52d499643 100644 --- a/src/app/Dashboard/Dashboard.tsx +++ b/src/app/Dashboard/Dashboard.tsx @@ -26,6 +26,7 @@ import { FeatureLevel } from '@app/Shared/Services/service.types'; import { ServiceContext } from '@app/Shared/Services/Services'; import { TargetView } from '@app/TargetView/TargetView'; import { getFromLocalStorage } from '@app/utils/LocalStorage'; +import { toPath } from '@app/utils/utils'; import { useCryostatTranslation } from '@i18n/i18nextUtil'; import { Grid, GridItem } from '@patternfly/react-core'; import * as React from 'react'; @@ -130,7 +131,7 @@ export const Dashboard: React.FC = (_) => { key={`${cfg.name}-actions`} onRemove={() => handleRemove(idx)} onResetSize={() => handleResetSize(idx)} - onView={() => navigate(`/d-solo?layout=${currLayout.name}&cardId=${cfg.id}`)} + onView={() => navigate(toPath(`/d-solo?layout=${currLayout.name}&cardId=${cfg.id}`))} />, ], }) diff --git a/src/app/Dashboard/DashboardSolo.tsx b/src/app/Dashboard/DashboardSolo.tsx index 0296dd843..f1bcf7ac6 100644 --- a/src/app/Dashboard/DashboardSolo.tsx +++ b/src/app/Dashboard/DashboardSolo.tsx @@ -15,6 +15,7 @@ */ import { RootState } from '@app/Shared/Redux/ReduxStore'; import { TargetView } from '@app/TargetView/TargetView'; +import { toPath } from '@app/utils/utils'; import { Bullseye, Button, @@ -69,7 +70,7 @@ const DashboardSolo: React.FC = () => { Provide valid layout and cardId query parameters and try again. - @@ -80,7 +81,7 @@ const DashboardSolo: React.FC = () => { const { name, span, props } = cardConfig; return ( // Use default chart controller - +
{React.createElement(getCardDescriptorByName(name).component, { span: span, diff --git a/src/app/Events/EventTemplates.tsx b/src/app/Events/EventTemplates.tsx index 42d182871..11a66b742 100644 --- a/src/app/Events/EventTemplates.tsx +++ b/src/app/Events/EventTemplates.tsx @@ -25,7 +25,7 @@ import { LoadingProps } from '@app/Shared/Components/types'; import { EventTemplate, NotificationCategory, Target } from '@app/Shared/Services/api.types'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; -import { portalRoot, sortResources, TableColumn } from '@app/utils/utils'; +import { portalRoot, sortResources, TableColumn, toPath } from '@app/utils/utils'; import { useCryostatTranslation } from '@i18n/i18nextUtil'; import { ActionGroup, @@ -255,7 +255,7 @@ export const EventTemplates: React.FC = () => { { title: 'Create Recording...', onClick: () => - navigate('/recordings/create', { + navigate(toPath('/recordings/create'), { state: { template: { name: t.name, type: t.type } } as Partial, }), }, diff --git a/src/app/Rules/CreateRule.tsx b/src/app/Rules/CreateRule.tsx index 41be5a7e7..565d9a549 100644 --- a/src/app/Rules/CreateRule.tsx +++ b/src/app/Rules/CreateRule.tsx @@ -27,7 +27,7 @@ import { SearchExprServiceContext } from '@app/Shared/Services/service.utils'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useMatchExpressionSvc } from '@app/utils/hooks/useMatchExpressionSvc'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; -import { portalRoot } from '@app/utils/utils'; +import { portalRoot, toPath } from '@app/utils/utils'; import { useCryostatTranslation } from '@i18n/i18nextUtil'; import { ActionGroup, @@ -656,7 +656,7 @@ export const CreateRule: React.FC = () => { () => [ { title: t('AUTOMATED_RULES'), - path: '/rules', + path: toPath('/rules'), }, ], [t], diff --git a/src/app/Topology/Actions/CreateTarget.tsx b/src/app/Topology/Actions/CreateTarget.tsx index 03c4958cd..12c26eaa4 100644 --- a/src/app/Topology/Actions/CreateTarget.tsx +++ b/src/app/Topology/Actions/CreateTarget.tsx @@ -22,7 +22,7 @@ import { Target } from '@app/Shared/Services/api.types'; import { ServiceContext } from '@app/Shared/Services/Services'; import '@app/Topology/styles/base.css'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; -import { getAnnotation, portalRoot } from '@app/utils/utils'; +import { getAnnotation, portalRoot, toPath } from '@app/utils/utils'; import { useCryostatTranslation } from '@i18n/i18nextUtil'; import { Accordion, @@ -286,7 +286,7 @@ export const CreateTarget: React.FC = ({ prefilled }) => { }, [validConnectUrl, example]); return ( - + Create Custom Target diff --git a/src/app/Topology/Actions/quicksearches/custom-target.tsx b/src/app/Topology/Actions/quicksearches/custom-target.tsx index 95d583ce3..4cb558c9a 100644 --- a/src/app/Topology/Actions/quicksearches/custom-target.tsx +++ b/src/app/Topology/Actions/quicksearches/custom-target.tsx @@ -15,6 +15,7 @@ */ import openjdkSvg from '@app/assets/openjdk.svg'; import { FeatureLevel } from '@app/Shared/Services/service.types'; +import { toPath } from '@app/utils/utils'; import { QuickSearchItem } from '../types'; const _CustomTargetSearchItem: QuickSearchItem = { @@ -31,7 +32,7 @@ const _CustomTargetSearchItem: QuickSearchItem = { descriptionFull: 'Provide a JMX Service URL along with necessary credentials to point to a Target JVM.', featureLevel: FeatureLevel.PRODUCTION, createAction: ({ navigate }) => { - navigate('/topology/create-custom-target'); + navigate(toPath('/topology/create-custom-target')); }, }; diff --git a/src/app/Topology/Actions/utils.tsx b/src/app/Topology/Actions/utils.tsx index f31dad221..a35848cb6 100644 --- a/src/app/Topology/Actions/utils.tsx +++ b/src/app/Topology/Actions/utils.tsx @@ -24,6 +24,7 @@ import { } from '@app/Shared/Services/api.types'; import { getAllLeaves, isTargetNode } from '@app/Shared/Services/api.utils'; import { NotificationService } from '@app/Shared/Services/Notifications.service'; +import { toPath } from '@app/utils/utils'; import { ContextMenuSeparator } from '@patternfly/react-topology'; import { merge, filter, map, debounceTime } from 'rxjs'; import { getJvmIdFromEvent } from '../Entity/utils'; @@ -68,7 +69,7 @@ export const nodeActions: NodeAction[] = [ const targetNode: TargetNode = element.getData(); services.target.setTarget(targetNode.target); - navigate('/'); + navigate(toPath('/')); }, title: 'View Dashboard', }, @@ -78,7 +79,7 @@ export const nodeActions: NodeAction[] = [ const targetNode: TargetNode = element.getData(); services.target.setTarget(targetNode.target); - navigate('/recordings'); + navigate(toPath('/recordings')); }, title: 'View Recordings', }, @@ -89,7 +90,7 @@ export const nodeActions: NodeAction[] = [ const targetNode: TargetNode = element.getData(); services.target.setTarget(targetNode.target); - navigate('/recordings/create'); + navigate(toPath('/recordings/create')); }, title: 'Create Recordings', }, @@ -99,7 +100,7 @@ export const nodeActions: NodeAction[] = [ const targetNode: TargetNode = element.getData(); services.target.setTarget(targetNode.target); - navigate('/rules/create'); + navigate(toPath('/rules/create')); }, title: 'Create Automated Rules', }, diff --git a/src/app/routes.tsx b/src/app/routes.tsx index ac2aa8506..3f8c9cd5a 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -35,7 +35,7 @@ import CreateTarget from './Topology/Actions/CreateTarget'; import Topology from './Topology/Topology'; import { useDocumentTitle } from './utils/hooks/useDocumentTitle'; import { useFeatureLevel } from './utils/hooks/useFeatureLevel'; -import { accessibleRouteChangeHandler } from './utils/utils'; +import { accessibleRouteChangeHandler, BASEPATH, toPath } from './utils/utils'; let routeFocusTimer: number; const OVERVIEW = 'Overview'; @@ -57,56 +57,51 @@ const routes: IAppRoute[] = [ { component: About, label: 'About', - path: '/about', + path: toPath('/about'), title: 'About', description: 'Get information, help, or support for Cryostat.', navGroup: OVERVIEW, }, { component: Dashboard, - label: 'Dashboard', - path: '/', + path: toPath('/'), title: 'Dashboard', navGroup: OVERVIEW, children: [ { component: DashboardSolo, - path: '/d-solo', + path: toPath('/d-solo'), title: 'Dashboard', }, ], }, { component: QuickStarts, - label: 'Quick starts', - path: '/quickstarts', + path: toPath('/quickstarts'), title: 'Quick starts', description: 'Get started with Cryostat.', }, { component: Topology, - label: 'Topology', - path: '/topology', + path: toPath('/topology'), title: 'Topology', navGroup: OVERVIEW, children: [ { component: CreateTarget, - - path: '/topology/create-custom-target', + path: toPath('/topology/create-custom-target'), title: 'Create Custom Target', }, ], }, { component: RulesTable, - label: 'Automated Rules', - path: '/rules', + path: toPath('/rules'), title: 'Automated Rules', description: 'Create Recordings on multiple target JVMs at once using Automated Rules consisting of a name, Match Expression, template, archival period, and more.', @@ -114,34 +109,30 @@ const routes: IAppRoute[] = [ children: [ { component: CreateRule, - - path: '/rules/create', + path: toPath('/rules/create'), title: 'Create Automated Rule', }, ], }, { component: Recordings, - label: 'Recordings', - path: '/recordings', + path: toPath('/recordings'), title: 'Recordings', description: 'Create, view and archive JFR Recordings on single target JVMs.', navGroup: CONSOLE, children: [ { component: CreateRecording, - - path: '/recordings/create', + path: toPath('/recordings/create'), title: 'Create Recording', }, ], }, { component: Archives, - label: 'Archives', - path: '/archives', + path: toPath('/archives'), title: 'Archives', description: 'View Archived Recordings across all target JVMs, as well as upload Recordings directly to the archive.', @@ -149,26 +140,23 @@ const routes: IAppRoute[] = [ }, { component: Events, - label: 'Events', - path: '/events', + path: toPath('/events'), title: 'Events', description: 'View available JFR Event Templates and types for target JVMs, as well as upload custom templates.', navGroup: CONSOLE, }, { component: SecurityPanel, - label: 'Security', - path: '/security', + path: toPath('/security'), title: 'Security', description: 'Upload SSL/TLS certificates for Cryostat to trust when communicating with target applications.', navGroup: CONSOLE, }, { component: Settings, - - path: '/settings', + path: toPath('/settings'), title: 'Settings', description: 'View or modify Cryostat web-client application settings.', }, @@ -176,7 +164,10 @@ const routes: IAppRoute[] = [ const flatten = (routes: IAppRoute[]): IAppRoute[] => { const ret: IAppRoute[] = []; - for (const r of routes) { + for (var r of routes) { + if (BASEPATH) { + r.path = `/${BASEPATH}/${r.path}`; + } ret.push(r); if (r.children) { ret.push(...flatten(r.children)); diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts index d0275a422..02bce9f9c 100644 --- a/src/app/utils/utils.ts +++ b/src/app/utils/utils.ts @@ -25,6 +25,7 @@ import semverGt from 'semver/functions/gt'; import semverValid from 'semver/functions/valid'; import { getFromLocalStorage } from './LocalStorage'; +export const BASEPATH = process.env.BASEPATH || ''; const SECOND_MILLIS = 1000; const MINUTE_MILLIS = 60 * SECOND_MILLIS; const HOUR_MILLIS = 60 * MINUTE_MILLIS; @@ -268,3 +269,16 @@ export const isAssetNew = (currVer: string) => { const oldVer: string = getFromLocalStorage('ASSET_VERSION', '0.0.0'); return !semverValid(oldVer) || semverGt(currVer, oldVer); }; + +/** + * Formats a route path by prepending a basepath if necessary + * @param {string} path - the target path within cryostat-web + */ +export const toPath = (path: string) => { + if (BASEPATH) { + // incoming path already includes /, so don't add another one here + return `/${BASEPATH}${path}`; + } else { + return path; + } +}; diff --git a/src/test/utils.tsx b/src/test/utils.tsx index 928a3a4b1..0b5d9aa00 100644 --- a/src/test/utils.tsx +++ b/src/test/utils.tsx @@ -25,6 +25,7 @@ import { } from '@app/Shared/Redux/ReduxStore'; import { NotificationsContext, NotificationsInstance } from '@app/Shared/Services/Notifications.service'; import { defaultServices, ServiceContext } from '@app/Shared/Services/Services'; +import { toPath } from '@app/utils/utils'; import { PreloadedState } from '@reduxjs/toolkit'; import { render as tlrRender } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -70,7 +71,7 @@ const _setupProviders = (providers: ProviderInstance[]) => { const _setupRoutes = ({ routes, options }: RenderOptions['routerConfigs']) => { if (!routes.some((r) => r.path === '/')) { - routes = [{ path: '/', element: <>Root }, ...routes]; + routes = [{ path: toPath('/'), element: <>Root }, ...routes]; } options = options ?? { initialEntries: routes.map((r) => r.path ?? '').filter((p) => p != '') }; return { routes, options }; diff --git a/webpack.dev.js b/webpack.dev.js index ed6f033ca..b07af7f9a 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -36,7 +36,8 @@ module.exports = merge(common('development'), { // In preview mode, a base url is required. CRYOSTAT_AUTHORITY: process.env.PREVIEW? 'http://localhost:8181': '', PREVIEW: process.env.PREVIEW || 'false', - I18N_NAMESPACE: process.env.I18N_NAMESPACE || '' + I18N_NAMESPACE: process.env.I18N_NAMESPACE || '', + BASEPATH: process.env.BASEPATH || '' }) ], module: { diff --git a/webpack.prod.js b/webpack.prod.js index e875652a5..15e642761 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -45,7 +45,8 @@ module.exports = merge(common('production'), { new EnvironmentPlugin({ CRYOSTAT_AUTHORITY: process.env.PREVIEW ? 'http://localhost:8181' : '', PREVIEW: process.env.PREVIEW || 'false', - I18N_NAMESPACE: process.env.I18N_NAMESPACE || '' + I18N_NAMESPACE: process.env.I18N_NAMESPACE || '', + BASEPATH: process.env.BASEPATH || '' }) ], module: {