Skip to content

CONSOLE-4439: Upgrade to React Router 7#14957

Open
vojtechszocs wants to merge 4 commits intoopenshift:mainfrom
vojtechszocs:drop-react-router-v5
Open

CONSOLE-4439: Upgrade to React Router 7#14957
vojtechszocs wants to merge 4 commits intoopenshift:mainfrom
vojtechszocs:drop-react-router-v5

Conversation

@vojtechszocs
Copy link
Contributor

@vojtechszocs vojtechszocs commented Apr 10, 2025

Work in progress 🚧

Summary by CodeRabbit

  • New Features

    • Upgraded to React Router v6 with improved routing capabilities and performance.
  • Updates

    • Updated routing dependencies (history package) to compatible versions.
    • Maintained backwards compatibility during migration to React Router v6.

@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Apr 10, 2025
@openshift-merge-robot openshift-merge-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Apr 10, 2025
@vojtechszocs vojtechszocs marked this pull request as draft April 10, 2025 16:01
@openshift-ci openshift-ci bot added component/core Related to console core functionality component/dashboard Related to dashboard approved Indicates a PR has been approved by an approver from all required OWNERS files. component/dev-console Related to dev-console component/gitops Related to gitops-plugin component/helm Related to helm-plugin component/knative Related to knative-plugin component/metal3 Related to metal3-plugin component/monitoring Related to monitoring component/olm Related to OLM component/pipelines Related to pipelines-plugin component/sdk Related to console-plugin-sdk component/shared Related to console-shared component/topology Related to topology plugin-api-changed Categorizes a PR as containing plugin API changes labels Apr 10, 2025
@logonoff
Copy link
Member

/label tide/merge-method-squash

@openshift-ci openshift-ci bot added the tide/merge-method-squash Denotes a PR that should be squashed by tide when it merges. label Apr 11, 2025
@openshift-merge-robot openshift-merge-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Apr 11, 2025
@logonoff
Copy link
Member

/retest NO-JIRA: Drop support of React Router 5 (wip)
/retest

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Apr 11, 2025

@logonoff: The /retest command does not accept any targets.
The following commands are available to trigger required jobs:

/test analyze
/test backend
/test e2e-gcp-console
/test frontend
/test images
/test okd-scos-images

The following commands are available to trigger optional jobs:

/test okd-scos-e2e-aws-ovn

Use /test all to run all jobs.

Details

In response to this:

/retest NO-JIRA: Drop support of React Router 5 (wip)
/retest

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@logonoff
Copy link
Member

/test all

@logonoff logonoff force-pushed the drop-react-router-v5 branch from 7827ca9 to b53853f Compare April 11, 2025 15:33
@openshift-ci openshift-ci bot removed the lifecycle/rotten Denotes an issue or PR that has aged beyond stale and will be auto-closed. label Jan 15, 2026
@logonoff logonoff force-pushed the drop-react-router-v5 branch from 1d70080 to ac55799 Compare February 25, 2026 15:04
@openshift-merge-robot openshift-merge-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Feb 25, 2026
@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Feb 25, 2026

@vojtechszocs: This pull request references CONSOLE-4439 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Work in progress 🚧

Summary by CodeRabbit

  • New Features

  • Upgraded to React Router v6 with improved routing capabilities and performance.

  • Updates

  • Updated routing dependencies (history package) to compatible versions.

  • Maintained backwards compatibility during migration to React Router v6.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci openshift-ci bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Feb 25, 2026
@logonoff logonoff force-pushed the drop-react-router-v5 branch from ac55799 to 63e9560 Compare February 25, 2026 15:11
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/public/components/utils/router.ts (1)

100-100: ⚠️ Potential issue | 🟡 Minor

Trailing ? when all query params are removed.

The URL template `${loc.pathname}?${updated.toString()}${loc.hash}` will produce a trailing ? (e.g., /foo?#hash or /foo?) when updated is empty. This is a cosmetic issue but can cause URL normalization mismatches in tests or analytics.

Suggested fix
-        navigate(`${loc.pathname}?${updated.toString()}${loc.hash}`, {
+        const search = updated.toString();
+        navigate(`${loc.pathname}${search ? `?${search}` : ''}${loc.hash}`, {

Apply the same pattern to lines 121, 143, 157, and 178.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/public/components/utils/router.ts` at line 100, The navigate URL
template currently always injects a "?" even when updated.toString() is empty,
producing trailing question marks; change each navigate call (the one at the
shown line and the other calls at lines 121, 143, 157, 178) to build the URL
conditionally: compute const q = updated.toString(); then use const url = q ?
`${loc.pathname}?${q}${loc.hash}` : `${loc.pathname}${loc.hash}` and pass url to
navigate(...). Replace the inline
`${loc.pathname}?${updated.toString()}${loc.hash}` usage in those navigate calls
with this conditional URL construction.
frontend/packages/console-dynamic-plugin-sdk/src/extensions/pages.ts (1)

73-82: ⚠️ Potential issue | 🟡 Minor

Stale React Router v5 reference in StandaloneRoutePage doc.

The StandaloneRoutePage JSDoc (lines 76-77) still says "Under the hood we use React Router. See https://v5.reactrouter.com/". Since this PR drops v5 support, this should be updated to reference v6 (consistent with the RoutePage doc at line 30 which correctly links to v6).

📝 Proposed fix
 /**
  * Adds new standalone page (rendered outside the common page layout) to Console router.
  *
- * Under the hood we use React Router.
- * See https://v5.reactrouter.com/
+ * Under the hood we use [React Router v6](https://reactrouter.com/docs/en/v6).
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/packages/console-dynamic-plugin-sdk/src/extensions/pages.ts` around
lines 73 - 82, The JSDoc for StandaloneRoutePage still references React Router
v5; update its comment to reference React Router v6 to match RoutePage's doc.
Edit the comment block above the StandaloneRoutePage type (the Extension
declaration using 'console.page/route/standalone' and Omit<RoutePageProperties,
'perspective'>) and replace the v5 URL/text with the v6 documentation URL and
wording consistent with RoutePage's doc.
♻️ Duplicate comments (1)
frontend/packages/console-dynamic-plugin-sdk/docs/console-extensions.md (1)

819-832: Stale v5 link in console.page/route/standalone summary — downstream of source file.

Line 823 still reads "See https://v5.reactrouter.com/" which is inconsistent with the v6 migration. This appears to be auto-generated from the StandaloneRoutePage JSDoc in pages.ts — fixing the source (as noted in that file's review) should resolve this on the next doc generation pass.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/packages/console-dynamic-plugin-sdk/docs/console-extensions.md`
around lines 819 - 832, Update the JSDoc for the StandaloneRoutePage
(StandaloneRoutePage or StandaloneRoutePageProps) in pages.ts to reference React
Router v6 instead of the v5 URL shown in generated docs; modify the JSDoc
summary or See link text to point to the v6 docs (https://reactrouter.com/) or
remove the versioned URL so the generated docs for console.page/route/standalone
no longer include "See https://v5.reactrouter.com/". Ensure the JSDoc change is
in the StandaloneRoutePage declaration so the next docs generation picks up the
corrected link.
🧹 Nitpick comments (5)
frontend/packages/console-dynamic-plugin-sdk/release-notes/4.22.md (1)

15-16: Add a forward migration pointer for "deprecated" users.

The note tells plugin authors to "continue to use this deprecated shared module as is" but offers no guidance on what the eventual migration target is. The intent was always to change all react-router-dom-v5-compat imports to react-router-dom once the app is fully on v6. Pointing to either the official React Router v6 migration guide or a Console-specific migration document would make this release note actionable, especially given the plugin-api-changed label on the PR.

✏️ Suggested addition
  compatible with the previous version of `react-router-dom-v5-compat`. Plugins that need to support
  versions of OpenShift starting from `4.14.x` can continue to use this deprecated shared module as is.
+ To migrate off this deprecated module, replace all `react-router-dom-v5-compat` imports with
+ `react-router-dom` and refer to the [React Router v6 upgrade guide](https://reactrouter.com/6.30.0/upgrading/v5).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/packages/console-dynamic-plugin-sdk/release-notes/4.22.md` around
lines 15 - 16, The release note tells authors to keep using the deprecated
shared module but lacks guidance on migrating; update the text in
release-notes/4.22.md to include a forward migration pointer instructing plugin
authors to replace imports of react-router-dom-v5-compat with react-router-dom
once the app is fully on v6, and link to the React Router v6 migration guide (or
a Console-specific migration doc) for step-by-step instructions and mention the
plugin-api-changed context so consumers know this is the intended long-term
change.
frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-shared-modules.ts (1)

34-37: Clean implementation. Consider a defensive guard for robustness.

The aliasing logic is correct and the JSDoc is excellent. One minor thought: if scope['react-router-dom'] were ever missing (e.g., due to a future refactor removing it from shared modules), this would silently set the compat entry to undefined, which would surface as a confusing runtime error in plugins.

A lightweight guard could help future-proof this:

Optional defensive check
 export const patchSharedScope = () => {
   const scope = getSharedScope();
+  if (!scope['react-router-dom']) {
+    // eslint-disable-next-line no-console
+    console.warn('react-router-dom not found in shared scope; skipping v5-compat patch');
+    return;
+  }
   scope['react-router-dom-v5-compat'] = scope['react-router-dom'];
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-shared-modules.ts`
around lines 34 - 37, In patchSharedScope(), guard the alias assignment so you
only set scope['react-router-dom-v5-compat'] when scope['react-router-dom']
exists: retrieve the scope via getSharedScope(), check for the presence
(non-undefined) of the 'react-router-dom' entry and only then assign it to
'react-router-dom-v5-compat'; if missing, skip the alias (optionally log or
leave a TODO comment) to avoid creating an undefined compat binding.
frontend/public/components/utils/router.ts (1)

89-107: Setting an empty string deletes the parameter — potentially surprising.

Lines 94-95: when v === '' is passed to setQueryArgument, the key is deleted rather than set to an empty string. This is an implicit contract that differs from URLSearchParams.set(k, '') behavior. If any caller genuinely wants to set a key to "" (e.g., to clear a filter value while keeping the parameter present), this will silently remove it instead.

If this is intentional, a brief inline comment would help. If not:

Suggested clarification
       if (v === '') {
+        // Intentionally remove the param rather than setting it to empty string
         updated.delete(k);
       } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/public/components/utils/router.ts` around lines 89 - 107, The
setQueryArgument callback currently deletes the key when v === '' which diverges
from URLSearchParams.set(k, '') semantics; update setQueryArgument (the function
that builds updated = new URLSearchParams(searchParamsRef.current)) to always
call updated.set(k, v) even for empty strings (i.e., remove the if (v === '') {
updated.delete(k) } branch) so callers can preserve an empty-valued param, and
add a brief inline comment above setQueryArgument explaining that empty string
values are intentionally preserved (or, if deletion was intended, instead
document that behavior and use a distinct sentinel like null/undefined to mean
“remove”).
frontend/packages/console-app/src/hooks/usePluginRoutes.tsx (1)

60-81: Well-designed helper — one subtle concern about exact semantics inversion.

The implementation is clean and correctly handles the array/string normalization and /* suffix logic. A few observations:

  1. exact default is inverted from v6 native behavior: When exact is undefined/false, you append /* (non-exact matching). This is the opposite of React Router v6's native behavior (exact by default). The JSDoc + extension docs cover this, but consider adding a brief inline comment at line 77 to prevent future contributors from being confused by the inversion.

  2. getElement parameter currentPath is unused by all current callers (both here at line 96 and in app.tsx). If no caller needs it now, you could simplify the signature to getElement: () => RouteProps['element']. But keeping it is reasonable for future extensibility.

💡 Optional inline comment for clarity
   return routePaths.map((currentPath) => (
     <Route
       key={`${uid}/${currentPath}`}
+      // Note: Unlike React Router v6 defaults (exact match), Console retains v5 behavior
+      // where routes are non-exact unless explicitly opted in via `exact: true`.
       path={`${currentPath}${exact ? '' : '/*'}`}
       element={getElement(currentPath)}
     />
   ));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/packages/console-app/src/hooks/usePluginRoutes.tsx` around lines 60
- 81, Add a short inline comment inside mapExtensionToRoutes explaining that the
exact parameter is intentionally inverted relative to React Router v6 (i.e.,
when exact is falsy we append '/*' to allow nested matching because v6 routes
are exact by default), so future contributors won’t be confused; locate the
comment near where path is suffixed (`path={`${currentPath}${exact ? '' :
'/*'}`}`) and keep the current logic, and optionally consider (but do not change
now) simplifying the getElement signature from (currentPath: string) =>
RouteProps['element'] to () => RouteProps['element'] if no callers need the
parameter.
frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts (1)

83-83: VIRTUAL_DEPS spread last gives it override precedence over actual root package deps.

Since VIRTUAL_DEPS is intended to satisfy modules not installed in the root package, it should act as a fallback, not a trump card. As written, if react-router-dom-v5-compat is later added to rootPackage.json, the virtual entry silently wins with a potentially stale/different version.

♻️ Proposed fix — spread VIRTUAL_DEPS first
-  const srcDeps = { ...pkg.devDependencies, ...pkg.dependencies, ...VIRTUAL_DEPS };
+  const srcDeps = { ...VIRTUAL_DEPS, ...pkg.devDependencies, ...pkg.dependencies };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts`
at line 83, The spread ordering gives VIRTUAL_DEPS higher precedence than real
deps; change the srcDeps construction so VIRTUAL_DEPS are spread first and
pkg.devDependencies and pkg.dependencies come after (i.e., build srcDeps from
VIRTUAL_DEPS, then ...pkg.devDependencies, then ...pkg.dependencies) so actual
root package entries override the virtual fallbacks; update the srcDeps
assignment that references VIRTUAL_DEPS, pkg.devDependencies, and
pkg.dependencies accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/package.json`:
- Around line 220-222: Remove the incompatible dependency
"react-router-hash-link" from package.json and either implement manual hash
scrolling or swap to a v6-compatible package: if implementing manually, use
React Router's useLocation() in your layout or a scroll handler (e.g., in App or
a ScrollManager component) to detect location.hash and call
document.getElementById(...).scrollIntoView({ behavior: 'smooth' }) (or
element.scrollIntoView) after navigation; if replacing, remove
"react-router-hash-link" and add a v6-compatible alternative such as
"scroll-to-hash-element" and update any Link usage to the new API. Ensure you
update package.json dependencies and any imports/usages referencing
react-router-hash-link.

In `@frontend/packages/console-app/src/hooks/usePluginRoutes.tsx`:
- Around line 87-112: The memo in usePluginRoutes is being invalidated by an
unstable setActivePerspective reference—wrap the setPerspective function created
in useValuesForPerspectiveContext (used by DetectPerspective) in useCallback to
provide referential stability; specifically, wrap setPerspective with
useCallback and use [navigate, fireTelemetryEvent, setLastPerspective] as its
dependency array so the setActivePerspective value passed into usePluginRoutes
remains stable and the useMemo in usePluginRoutes can actually memoize the
routePages reduce.

In `@frontend/packages/console-dynamic-plugin-sdk/release-notes/4.22.md`:
- Around line 14-16: The release note claims backward compatibility but
patchSharedScope in plugin-shared-modules.ts (the line setting
scope['react-router-dom-v5-compat'] = scope['react-router-dom']) only aliases
the module and does not re-export CompatRouter or CompatRoute, so plugins
importing CompatRouter/CompatRoute will get undefined; update the 4.22 release
note text to explicitly state that the shared module exposes only standard React
Router v6 APIs and add the sentence: "Plugins must migrate away from
CompatRouter and CompatRoute before upgrading to 4.22, as only standard React
Router v6 APIs are available in the shared module."

In `@frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts`:
- Around line 74-76: The VIRTUAL_DEPS entry uses an npm install-time alias
('npm:react-router-dom@^6.30.x') which is not a valid SemVer range and will end
up in peerDependencies via parseSharedModuleDeps; change the value for
'react-router-dom-v5-compat' in VIRTUAL_DEPS to a plain SemVer range (e.g.
'^6.30.x') so parseSharedModuleDeps emits a valid peerDependency, and if you
still need the install-time aliasing, implement that at install/alias resolution
time (not inside VIRTUAL_DEPS) or map the virtual name to the actual package
name elsewhere in the resolver that synthesizes install aliases.

In `@frontend/public/components/app.tsx`:
- Around line 19-21: Consolidate the mixed routing imports by importing all
router symbols from the same package: move the unstable_HistoryRouter
(UnstableHistoryRouter) import so it's grouped with useParams, useLocation,
Routes, Route (or vice versa) instead of importing from both 'react-router-dom'
and 'react-router-dom-v5-compat'; update the import statement(s) so that
UnstableHistoryRouter, useParams, useLocation, Routes, and Route come from a
single source (either 'react-router-dom' or 'react-router-dom-v5-compat') and
keep mapExtensionToRoutes import unchanged.

In `@frontend/public/components/factory/modal.tsx`:
- Around line 74-80: The modal rendering path mounts components outside the app
router so any component using router hooks (e.g., useNavigate/useLocation)
fails; update the render in ModalWrapper/Component branch to wrap the output
with the app's HistoryRouter (the UnstableHistoryRouter or HistoryRouter
instance used in app.tsx) using the shared history instance so router context is
available to launched modals; specifically import the UnstableHistoryRouter (or
HistoryRouter) and the shared history that app.tsx initializes, and wrap the JSX
returned for both the ModalWrapper and direct Component render inside that
router (preserving blocking, className, onClose, cancel, close props) so
createModal-mounted components like DeleteModal and ClusterUpdateModal receive
router context.

In `@frontend/public/components/utils/router.ts`:
- Around line 44-48: The monkey-patched overrides for
history.replace/history.push (history.__replace__/history.__push and the new
history.replace/history.push) drop the optional state parameter and mishandle
non-string locations; update the patched signatures to match the original (to:
To, state?: any) and preserve state when delegating to the originals; inside the
new functions, detect if to is a string and call removeBasePath(to) before
passing it through, otherwise pass the location object unchanged, and forward
the optional state argument to (history as any).__replace__ / __push__; also
update typings/JSDoc for history.replace and history.push to reflect the
original signature.

---

Outside diff comments:
In `@frontend/packages/console-dynamic-plugin-sdk/src/extensions/pages.ts`:
- Around line 73-82: The JSDoc for StandaloneRoutePage still references React
Router v5; update its comment to reference React Router v6 to match RoutePage's
doc. Edit the comment block above the StandaloneRoutePage type (the Extension
declaration using 'console.page/route/standalone' and Omit<RoutePageProperties,
'perspective'>) and replace the v5 URL/text with the v6 documentation URL and
wording consistent with RoutePage's doc.

In `@frontend/public/components/utils/router.ts`:
- Line 100: The navigate URL template currently always injects a "?" even when
updated.toString() is empty, producing trailing question marks; change each
navigate call (the one at the shown line and the other calls at lines 121, 143,
157, 178) to build the URL conditionally: compute const q = updated.toString();
then use const url = q ? `${loc.pathname}?${q}${loc.hash}` :
`${loc.pathname}${loc.hash}` and pass url to navigate(...). Replace the inline
`${loc.pathname}?${updated.toString()}${loc.hash}` usage in those navigate calls
with this conditional URL construction.

---

Duplicate comments:
In `@frontend/packages/console-dynamic-plugin-sdk/docs/console-extensions.md`:
- Around line 819-832: Update the JSDoc for the StandaloneRoutePage
(StandaloneRoutePage or StandaloneRoutePageProps) in pages.ts to reference React
Router v6 instead of the v5 URL shown in generated docs; modify the JSDoc
summary or See link text to point to the v6 docs (https://reactrouter.com/) or
remove the versioned URL so the generated docs for console.page/route/standalone
no longer include "See https://v5.reactrouter.com/". Ensure the JSDoc change is
in the StandaloneRoutePage declaration so the next docs generation picks up the
corrected link.

---

Nitpick comments:
In `@frontend/packages/console-app/src/hooks/usePluginRoutes.tsx`:
- Around line 60-81: Add a short inline comment inside mapExtensionToRoutes
explaining that the exact parameter is intentionally inverted relative to React
Router v6 (i.e., when exact is falsy we append '/*' to allow nested matching
because v6 routes are exact by default), so future contributors won’t be
confused; locate the comment near where path is suffixed
(`path={`${currentPath}${exact ? '' : '/*'}`}`) and keep the current logic, and
optionally consider (but do not change now) simplifying the getElement signature
from (currentPath: string) => RouteProps['element'] to () =>
RouteProps['element'] if no callers need the parameter.

In `@frontend/packages/console-dynamic-plugin-sdk/release-notes/4.22.md`:
- Around line 15-16: The release note tells authors to keep using the deprecated
shared module but lacks guidance on migrating; update the text in
release-notes/4.22.md to include a forward migration pointer instructing plugin
authors to replace imports of react-router-dom-v5-compat with react-router-dom
once the app is fully on v6, and link to the React Router v6 migration guide (or
a Console-specific migration doc) for step-by-step instructions and mention the
plugin-api-changed context so consumers know this is the intended long-term
change.

In `@frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts`:
- Line 83: The spread ordering gives VIRTUAL_DEPS higher precedence than real
deps; change the srcDeps construction so VIRTUAL_DEPS are spread first and
pkg.devDependencies and pkg.dependencies come after (i.e., build srcDeps from
VIRTUAL_DEPS, then ...pkg.devDependencies, then ...pkg.dependencies) so actual
root package entries override the virtual fallbacks; update the srcDeps
assignment that references VIRTUAL_DEPS, pkg.devDependencies, and
pkg.dependencies accordingly.

In
`@frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-shared-modules.ts`:
- Around line 34-37: In patchSharedScope(), guard the alias assignment so you
only set scope['react-router-dom-v5-compat'] when scope['react-router-dom']
exists: retrieve the scope via getSharedScope(), check for the presence
(non-undefined) of the 'react-router-dom' entry and only then assign it to
'react-router-dom-v5-compat'; if missing, skip the alias (optionally log or
leave a TODO comment) to avoid creating an undefined compat binding.

In `@frontend/public/components/utils/router.ts`:
- Around line 89-107: The setQueryArgument callback currently deletes the key
when v === '' which diverges from URLSearchParams.set(k, '') semantics; update
setQueryArgument (the function that builds updated = new
URLSearchParams(searchParamsRef.current)) to always call updated.set(k, v) even
for empty strings (i.e., remove the if (v === '') { updated.delete(k) } branch)
so callers can preserve an empty-valued param, and add a brief inline comment
above setQueryArgument explaining that empty string values are intentionally
preserved (or, if deletion was intended, instead document that behavior and use
a distinct sentinel like null/undefined to mean “remove”).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between b5c456a and ac55799.

⛔ Files ignored due to path filters (1)
  • frontend/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (16)
  • frontend/.eslintrc.js
  • frontend/package.json
  • frontend/packages/console-app/src/hooks/usePluginRoutes.tsx
  • frontend/packages/console-dynamic-plugin-sdk/docs/console-extensions.md
  • frontend/packages/console-dynamic-plugin-sdk/release-notes/4.22.md
  • frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts
  • frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts
  • frontend/packages/console-dynamic-plugin-sdk/src/extensions/pages.ts
  • frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-init.ts
  • frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-shared-modules.ts
  • frontend/public/components/app.tsx
  • frontend/public/components/factory/modal.tsx
  • frontend/public/components/modals/delete-modal.tsx
  • frontend/public/components/utils/router.ts
  • frontend/tsconfig.json
  • frontend/webpack.config.ts

Comment on lines 220 to 222
"react-router": "6.29.x",
"react-router-dom": "6.29.x",
"react-router-hash-link": "^2.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

react-router-hash-link React Router v6 compatibility

💡 Result:

react-router-hash-link (rafgraph, latest 2.4.3, published May 13, 2021) was built for React Router v4/v5 and hasn’t been updated for React Router v6+; its own package metadata still describes it as “Hash link scroll functionality for React Router v4/5”. [1][2]

In practice, many people hit broken/quirky behavior after upgrading to react-router-dom v6 (hash updates but no scroll / inconsistent scroll), and common guidance is that it “doesn’t appear to be compatible with react-router v6”. [3][4]

What to do on React Router v6:

  • Implement hash scrolling yourself (listen to location.hash from useLocation() and call element.scrollIntoView()), or
  • Use a v6-friendly alternative (e.g., the “scroll-to-hash-element” approach/package referenced in the v6 hash-links writeup). [3]

Sources:
[1] https://app.unpkg.com/react-router-hash-link%402.3.0/files/package.json
[2] https://classic.yarnpkg.com/en/package/react-router-hash-link
[3] https://ncoughlin.com/posts/react-router-v6-hash-links/
[4] https://stackoverflow.com/questions/70441115/smooth-scroll-not-working-with-react-router-hash-link


Remove or replace react-router-hash-link — incompatible with React Router v6.

react-router-hash-link ^2.0.0 (line 222) is incompatible with React Router v6. The library was built exclusively for v4/v5, and its maintainer has not updated it for v6. In practice, this causes hash scrolling to break or behave inconsistently at runtime.

Options:

  • Remove the dependency and implement hash scrolling manually using useLocation() and element.scrollIntoView(), or
  • Replace with a v6-compatible alternative (e.g., scroll-to-hash-element).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/package.json` around lines 220 - 222, Remove the incompatible
dependency "react-router-hash-link" from package.json and either implement
manual hash scrolling or swap to a v6-compatible package: if implementing
manually, use React Router's useLocation() in your layout or a scroll handler
(e.g., in App or a ScrollManager component) to detect location.hash and call
document.getElementById(...).scrollIntoView({ behavior: 'smooth' }) (or
element.scrollIntoView) after navigation; if replacing, remove
"react-router-hash-link" and add a v6-compatible alternative such as
"scroll-to-hash-element" and update any Link usage to the new API. Ensure you
update package.json dependencies and any imports/usages referencing
react-router-hash-link.

Comment on lines 87 to 112
return useMemo(
() =>
routePages.reduce(
([activeAcc, inactiveAcc], extension) => {
const active = isRoutePageExtensionActive(extension, activePerspective);
const routes = getRoutesForExtension(extension);
const routes = mapExtensionToRoutes({
uid: extension.uid,
path: extension.properties.path,
exact: extension.properties.exact,
getElement: () => (
<RoutePage
extension={extension}
activePerspective={activePerspective}
setActivePerspective={setActivePerspective}
/>
),
});

return active
? [[...activeAcc, ...routes], inactiveAcc]
: [activeAcc, [...inactiveAcc, ...routes]];
},
[[], []],
),
[routePages, getRoutesForExtension, activePerspective],
[routePages, activePerspective, setActivePerspective],
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for useActivePerspective hook definition
rg -n "useActivePerspective|export.*useActivePerspective" --type=ts --type=tsx -g '!node_modules/**' | head -40

Repository: openshift/console

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Get the full useActivePerspective hook implementation
rg -n "const useActivePerspective|function useActivePerspective" --type=ts --type=tsx -A 30 -g '!node_modules/**'

Repository: openshift/console

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Search for useActivePerspective using pattern without type flags
rg "useActivePerspective" -g '!node_modules/**' -A 30 | head -100

Repository: openshift/console

Length of output: 11108


🏁 Script executed:

#!/bin/bash
# Find files containing useActivePerspective
fd "\.tsx?$" -g '!node_modules/**' -g '!dist/**' -x rg -l "useActivePerspective" {} + 2>/dev/null | head -20

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Find the dynamic-plugin-sdk package
fd -t d "dynamic-plugin-sdk" -g '!node_modules/**' 2>/dev/null

Repository: openshift/console

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Search for useActivePerspective definition in packages
find frontend/packages -name "*.ts" -o -name "*.tsx" | xargs rg -l "export.*useActivePerspective|const useActivePerspective" 2>/dev/null | head -10

Repository: openshift/console

Length of output: 360


🏁 Script executed:

#!/bin/bash
# Read the useActivePerspective implementation
cat -n frontend/packages/console-dynamic-plugin-sdk/src/perspective/useActivePerspective.ts

Repository: openshift/console

Length of output: 647


🏁 Script executed:

#!/bin/bash
# Check the index.ts export
cat -n frontend/packages/console-dynamic-plugin-sdk/src/perspective/index.ts

Repository: openshift/console

Length of output: 187


🏁 Script executed:

#!/bin/bash
# Find the perspective-context file
fd "perspective-context" frontend/packages/console-dynamic-plugin-sdk/

Repository: openshift/console

Length of output: 144


🏁 Script executed:

#!/bin/bash
# Read perspective-context implementation
cat -n frontend/packages/console-dynamic-plugin-sdk/src/perspective/perspective-context.ts

Repository: openshift/console

Length of output: 785


🏁 Script executed:

#!/bin/bash
# Find where PerspectiveContext.Provider is used
rg "PerspectiveContext\\.Provider|<PerspectiveContext" -A 5 -g '!node_modules/**' | head -80

Repository: openshift/console

Length of output: 2259


🏁 Script executed:

#!/bin/bash
# Also search for where the context value is created
rg "activePerspective.*setActivePerspective|setActivePerspective.*activePerspective" -B 3 -A 3 -g '!node_modules/**' | head -80

Repository: openshift/console

Length of output: 8312


🏁 Script executed:

#!/bin/bash
# Read the full useValuesForPerspectiveContext implementation
cat -n frontend/packages/console-app/src/components/detect-perspective/useValuesForPerspectiveContext.ts

Repository: openshift/console

Length of output: 2416


🏁 Script executed:

#!/bin/bash
# Confirm the usePluginRoutes useMemo dependency array one more time
sed -n '87,112p' frontend/packages/console-app/src/hooks/usePluginRoutes.tsx

Repository: openshift/console

Length of output: 912


setActivePerspective in the memoization dependency array breaks optimization — lacks referential stability.

The setPerspective function returned by useValuesForPerspectiveContext (in DetectPerspective) is created inline without useCallback, generating a new reference on every render. This unstable reference flows through context → useActivePerspective()usePluginRoutes, invalidating the useMemo on every render and defeating the memoization of the route reduce operation.

Wrap setPerspective in useCallback within useValuesForPerspectiveContext with dependencies [navigate, fireTelemetryEvent, setLastPerspective] to stabilize the reference across renders.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/packages/console-app/src/hooks/usePluginRoutes.tsx` around lines 87
- 112, The memo in usePluginRoutes is being invalidated by an unstable
setActivePerspective reference—wrap the setPerspective function created in
useValuesForPerspectiveContext (used by DetectPerspective) in useCallback to
provide referential stability; specifically, wrap setPerspective with
useCallback and use [navigate, fireTelemetryEvent, setLastPerspective] as its
dependency array so the setActivePerspective value passed into usePluginRoutes
remains stable and the useMemo in usePluginRoutes can actually memoize the
routePages reduce.

Comment on lines +14 to +16
- Upgraded `react-router-dom-v5-compat` to version `npm:react-router-dom@^6.30.x`. This change is API-
compatible with the previous version of `react-router-dom-v5-compat`. Plugins that need to support
versions of OpenShift starting from `4.14.x` can continue to use this deprecated shared module as is.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for CompatRouter, CompatRoute, and patchSharedScope in the codebase
echo "=== Searching for CompatRouter, CompatRoute, patchSharedScope ==="
rg -rn 'CompatRouter|CompatRoute|patchSharedScope' --type ts --type tsx -g '!*.test.*' -g '!node_modules'

Repository: openshift/console

Length of output: 171


🏁 Script executed:

#!/bin/bash
# Look for the react-router-dom-v5-compat alias/patch configuration
echo "=== Searching for react-router-dom alias/patch configuration ==="
rg -rn 'react-router-dom' --type ts --type tsx --type json -g '!node_modules' -g '!dist' | head -50

Repository: openshift/console

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Check the release notes file to understand the full context
echo "=== Checking release notes file ==="
cat -n frontend/packages/console-dynamic-plugin-sdk/release-notes/4.22.md

Repository: openshift/console

Length of output: 3538


🏁 Script executed:

#!/bin/bash
# Search for CompatRouter, CompatRoute, patchSharedScope without type restriction
echo "=== Searching for CompatRouter, CompatRoute, patchSharedScope ==="
rg -rn 'CompatRouter|CompatRoute|patchSharedScope' --max-count=50

Repository: openshift/console

Length of output: 393


🏁 Script executed:

#!/bin/bash
# Search for react-router-dom references in config files
echo "=== Searching for react-router-dom in config/webpack ==="
rg -rn 'react-router-dom' -t json -t js | head -80

Repository: openshift/console

Length of output: 921


🏁 Script executed:

#!/bin/bash
# Look for the actual plugin SDK package structure
echo "=== Listing console-dynamic-plugin-sdk directory ==="
find frontend/packages/console-dynamic-plugin-sdk -type f -name '*.ts' -o -name '*.js' -o -name '*.json' | grep -E '(webpack|tsconfig|package\.json|shared)' | head -20

Repository: openshift/console

Length of output: 1106


🏁 Script executed:

#!/bin/bash
# Check for webpack configuration and aliases
echo "=== Searching for webpack config and aliases ==="
find . -name 'webpack*.js' -o -name 'webpack*.config.*' | head -10 | xargs ls -lah

Repository: openshift/console

Length of output: 276


🏁 Script executed:

#!/bin/bash
# Read the plugin-shared-modules.ts file to see what's exported
echo "=== plugin-shared-modules.ts ==="
cat -n frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-shared-modules.ts

Repository: openshift/console

Length of output: 1677


🏁 Script executed:

#!/bin/bash
# Check the tsconfig.json for the actual path aliases
echo "=== frontend/tsconfig.json path aliases ==="
cat frontend/tsconfig.json | grep -A 20 '"paths"'

Repository: openshift/console

Length of output: 1541


🏁 Script executed:

#!/bin/bash
# Search for "react-router-dom-v5-compat" (full name, not abbreviated)
echo "=== Searching for react-router-dom-v5-compat (full name) ==="
rg -rn 'react-router-dom-v5-compat' --max-count=50

Repository: openshift/console

Length of output: 42006


🏁 Script executed:

#!/bin/bash
# Check the package.json files for the actual module configuration
echo "=== frontend/package.json react/router entries ==="
cat frontend/package.json | grep -A 5 -B 5 'react-router'

Repository: openshift/console

Length of output: 516


Release note is misleading on backward compatibility—clarify that only standard React Router v6 APIs are supported.

The patchSharedScope implementation (in plugin-shared-modules.ts:36) only aliases the module reference: scope['react-router-dom-v5-compat'] = scope['react-router-dom']. It does not re-export CompatRouter or CompatRoute. Since vanilla react-router-dom@^6.29.x does not export these symbols, plugins importing them will receive undefined at runtime—a silent breaking change that contradicts "continue to use this deprecated shared module as is."

Update the note to explicitly state: "Plugins must migrate away from CompatRouter and CompatRoute before upgrading to 4.22, as only standard React Router v6 APIs are available in the shared module."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/packages/console-dynamic-plugin-sdk/release-notes/4.22.md` around
lines 14 - 16, The release note claims backward compatibility but
patchSharedScope in plugin-shared-modules.ts (the line setting
scope['react-router-dom-v5-compat'] = scope['react-router-dom']) only aliases
the module and does not re-export CompatRouter or CompatRoute, so plugins
importing CompatRouter/CompatRoute will get undefined; update the 4.22 release
note text to explicitly state that the shared module exposes only standard React
Router v6 APIs and add the sentence: "Plugins must migrate away from
CompatRouter and CompatRoute before upgrading to 4.22, as only standard React
Router v6 APIs are available in the shared module."

Comment on lines +74 to +76
const VIRTUAL_DEPS: PackageJson['dependencies'] = {
'react-router-dom-v5-compat': 'npm:react-router-dom@^6.30.x',
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts | head -100

Repository: openshift/console

Length of output: 4043


🏁 Script executed:

rg -n "sharedPluginModules" frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts -A 20 -B 5

Repository: openshift/console

Length of output: 1900


🏁 Script executed:

rg -n "parseSharedModuleDeps\|getMinDepVersion" frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts -B 3 -A 3

Repository: openshift/console

Length of output: 43


🏁 Script executed:

rg -n "VIRTUAL_DEPS\|srcDeps" frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts -B 2 -A 2

Repository: openshift/console

Length of output: 43


🏁 Script executed:

rg -r "react-router-dom-v5-compat" frontend/packages/console-dynamic-plugin-sdk/ --type ts --type json

Repository: openshift/console

Length of output: 831


🏁 Script executed:

cat -n frontend/packages/console-dynamic-plugin-sdk/src/shared-modules/shared-modules-meta.ts | head -150

Repository: openshift/console

Length of output: 2487


🏁 Script executed:

rg -n "getMinDepVersion" frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts -A 2 -B 2

Repository: openshift/console

Length of output: 396


🏁 Script executed:

grep -r "react-router-dom-v5-compat" frontend/packages/console-dynamic-plugin-sdk/src/ --include="*.ts" --include="*.tsx"

Repository: openshift/console

Length of output: 931


🏁 Script executed:

sed -n '115,250p' frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts

Repository: openshift/console

Length of output: 2891


🏁 Script executed:

rg -n "parseSharedModuleDeps\|peerDependencies" frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts -A 2 -B 2

Repository: openshift/console

Length of output: 43


🏁 Script executed:

grep -r "npm:" frontend/packages/console-dynamic-plugin-sdk/package.json

Repository: openshift/console

Length of output: 43


🏁 Script executed:

cat -n package.json | head -150

Repository: openshift/console

Length of output: 105


🏁 Script executed:

ls -la | grep package.json

Repository: openshift/console

Length of output: 43


🏁 Script executed:

fd package.json -d 1

Repository: openshift/console

Length of output: 43


🏁 Script executed:

cat -n frontend/packages/console-dynamic-plugin-sdk/package.json

Repository: openshift/console

Length of output: 1576


🏁 Script executed:

find . -maxdepth 2 -name "package.json" -exec grep -l "react-router-dom-v5-compat" {} \;

Repository: openshift/console

Length of output: 95


🏁 Script executed:

cat frontend/dynamic-demo-plugin/package.json | grep -A 5 -B 5 "react-router-dom-v5-compat"

Repository: openshift/console

Length of output: 134


🏁 Script executed:

rg -n "react-router-dom\|react-router-dom-v5-compat" frontend/package.json -A 2 -B 2

Repository: openshift/console

Length of output: 43


🏁 Script executed:

find . -maxdepth 3 -name "package.json" -type f | head -20

Repository: openshift/console

Length of output: 150


🏁 Script executed:

head -100 ./package.json

Repository: openshift/console

Length of output: 134


🏁 Script executed:

head -150 ./frontend/package.json | grep -A 10 -B 2 "react-router"

Repository: openshift/console

Length of output: 43


🏁 Script executed:

cat ./dynamic-demo-plugin/package.json

Repository: openshift/console

Length of output: 3784


🏁 Script executed:

web_search
npm: protocol peerDependencies semver validation

Repository: openshift/console

Length of output: 154


🌐 Web query:

npm: protocol peerDependencies package.json semver validation

💡 Result:

  • peerDependencies values are meant to be SemVer ranges (compatibility constraints), not “where to fetch it from.” npm documents peerDependencies as expressing compatibility with a host and gives only SemVer-range examples ("2.x", "^1.0", "1.x", ">= 1.5.2 < 2", etc.). (docs.npmjs.com)
  • npm validates versions/ranges using node-semver. The npm docs explicitly say the version field must be parseable by node-semver (the same library family used for range parsing/validation throughout npm). (docs.npmjs.com)
  • Protocol specifiers (eg file:, git+https:, http(s):, npm:alias) are “package specifiers” that npm parses for installable dependencies, via npm-package-arg. That parser recognizes types like range, version, tag, file, directory, git, remote, alias, and throws on unsupported protocols. (npmjs.com)
  • So if you put a protocol in peerDependencies, you’re outside the intended model (a peer is provided by the consumer, and npm needs a range to check satisfaction). Practically, this often shows up as parse errors like “Unsupported URL Type …” for protocols npm doesn’t support (notably workspace:). (github.com)
  • Behavior note: since npm v7, peer deps are installed by default and conflicts can become hard install errors (vs warnings in npm v3–v6). (docs.npmjs.com)

How to validate in tooling (what npm effectively does):

  • Parse the spec like npm would: npm-package-arg (rejects unsupported protocols). (npmjs.com)
  • If it’s a peer dep, additionally require it to be a SemVer range/version (type === 'range' || type === 'version') and validate with semver.validRange(...) / semver.valid(...) (from node-semver, referenced by npm docs). (docs.npmjs.com)

Citations:


npm: protocol is not a valid SemVer range—will break generated peerDependencies.

The version string 'npm:react-router-dom@^6.30.x' is the npm install-time alias syntax, not a semver range. Since react-router-dom-v5-compat is in sharedPluginModules (line 34 of shared-modules-meta.ts), parseSharedModuleDeps will include it in the generated core package's peerDependencies (lines 135–136) with this alias as-is.

peerDependencies must contain valid SemVer ranges only. The npm: protocol is a package specifier (where-to-fetch), not a version range. npm and pnpm will reject this—pnpm explicitly errors with ERR_PNPM_INVALID_PEER_DEPENDENCY_SPECIFICATION when encountering protocol specifiers in peerDependencies.

🔧 Proposed fix
-const VIRTUAL_DEPS: PackageJson['dependencies'] = {
-  'react-router-dom-v5-compat': 'npm:react-router-dom@^6.30.x',
-};
+const VIRTUAL_DEPS: PackageJson['dependencies'] = {
+  // Semver range only; the npm: alias lives in the consuming package's dependencies.
+  'react-router-dom-v5-compat': '^6.30.0',
+};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const VIRTUAL_DEPS: PackageJson['dependencies'] = {
'react-router-dom-v5-compat': 'npm:react-router-dom@^6.30.x',
};
const VIRTUAL_DEPS: PackageJson['dependencies'] = {
// Semver range only; the npm: alias lives in the consuming package's dependencies.
'react-router-dom-v5-compat': '^6.30.0',
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts`
around lines 74 - 76, The VIRTUAL_DEPS entry uses an npm install-time alias
('npm:react-router-dom@^6.30.x') which is not a valid SemVer range and will end
up in peerDependencies via parseSharedModuleDeps; change the value for
'react-router-dom-v5-compat' in VIRTUAL_DEPS to a plain SemVer range (e.g.
'^6.30.x') so parseSharedModuleDeps emits a valid peerDependency, and if you
still need the install-time aliasing, implement that at install/alias resolution
time (not inside VIRTUAL_DEPS) or map the virtual name to the actual package
name elsewhere in the resolver that synthesizes install aliases.

Comment on lines +19 to +21
import { unstable_HistoryRouter as UnstableHistoryRouter } from 'react-router-dom';
import { mapExtensionToRoutes } from '@console/app/src/hooks/usePluginRoutes';
import { useParams, useLocation, Routes, Route } from 'react-router-dom-v5-compat';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

React Router v7 unstable_HistoryRouter removed

💡 Result:

unstable_HistoryRouter is not removed in React Router v7—it’s still exported, but it remains unstable and discouraged for most apps. In v7 it lives on the main react-router package exports. [1][2]

What likely changed for you in v7

  • You may have upgraded and (correctly) switched imports from react-router-dom to react-router. The v7 upgrade guide recommends importing everything from react-router. [3]
  • The component is still named unstable_HistoryRouter (there isn’t a stable HistoryRouter export). [1][2]

How to use it in v7

  • Import from react-router:
    • import { unstable_HistoryRouter } from "react-router"; [2]

If you used a custom history to migrate v6 → v7

  • React Router v7 added UNSAFE_createBrowserHistory plus UNSAFE_createMemoryHistory / UNSAFE_createHashHistory specifically to help apps using unstable_HistoryRouter migrate while avoiding mismatched history versions. [4]

Preferred alternative (if your goal is “navigate outside React”)

  • Use a data router (createBrowserRouter) and call router.navigate(...) from wherever you need, instead of maintaining your own history instance. (This is the direction React Router has been pushing since v6.4+.) [3]

Sources:
[1] https://reactrouter.com/api/declarative-routers/unstable_HistoryRouter
[2] https://api.reactrouter.com/v7/modules/react-router.html
[3] https://reactrouter.com/upgrading/v6
[4] https://reactrouter.com/start/start/changelog


🏁 Script executed:

# Check the actual imports in app.tsx at lines 19-21
sed -n '1,30p' frontend/public/components/app.tsx

Repository: openshift/console

Length of output: 1340


🏁 Script executed:

# Check project React Router version
cat package.json | grep -A 5 -B 5 react-router

Repository: openshift/console

Length of output: 105


🏁 Script executed:

# Search for unstable_HistoryRouter usage in the codebase
rg "unstable_HistoryRouter|UnstableHistoryRouter" --type tsx --type ts

Repository: openshift/console

Length of output: 88


🏁 Script executed:

# Find package.json in the repo
find . -name "package.json" -type f | head -5

Repository: openshift/console

Length of output: 276


🏁 Script executed:

# Search for unstable_HistoryRouter usage with correct syntax
rg "unstable_HistoryRouter|UnstableHistoryRouter" --type typescript

Repository: openshift/console

Length of output: 95


🏁 Script executed:

# Also check what's actually imported from react-router-dom-v5-compat
rg "from 'react-router-dom-v5-compat'" --type typescript -A 2

Repository: openshift/console

Length of output: 95


🏁 Script executed:

# Check the main frontend package.json for react-router versions
cat frontend/package.json | grep -E "react-router|react-router-dom"

Repository: openshift/console

Length of output: 164


🏁 Script executed:

# Search for unstable_HistoryRouter usage with correct file type
rg "unstable_HistoryRouter|UnstableHistoryRouter" -t ts -t tsx

Repository: openshift/console

Length of output: 88


🏁 Script executed:

# Check all imports from react-router-dom and react-router-dom-v5-compat in app.tsx
rg "from ['\"]react-router" frontend/public/components/app.tsx

Repository: openshift/console

Length of output: 228


unstable_HistoryRouter is a known trade-off; consolidate mixed imports.

The unstable_HistoryRouter carries an explicit stability warning and is the documented approach for maintaining Console's external history object (needed for programmatic navigation outside the React tree). Understand that this creates a dependency on react-router-dom 6.x as long as external history control is required; React Router v7 still exports it, but it remains unstable and the v7 upgrade guide recommends migrating to data routers with router.navigate(...) instead. This is a longer-term refactoring consideration if you can eliminate the external history dependency.

More immediately: consolidate the mixed imports. Line 19 imports from react-router-dom, line 21 from react-router-dom-v5-compat—pick one source and group all routing imports together.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/public/components/app.tsx` around lines 19 - 21, Consolidate the
mixed routing imports by importing all router symbols from the same package:
move the unstable_HistoryRouter (UnstableHistoryRouter) import so it's grouped
with useParams, useLocation, Routes, Route (or vice versa) instead of importing
from both 'react-router-dom' and 'react-router-dom-v5-compat'; update the import
statement(s) so that UnstableHistoryRouter, useParams, useLocation, Routes, and
Route come from a single source (either 'react-router-dom' or
'react-router-dom-v5-compat') and keep mapExtensionToRoutes import unchanged.

Comment on lines +74 to +80
{modalWrapper ? (
<ModalWrapper blocking={blocking} className={modalClassName} onClose={handleClose}>
<Component {...(props as any)} cancel={handleCancel} close={handleClose} />
</ModalWrapper>
) : (
<Component {...(props as any)} cancel={handleCancel} close={handleClose} />
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== createModalLauncher call sites =="
rg -nP --type=ts --type=tsx '\bcreateModalLauncher\s*\(' -C2

echo
echo "== Files containing both createModalLauncher and React Router APIs =="
python3 - <<'PY'
from pathlib import Path
import re

launcher = re.compile(r'\bcreateModalLauncher\s*\(')
router = re.compile(
    r'\b(useNavigate|useLocation|useParams|useSearchParams|useMatch|useResolvedPath)\b'
    r'|<\s*(Link|NavLink|Navigate)\b'
)

matched_files = []
for p in Path('.').rglob('*'):
    if p.suffix not in {'.ts', '.tsx'}:
        continue
    try:
        text = p.read_text(encoding='utf-8', errors='ignore')
        if launcher.search(text) and router.search(text):
            matched_files.append(str(p))
    except Exception as e:
        pass

if matched_files:
    print(f"Found {len(matched_files)} files:")
    for f in sorted(matched_files):
        print(f"  {f}")
else:
    print("No files found with both createModalLauncher and Router APIs")
PY

Repository: openshift/console

Length of output: 142


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Searching for createModalLauncher in TS/TSX files =="
fd -e ts -e tsx | xargs rg -l 'createModalLauncher' 2>/dev/null | head -20

echo
echo "== Files with both createModalLauncher and Router APIs =="
python3 - <<'PY'
from pathlib import Path
import re

launcher = re.compile(r'\bcreateModalLauncher\s*\(')
router = re.compile(
    r'\b(useNavigate|useLocation|useParams|useSearchParams|useMatch|useResolvedPath)\b'
    r'|<\s*(Link|NavLink|Navigate)\b'
)

matched_files = []
for p in Path('.').rglob('*.ts'):
    try:
        text = p.read_text(encoding='utf-8', errors='ignore')
        if launcher.search(text) and router.search(text):
            matched_files.append(str(p))
    except Exception:
        pass

for p in Path('.').rglob('*.tsx'):
    try:
        text = p.read_text(encoding='utf-8', errors='ignore')
        if launcher.search(text) and router.search(text):
            matched_files.append(str(p))
    except Exception:
        pass

matched_files = sorted(set(matched_files))
if matched_files:
    print(f"Found {len(matched_files)} files with both patterns:")
    for f in matched_files:
        print(f"  {f}")
else:
    print("No files found with both createModalLauncher and Router APIs")
PY

echo
echo "== Check the modal.tsx file directly =="
cat -n frontend/public/components/factory/modal.tsx | head -100

Repository: openshift/console

Length of output: 1755


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== modal.tsx content (first 120 lines) =="
cat -n frontend/public/components/factory/modal.tsx | head -120

echo
echo "== Check confirm-modal.tsx for Router APIs =="
grep -n -E 'useNavigate|useLocation|useParams|<Link|<NavLink|<Navigate' frontend/public/components/modals/confirm-modal.tsx 2>/dev/null || echo "No Router APIs found"

echo
echo "== Check add-users-modal.tsx for Router APIs =="
grep -n -E 'useNavigate|useLocation|useParams|<Link|<NavLink|<Navigate' frontend/public/components/modals/add-users-modal.tsx 2>/dev/null || echo "No Router APIs found"

echo
echo "== Check all modals for Router APIs - comprehensive search =="
for file in frontend/public/components/modals/*.tsx; do
  if grep -q -E 'useNavigate|useLocation|useParams|<Link|<NavLink|<Navigate' "$file" 2>/dev/null; then
    echo "  $file: USES ROUTER APIs"
  fi
done

Repository: openshift/console

Length of output: 5570


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== delete-modal.tsx Router API usage =="
grep -n -E 'useNavigate|useLocation|useParams|<Link|<NavLink|<Navigate' frontend/public/components/modals/delete-modal.tsx

echo
echo "== cluster-update-modal.tsx Router API usage =="
grep -n -E 'useNavigate|useLocation|useParams|<Link|<NavLink|<Navigate' frontend/public/components/modals/cluster-update-modal.tsx

echo
echo "== Sample of delete-modal.tsx showing Router hook usage =="
grep -B3 -A3 'useNavigate\|useLocation' frontend/public/components/modals/delete-modal.tsx | head -30

Repository: openshift/console

Length of output: 1140


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check for Router/BrowserRouter usage in codebase =="
grep -r 'from .react-router' --include='*.ts' --include='*.tsx' | grep -i 'router\|browserrouter' | head -20

echo
echo "== Check App.tsx for router setup =="
find . -name 'App.tsx' -type f | head -5

echo
echo "== Look at main entry point for router context =="
grep -r 'BrowserRouter\|HistoryRouter' --include='*.ts' --include='*.tsx' | head -10

Repository: openshift/console

Length of output: 2821


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check main console app initialization for Router =="
fd -e tsx -e ts | xargs grep -l 'BrowserRouter\|HistoryRouter' | head -10

echo
echo "== Check index or root App file =="
find . -name 'index.tsx' -o -name 'App.tsx' | grep -E 'frontend/public|console-app' | head -5 | xargs ls -la 2>/dev/null

echo
echo "== Search for Router initialization in public folder =="
grep -r 'Router\|BrowserRouter' frontend/public --include='*.tsx' --include='*.ts' | grep -v node_modules | grep -v '.spec' | head -10

Repository: openshift/console

Length of output: 291


🏁 Script executed:

#!/bin/bash
set -euoarpipefail

echo "== Check app.tsx Router context =="
grep -n 'BrowserRouter\|HistoryRouter\|Router' frontend/public/components/app.tsx | head -20

echo
echo "== Confirm modal mounting location (separate root) =="
grep -n 'modal-container' frontend/public/components/factory/modal.tsx

echo
echo "== Verify delete-modal is launched via createModalLauncher =="
grep -n 'createModalLauncher\|DeleteModal' frontend/public/components/modals/delete-modal.tsx | tail -5

Repository: openshift/console

Length of output: 735


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check app.tsx Router context =="
grep -n 'BrowserRouter\|HistoryRouter\|Router' frontend/public/components/app.tsx | head -20

echo
echo "== Confirm modal mounting location (separate root) =="
grep -n 'modal-container' frontend/public/components/factory/modal.tsx

echo
echo "== Verify delete-modal is launched via createModalLauncher =="
grep -n 'createModalLauncher\|DeleteModal' frontend/public/components/modals/delete-modal.tsx | tail -5

Repository: openshift/console

Length of output: 1052


Router context missing in launcher-mounted modals.

Lines 74–80 render Component without a router provider. Since createModal mounts via ReactDOM.render() to a separate root (line 17), any modal using useNavigate(), useLocation(), or router components (Link, etc.) will fail at runtime with "must be used within a <Router>".

At least 7 modals already depend on Router APIs (e.g., DeleteModal, ClusterUpdateModal), making this a blocker for their use.

Fix: Wrap with HistoryRouter using the app's history instance

The app already initializes UnstableHistoryRouter with a shared history instance in app.tsx. Import and reuse it:

+import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom';
+import { history } from '../../history';  // shared instance from app.tsx setup

 <Provider store={store}>
   <PluginStoreProvider store={pluginStore}>
+    <HistoryRouter history={history}>
       {modalWrapper ? (
         <ModalWrapper blocking={blocking} className={modalClassName} onClose={handleClose}>
           <Component {...(props as any)} cancel={handleCancel} close={handleClose} />
         </ModalWrapper>
       ) : (
         <Component {...(props as any)} cancel={handleCancel} close={handleClose} />
       )}
+    </HistoryRouter>
   </PluginStoreProvider>
 </Provider>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{modalWrapper ? (
<ModalWrapper blocking={blocking} className={modalClassName} onClose={handleClose}>
<Component {...(props as any)} cancel={handleCancel} close={handleClose} />
</ModalWrapper>
) : (
<Component {...(props as any)} cancel={handleCancel} close={handleClose} />
)}
<HistoryRouter history={history}>
{modalWrapper ? (
<ModalWrapper blocking={blocking} className={modalClassName} onClose={handleClose}>
<Component {...(props as any)} cancel={handleCancel} close={handleClose} />
</ModalWrapper>
) : (
<Component {...(props as any)} cancel={handleCancel} close={handleClose} />
)}
</HistoryRouter>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/public/components/factory/modal.tsx` around lines 74 - 80, The modal
rendering path mounts components outside the app router so any component using
router hooks (e.g., useNavigate/useLocation) fails; update the render in
ModalWrapper/Component branch to wrap the output with the app's HistoryRouter
(the UnstableHistoryRouter or HistoryRouter instance used in app.tsx) using the
shared history instance so router context is available to launched modals;
specifically import the UnstableHistoryRouter (or HistoryRouter) and the shared
history that app.tsx initializes, and wrap the JSX returned for both the
ModalWrapper and direct Component render inside that router (preserving
blocking, className, onClose, cancel, close props) so createModal-mounted
components like DeleteModal and ClusterUpdateModal receive router context.

Comment on lines 44 to 48
history.__replace__ = history.replace;
history.replace = (url: string) => (history as any).__replace__(removeBasePath(url));

(history as any).__push__ = history.push;
history.__push__ = history.push;
history.push = (url: string) => (history as any).__push__(removeBasePath(url));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Monkey-patched push/replace narrow the call signature, dropping state support.

The original history.push and history.replace in @remix-run/router accept (to: To, state?: any). The overrides on lines 45 and 48 only accept (url: string), which means:

  1. Any caller passing a location object (e.g., { pathname, search, hash }) will silently misbehave — removeBasePath will receive an object, and _.startsWith(obj, ...) returns false, passing the object through as-is.
  2. Navigation state is silently dropped — there's no way to pass state through the patched methods.

If this is intentional to discourage non-hook usage, the @deprecated JSDoc is good, but callers that do still use history.push(path, state) will lose their state without warning.

Consider at minimum preserving the state parameter:

Proposed fix
-history.__replace__ = history.replace;
-history.replace = (url: string) => (history as any).__replace__(removeBasePath(url));
+history.__replace__ = history.replace;
+history.replace = (url: string, state?: any) => (history as any).__replace__(removeBasePath(url), state);

-history.__push__ = history.push;
-history.push = (url: string) => (history as any).__push__(removeBasePath(url));
+history.__push__ = history.push;
+history.push = (url: string, state?: any) => (history as any).__push__(removeBasePath(url), state);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/public/components/utils/router.ts` around lines 44 - 48, The
monkey-patched overrides for history.replace/history.push
(history.__replace__/history.__push and the new history.replace/history.push)
drop the optional state parameter and mishandle non-string locations; update the
patched signatures to match the original (to: To, state?: any) and preserve
state when delegating to the originals; inside the new functions, detect if to
is a string and call removeBasePath(to) before passing it through, otherwise
pass the location object unchanged, and forward the optional state argument to
(history as any).__replace__ / __push__; also update typings/JSDoc for
history.replace and history.push to reflect the original signature.

@logonoff logonoff force-pushed the drop-react-router-v5 branch from 63e9560 to 1f60c7a Compare February 25, 2026 15:40
@openshift-ci openshift-ci bot added the kind/demo-plugin Related to dynamic-demo-plugin label Feb 25, 2026
@openshift-ci
Copy link
Contributor

openshift-ci bot commented Feb 25, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: vojtechszocs

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@logonoff logonoff force-pushed the drop-react-router-v5 branch 3 times, most recently from 2225c09 to 3b134b3 Compare February 25, 2026 18:19
@logonoff
Copy link
Member

/uncc christoph-jerolimov
/uncc avivtur

@logonoff logonoff force-pushed the drop-react-router-v5 branch 4 times, most recently from 48fa012 to 708a672 Compare March 6, 2026 15:11
@logonoff
Copy link
Member

logonoff commented Mar 6, 2026

/remove-label tide/merge-method-squash

@openshift-ci openshift-ci bot removed the tide/merge-method-squash Denotes a PR that should be squashed by tide when it merges. label Mar 6, 2026
@logonoff logonoff force-pushed the drop-react-router-v5 branch from 708a672 to 6be4f11 Compare March 6, 2026 15:29
@logonoff
Copy link
Member

logonoff commented Mar 6, 2026

/retitle CONSOLE-4439: Upgrade to React Router 7

@logonoff
Copy link
Member

logonoff commented Mar 6, 2026

Blocked by #16022 and #15968

logonoff and others added 4 commits March 6, 2026 17:25
This is a re-implementation of the previous commit head https://github.com/openshift/console/tree/1d70080fb482a10dec9084d92165779dc0ad3ff6

Except instead of rewriting every `react-router-dom-v5-compat` import we will alias them for the time being, to save ourselves from rebase hell

Co-Authored-By: Vojtech Szocs <vojtech.szocs@gmail.com>
Co-Authored-By: Marek Ziska <49416557+mylanos@users.noreply.github.com>
Replace the `react-router-hash-link` package with a custom `HashLink`
component in `@console/shared`. The new implementation handles both
same-page and cross-page hash navigation using React Router v7 APIs,
with proper cleanup on unmount and support for smooth scrolling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@openshift-ci
Copy link
Contributor

openshift-ci bot commented Mar 7, 2026

@vojtechszocs: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/verify-deps 5e1f4dc link true /test verify-deps
ci/prow/frontend 6655cb0 link true /test frontend
ci/prow/e2e-gcp-console 6655cb0 link true /test e2e-gcp-console

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. component/backend Related to backend component/core Related to console core functionality component/dashboard Related to dashboard component/dev-console Related to dev-console component/gitops Related to gitops-plugin component/helm Related to helm-plugin component/knative Related to knative-plugin component/metal3 Related to metal3-plugin component/monitoring Related to monitoring component/olm Related to OLM component/pipelines Related to pipelines-plugin component/sdk Related to console-plugin-sdk component/shared Related to console-shared component/topology Related to topology jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. kind/demo-plugin Related to dynamic-demo-plugin plugin-api-changed Categorizes a PR as containing plugin API changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants