From a8df6a20a9ba76c043a54d30e3c6fde2565a83d2 Mon Sep 17 00:00:00 2001
From: Michele Baldessari
Date: Wed, 10 Jun 2026 17:25:22 +0200
Subject: [PATCH] React compatibility improvement
This makes the UI work across OCP 4.19 to 4.22.
4.22 uses react v6 so a small compatility layer is needed.
Tested manually on 4.18, 4.19, 4.20, 4.21 and 4.22
---
console/src/components/InstallPatternPage.tsx | 16 ++++++++--------
console/src/components/ManageSecretsPage.tsx | 12 ++++++------
console/src/components/PatternCatalogPage.tsx | 10 +++++-----
console/src/components/UninstallPatternPage.tsx | 12 ++++++------
console/src/hooks/useNavigateCompat.ts | 16 ++++++++++++++++
console/src/hooks/useParamsCompat.ts | 14 ++++++++++++++
6 files changed, 55 insertions(+), 25 deletions(-)
create mode 100644 console/src/hooks/useNavigateCompat.ts
create mode 100644 console/src/hooks/useParamsCompat.ts
diff --git a/console/src/components/InstallPatternPage.tsx b/console/src/components/InstallPatternPage.tsx
index ed288490e..0112e2889 100644
--- a/console/src/components/InstallPatternPage.tsx
+++ b/console/src/components/InstallPatternPage.tsx
@@ -1,7 +1,8 @@
import * as React from 'react';
import Helmet from 'react-helmet';
import { useTranslation } from 'react-i18next';
-import { useHistory, useRouteMatch } from 'react-router-dom';
+import { useNavigateCompat } from '../hooks/useNavigateCompat';
+import { useParamsCompat } from '../hooks/useParamsCompat';
import {
ActionGroup,
Alert,
@@ -56,9 +57,8 @@ const PatternModel = {
export default function InstallPatternPage() {
const { t } = useTranslation('plugin__patterns-operator-console-plugin');
- const history = useHistory();
- const match = useRouteMatch<{ name: string }>('/patterns/install/:name');
- const name = match?.params?.name;
+ const navigate = useNavigateCompat();
+ const { name } = useParamsCompat('/patterns/install/:name');
// Secret form state (integrated inline instead of separate page)
@@ -251,11 +251,11 @@ export default function InstallPatternPage() {
if (hasSecrets && vaultJobStatus?.status !== 'succeeded') return;
const timer = setTimeout(() => {
- history.push('/patterns');
+ navigate('/patterns');
}, 3000);
return () => clearTimeout(timer);
- }, [patternStatus, history, secretTemplate, secretFormData, vaultJobStatus]);
+ }, [patternStatus, navigate, secretTemplate, secretFormData, vaultJobStatus]);
const handleSubmit = async () => {
console.log('🚀 [InstallPatternPage] Starting pattern installation process');
@@ -410,7 +410,7 @@ export default function InstallPatternPage() {
return t('Your pattern has been created. The operator is now reconciling it.');
})()}
-
- history.push('/patterns')}>
+ navigate('/patterns')}>
{t('Cancel')}
diff --git a/console/src/components/ManageSecretsPage.tsx b/console/src/components/ManageSecretsPage.tsx
index ed22cf551..83b6dc507 100644
--- a/console/src/components/ManageSecretsPage.tsx
+++ b/console/src/components/ManageSecretsPage.tsx
@@ -1,7 +1,8 @@
import * as React from 'react';
import Helmet from 'react-helmet';
import { useTranslation } from 'react-i18next';
-import { useHistory, useRouteMatch } from 'react-router-dom';
+import { useNavigateCompat } from '../hooks/useNavigateCompat';
+import { useParamsCompat } from '../hooks/useParamsCompat';
import {
ActionGroup,
Alert,
@@ -29,9 +30,8 @@ import './SecretForm/SecretForm.css';
export default function ManageSecretsPage() {
const { t } = useTranslation('plugin__patterns-operator-console-plugin');
- const history = useHistory();
- const match = useRouteMatch<{ name: string }>('/patterns/secrets/:name');
- const name = match?.params?.name;
+ const navigate = useNavigateCompat();
+ const { name } = useParamsCompat('/patterns/secrets/:name');
const [loading, setLoading] = React.useState(true);
const [fetchError, setFetchError] = React.useState(null);
@@ -171,7 +171,7 @@ export default function ManageSecretsPage() {
{t('This pattern does not have a secret template defined.')}
- history.push('/patterns')}>
+ navigate('/patterns')}>
{t('Back to catalog')}
@@ -234,7 +234,7 @@ export default function ManageSecretsPage() {
{t('Inject Secrets')}
- history.push('/patterns')}>
+ navigate('/patterns')}>
{t('Back to catalog')}
diff --git a/console/src/components/PatternCatalogPage.tsx b/console/src/components/PatternCatalogPage.tsx
index b79c05a8d..5dfbd7b65 100644
--- a/console/src/components/PatternCatalogPage.tsx
+++ b/console/src/components/PatternCatalogPage.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import Helmet from 'react-helmet';
import { useTranslation } from 'react-i18next';
-import { useHistory } from 'react-router-dom';
+import { useNavigateCompat } from '../hooks/useNavigateCompat';
import {
Alert,
@@ -166,7 +166,7 @@ const TIER_DESCRIPTIONS: Record = {
export default function PatternCatalogPage() {
const { t } = useTranslation('plugin__patterns-operator-console-plugin');
- const history = useHistory();
+ const navigate = useNavigateCompat();
const [patterns, setPatterns] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
@@ -488,7 +488,7 @@ export default function PatternCatalogPage() {
- history.push(
+ navigate(
`/patterns/secrets/${pattern.catalogKey || pattern.name}`,
)
}
@@ -499,7 +499,7 @@ export default function PatternCatalogPage() {
{isInstalled && (
history.push(`/patterns/uninstall/${pattern.name}`)}
+ onClick={() => navigate(`/patterns/uninstall/${pattern.name}`)}
>
{t('Uninstall')}
@@ -515,7 +515,7 @@ export default function PatternCatalogPage() {
variant="primary"
isDisabled={isDisabled}
onClick={() =>
- history.push(
+ navigate(
`/patterns/install/${pattern.catalogKey || pattern.name}`,
)
}
diff --git a/console/src/components/UninstallPatternPage.tsx b/console/src/components/UninstallPatternPage.tsx
index 4353041cd..d5028b807 100644
--- a/console/src/components/UninstallPatternPage.tsx
+++ b/console/src/components/UninstallPatternPage.tsx
@@ -1,7 +1,8 @@
import * as React from 'react';
import Helmet from 'react-helmet';
import { useTranslation } from 'react-i18next';
-import { useHistory, useRouteMatch } from 'react-router-dom';
+import { useNavigateCompat } from '../hooks/useNavigateCompat';
+import { useParamsCompat } from '../hooks/useParamsCompat';
import {
Alert,
Button,
@@ -29,9 +30,8 @@ const DELETION_PHASES: Record = {
export default function UninstallPatternPage() {
const { t } = useTranslation('plugin__patterns-operator-console-plugin');
- const history = useHistory();
- const match = useRouteMatch<{ name: string }>('/patterns/uninstall/:name');
- const name = match?.params?.name;
+ const navigate = useNavigateCompat();
+ const { name } = useParamsCompat('/patterns/uninstall/:name');
const [status, setStatus] = React.useState(null);
const [deleting, setDeleting] = React.useState(false);
@@ -127,7 +127,7 @@ export default function UninstallPatternPage() {
{deleted && (
{t('The pattern and all its associated resources have been fully deleted.')}
- history.push('/patterns')}>
+ navigate('/patterns')}>
{t('Back to catalog')}
@@ -187,7 +187,7 @@ export default function UninstallPatternPage() {
>
{t('Confirm Uninstall')}
- history.push('/patterns')}>
+ navigate('/patterns')}>
{t('Cancel')}
diff --git a/console/src/hooks/useNavigateCompat.ts b/console/src/hooks/useNavigateCompat.ts
new file mode 100644
index 000000000..4b26bdc34
--- /dev/null
+++ b/console/src/hooks/useNavigateCompat.ts
@@ -0,0 +1,16 @@
+import * as React from 'react';
+import * as ReactRouterDom from 'react-router-dom';
+
+const hasUseNavigate = typeof (ReactRouterDom as any).useNavigate === 'function';
+
+// React Router v6 removed useHistory in favor of useNavigate.
+// OCP 4.22+ ships v6; OCP 4.21 and earlier ship v5.
+export const useNavigateCompat: () => (path: string) => void = hasUseNavigate
+ ? () => {
+ const navigate = (ReactRouterDom as any).useNavigate();
+ return React.useCallback((path: string) => navigate(path), [navigate]);
+ }
+ : () => {
+ const history = (ReactRouterDom as any).useHistory();
+ return React.useCallback((path: string) => history.push(path), [history]);
+ };
diff --git a/console/src/hooks/useParamsCompat.ts b/console/src/hooks/useParamsCompat.ts
new file mode 100644
index 000000000..87d624fe9
--- /dev/null
+++ b/console/src/hooks/useParamsCompat.ts
@@ -0,0 +1,14 @@
+import * as ReactRouterDom from 'react-router-dom';
+
+const isV6 = typeof (ReactRouterDom as any).useNavigate === 'function';
+
+// React Router v5 (OCP < 4.22): useParams may not work if the console
+// framework doesn't expose route params through the standard context.
+// useRouteMatch explicitly matches the current URL against the given pattern.
+// React Router v6 (OCP 4.22+): useParams is the standard API.
+export const useParamsCompat: (pattern: string) => Record = isV6
+ ? () => (ReactRouterDom as any).useParams()
+ : (pattern: string) => {
+ const match = (ReactRouterDom as any).useRouteMatch(pattern);
+ return match?.params || {};
+ };