From d975a6e1ca68877b364f3ecf1fe76fc24719d0ca Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 1 May 2026 21:15:33 +0800 Subject: [PATCH 1/2] HDDS-15148. Prevent 404 when toggling between Old/New UI on UI-exclusive routes Recon's Old and New UIs share most routes but have asymmetric paths: /Capacity exists only in New UI, /MissingContainers only in Old UI. The "Switch to" toggle previously swapped the routes table without rewriting the URL hash, so users on a UI-exclusive path saw a 404 after toggling. Validate the current path against the destination route table on toggle. If absent, redirect to /Overview and show an info toast explaining where the view actually lives. Skip parameterized routes when checking membership so the v1 /:NotFound catch-all does not falsely match every path. --- .../webapps/recon/ozone-recon-web/src/app.tsx | 61 +++++++++++++++---- .../v2/constants/breadcrumbs.constants.tsx | 3 +- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.tsx index 3f1327f1d6cd..63837fa749e8 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.tsx @@ -18,7 +18,7 @@ import React, { Suspense } from 'react'; -import { Switch as AntDSwitch, Layout } from 'antd'; +import { Switch as AntDSwitch, Layout, message } from 'antd'; import NavBar from './components/navBar/navBar'; import NavBarV2 from '@/v2/components/navBar/navBar'; import Breadcrumbs from './components/breadcrumbs/breadcrumbs'; @@ -26,6 +26,8 @@ import BreadcrumbsV2 from '@/v2/components/breadcrumbs/breadcrumbs'; import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom'; import { routes } from '@/routes'; import { routesV2 } from '@/v2/routes-v2'; +import { breadcrumbNameMap as breadcrumbNameMapV1 } from '@/constants/breadcrumbs.constants'; +import { breadcrumbNameMap as breadcrumbNameMapV2 } from '@/v2/constants/breadcrumbs.constants'; import { MakeRouteWithSubRoutes } from '@/makeRouteWithSubRoutes'; import classNames from 'classnames'; @@ -38,6 +40,18 @@ const { Header, Content, Footer } = Layout; +const FALLBACK_PATH = '/Overview'; +const TOAST_DURATION_SECONDS = 4; +type BreadcrumbNameMap = typeof breadcrumbNameMapV1; + +// Strict membership check that ignores parameterized/catch-all entries +// (the v1 routes table ends with `/:NotFound`, which would otherwise match anything). +const pathExistsIn = (path: string, table: ReadonlyArray<{ path: string }>): boolean => + table.some((r) => !r.path.includes(':') && r.path === path); + +const getViewName = (path: string, preferredMap: BreadcrumbNameMap): string => + preferredMap[path] ?? path; + interface IAppState { collapsed: boolean; enableOldUI: boolean; @@ -57,6 +71,36 @@ class App extends React.Component, IAppState> { this.setState({ collapsed }); }; + handleUIToggle = (enableOldUI: boolean) => { + const currentPath = window.location.hash.slice(1).split('?')[0] || FALLBACK_PATH; + const targetTable = enableOldUI ? routes : routesV2; + const sourceTable = enableOldUI ? routesV2 : routes; + const shouldRedirect = !pathExistsIn(currentPath, targetTable); + let redirectMessage: string | undefined; + + if (shouldRedirect) { + window.location.hash = FALLBACK_PATH; + // Only explain the redirect when the user came from a real page in the source UI; + // a typo'd path otherwise produces a misleading "only available in..." message. + if (pathExistsIn(currentPath, sourceTable)) { + const sourceMap = enableOldUI ? breadcrumbNameMapV2 : breadcrumbNameMapV1; + const targetMap = enableOldUI ? breadcrumbNameMapV1 : breadcrumbNameMapV2; + const friendly = getViewName(currentPath, sourceMap); + const fallbackViewName = getViewName(FALLBACK_PATH, targetMap); + const sourceUiName = enableOldUI ? 'New UI' : 'Old UI'; + redirectMessage = + `The '${friendly}' view is only available in the ${sourceUiName}. We've returned you to the ${fallbackViewName} dashboard.`; + } + } + + this.setState({ enableOldUI }, () => { + sessionStorage.setItem('enableOldUI', JSON.stringify(enableOldUI)); + if (redirectMessage) { + message.info(redirectMessage, TOAST_DURATION_SECONDS); + } + }); + }; + render() { const { collapsed, enableOldUI } = this.state; const layoutClass = classNames('content-layout', { 'sidebar-collapsed': collapsed }); @@ -80,16 +124,7 @@ class App extends React.Component, IAppState> { unCheckedChildren={
Old UI
} checkedChildren={
New UI
} checked={this.state.enableOldUI} - onChange={(checked: boolean) => { - this.setState({ - enableOldUI: checked - }, () => { - // This is to persist the state of the UI between refreshes. - // While using session storage to store state is an anti-pattern, provided the size of the data stored in this case - // and the plan to deprecate UI v1 (old UI) in the future - this is the simplest approach/fix for persisting state. - sessionStorage.setItem('enableOldUI', JSON.stringify(checked)); - }); - }} /> + onChange={this.handleUIToggle} /> @@ -97,7 +132,7 @@ class App extends React.Component, IAppState> { }> - + {(enableOldUI) ? routes.map( @@ -121,4 +156,4 @@ class App extends React.Component, IAppState> { } } -export default App; \ No newline at end of file +export default App; diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/breadcrumbs.constants.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/breadcrumbs.constants.tsx index 69e904f3df7b..18ebb02d93b7 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/breadcrumbs.constants.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/breadcrumbs.constants.tsx @@ -30,5 +30,6 @@ export const breadcrumbNameMap: BreadcrumbNameMap = { '/Insights': 'Insights', '/NamespaceUsage': 'Namespace Usage', '/Heatmap': 'Heatmap', - '/Om': 'OM DB Insights' + '/Om': 'OM DB Insights', + '/Capacity': 'Cluster Capacity' }; From 5fbf6b3b202ce65227ade7fef59b0db8de3d9946 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 1 May 2026 21:28:12 +0800 Subject: [PATCH 2/2] HDDS-15148. Restore original sessionStorage anti-pattern rationale comment --- .../main/resources/webapps/recon/ozone-recon-web/src/app.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.tsx index 63837fa749e8..7377dba31fe8 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.tsx @@ -94,6 +94,9 @@ class App extends React.Component, IAppState> { } this.setState({ enableOldUI }, () => { + // This is to persist the state of the UI between refreshes. + // While using session storage to store state is an anti-pattern, provided the size of the data stored in this case + // and the plan to deprecate UI v1 (old UI) in the future - this is the simplest approach/fix for persisting state. sessionStorage.setItem('enableOldUI', JSON.stringify(enableOldUI)); if (redirectMessage) { message.info(redirectMessage, TOAST_DURATION_SECONDS);