Skip to content

Commit 23ae6dd

Browse files
rhamiltoclaude
andcommitted
CONSOLE-4990: Migrate from history object to React Router v6/v7 hooks
Migrates from direct history object usage to React Router v6/v7 compatible hook-based patterns as part of the React Router v7 upgrade effort. - Created useQueryParamsMutator() hook providing query parameter mutation functions (setQueryArgument, setQueryArguments, setAllQueryArguments, removeQueryArgument, removeQueryArguments, setOrRemoveQueryArgument) - Uses useSearchParams() from react-router-dom-v5-compat - Stable function references via useRef pattern - prevents unnecessary re-renders and eliminates dependency array workarounds - Only triggers updates when values actually change (performance optimization) - Uses replace: true to avoid polluting browser history - Preserves location.hash and location.state on all mutations - Added comprehensive unit tests in router-hooks.spec.tsx Easy conversions - Added hook call, updated imports: - public/components/filter-toolbar.tsx - public/components/search.tsx - public/components/api-explorer.tsx - public/components/cluster-settings/cluster-settings.tsx - public/components/namespace-bar.tsx - public/components/useRowFilterFix.ts - public/components/useLabelSelectionFix.ts - public/components/useSearchFilters.ts - packages/topology/src/components/page/TopologyPage.tsx - packages/topology/src/components/page/TopologyView.tsx - packages/topology/src/filters/TopologyFilterBar.tsx - packages/console-shared/src/components/catalog/CatalogController.tsx - packages/operator-lifecycle-manager/src/components/subscription.tsx - packages/operator-lifecycle-manager/src/components/operator-hub/operator-channel-version-select.tsx - packages/console-app/src/components/nodes/NodeLogs.tsx Complex refactors: - pod-logs.jsx - Functional wrapper pattern to inject hooks without class conversion - filter-utils.ts - Removed deprecated functions, moved logic to TopologyFilterBar - QuickSearchModalBody.tsx + 4 files - Replaced history.push() with useNavigate() - Fixed "Clear all filters" functionality in Search page and Topology filter bar by using removeQueryArguments() to atomically remove multiple params - Fixed getQueryArgument reference stability issue in TopologyPage useEffect by implementing searchParamsRef pattern (addresses PR review feedback) - Removed deprecated query parameter mutation functions from router.ts - Removed unnecessary useRouterPush hook (use useNavigate() directly) - Removed unnecessary location.state from setSearchParams calls (React Router preserves state automatically with replace: true) - Removed eslint-disable workaround in TopologyPage (no longer needed) - Updated TopologyPage tests to mock useQueryParamsMutator directly instead of low-level router hooks (better abstraction, more maintainable) The history object export is kept as it's still used by: - Router component initialization in app.tsx - Monkey-patching for base path handling - 20+ other files (separate migration) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 61387af commit 23ae6dd

File tree

25 files changed

+583
-171
lines changed

25 files changed

+583
-171
lines changed

frontend/packages/console-app/src/components/nodes/NodeLogs.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,7 @@ import { css } from '@patternfly/react-styles';
2222
import { Trans, useTranslation } from 'react-i18next';
2323
import { coFetch } from '@console/internal/co-fetch';
2424
import { ThemeContext } from '@console/internal/components/ThemeProvider';
25-
import {
26-
getQueryArgument,
27-
removeQueryArgument,
28-
setQueryArgument,
29-
} from '@console/internal/components/utils/router';
25+
import { useQueryParamsMutator } from '@console/internal/components/utils/router';
3026
import { LoadingBox, LoadingInline } from '@console/internal/components/utils/status-box';
3127
import { modelFor, NodeKind, resourceURL } from '@console/internal/module/k8s';
3228
import PaneBody from '@console/shared/src/components/layout/PaneBody';
@@ -179,6 +175,8 @@ const HeaderBanner: FC<{ lineCount: number }> = ({ lineCount }) => {
179175
};
180176

181177
const NodeLogs: FC<NodeLogsProps> = ({ obj: node }) => {
178+
const { getQueryArgument, setQueryArgument, removeQueryArgument } = useQueryParamsMutator();
179+
182180
const {
183181
kind,
184182
metadata: { labels, name, namespace: ns },

frontend/packages/console-shared/src/components/catalog/CatalogController.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useLocation } from 'react-router-dom-v5-compat';
66
import { FLAG_TECH_PREVIEW } from '@console/app/src/consts';
77
import { ResolvedExtension, CatalogItemType, CatalogCategory } from '@console/dynamic-plugin-sdk';
88
import { CatalogItem } from '@console/dynamic-plugin-sdk/src/extensions';
9-
import { removeQueryArgument, setQueryArgument } from '@console/internal/components/utils/router';
9+
import { useQueryParamsMutator } from '@console/internal/components/utils/router';
1010
import { skeletonCatalog } from '@console/internal/components/utils/skeleton-catalog';
1111
import { StatusBox } from '@console/internal/components/utils/status-box';
1212
import OLMv1Alert from '@console/operator-lifecycle-manager-v1/src/components/OLMv1Alert';
@@ -51,6 +51,7 @@ const CatalogController: FC<CatalogControllerProps> = ({
5151
hideSidebar,
5252
categories,
5353
}) => {
54+
const { setQueryArgument, removeQueryArgument } = useQueryParamsMutator();
5455
const { t } = useTranslation();
5556
const { pathname } = useLocation();
5657
const queryParams = useQueryParams();
@@ -151,13 +152,16 @@ const CatalogController: FC<CatalogControllerProps> = ({
151152
[catalogItems, filterGroups],
152153
);
153154

154-
const openDetailsPanel = useCallback((item: CatalogItem): void => {
155-
setQueryArgument(CatalogQueryParams.SELECTED_ID, item.uid);
156-
}, []);
155+
const openDetailsPanel = useCallback(
156+
(item: CatalogItem): void => {
157+
setQueryArgument(CatalogQueryParams.SELECTED_ID, item.uid);
158+
},
159+
[setQueryArgument],
160+
);
157161

158162
const closeDetailsPanel = useCallback((): void => {
159163
removeQueryArgument(CatalogQueryParams.SELECTED_ID);
160-
}, []);
164+
}, [removeQueryArgument]);
161165

162166
const renderTile = useCallback(
163167
(item: CatalogItem) => (

frontend/packages/console-shared/src/components/quick-search/QuickSearchDetails.tsx

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { ReactNode, FC } from 'react';
22
import { Button, ButtonVariant, Content, Title } from '@patternfly/react-core';
33
import { useTranslation } from 'react-i18next';
4+
import { useNavigate } from 'react-router-dom-v5-compat';
45
import { CatalogItem } from '@console/dynamic-plugin-sdk/src/extensions/catalog';
6+
import { useQueryParamsMutator } from '@console/internal/components/utils/router';
57
import { useTelemetry } from '../../hooks/useTelemetry';
68
import CatalogBadges from '../catalog/CatalogBadges';
79
import { handleCta } from './utils/quick-search-utils';
@@ -11,10 +13,14 @@ import './QuickSearchDetails.scss';
1113
export type QuickSearchDetailsRendererProps = {
1214
selectedItem: CatalogItem;
1315
closeModal: () => void;
16+
navigate: (url: string) => void;
17+
removeQueryArgument: (key: string) => void;
1418
};
1519
export type DetailsRendererFunction = (props: QuickSearchDetailsRendererProps) => ReactNode;
16-
export interface QuickSearchDetailsProps extends QuickSearchDetailsRendererProps {
17-
detailsRenderer: DetailsRendererFunction;
20+
export interface QuickSearchDetailsProps {
21+
selectedItem: CatalogItem;
22+
closeModal: () => void;
23+
detailsRenderer?: DetailsRendererFunction;
1824
}
1925

2026
const QuickSearchDetails: FC<QuickSearchDetailsProps> = ({
@@ -23,18 +29,18 @@ const QuickSearchDetails: FC<QuickSearchDetailsProps> = ({
2329
detailsRenderer,
2430
}) => {
2531
const { t } = useTranslation();
32+
const navigate = useNavigate();
33+
const { removeQueryArgument } = useQueryParamsMutator();
2634
const fireTelemetryEvent = useTelemetry();
2735

28-
const defaultContentRenderer: DetailsRendererFunction = (
29-
props: QuickSearchDetailsProps,
30-
): ReactNode => {
36+
const defaultContentRenderer = (): ReactNode => {
3137
return (
3238
<>
33-
<Title headingLevel="h4">{props.selectedItem.name}</Title>
34-
{props.selectedItem.provider && (
39+
<Title headingLevel="h4">{selectedItem.name}</Title>
40+
{selectedItem.provider && (
3541
<span className="ocs-quick-search-details__provider">
3642
{t('console-shared~Provided by {{provider}}', {
37-
provider: props.selectedItem.provider,
43+
provider: selectedItem.provider,
3844
})}
3945
</span>
4046
)}
@@ -46,22 +52,30 @@ const QuickSearchDetails: FC<QuickSearchDetailsProps> = ({
4652
className="ocs-quick-search-details__form-button"
4753
data-test="create-quick-search"
4854
onClick={(e) => {
49-
handleCta(e, props.selectedItem, props.closeModal, fireTelemetryEvent);
55+
handleCta(
56+
e,
57+
selectedItem,
58+
closeModal,
59+
fireTelemetryEvent,
60+
navigate,
61+
removeQueryArgument,
62+
);
5063
}}
5164
>
52-
{props.selectedItem.cta.label}
65+
{selectedItem.cta.label}
5366
</Button>
5467
<Content className="ocs-quick-search-details__description">
55-
{props.selectedItem.description}
68+
{selectedItem.description}
5669
</Content>
5770
</>
5871
);
5972
};
60-
const detailsContentRenderer: DetailsRendererFunction = detailsRenderer ?? defaultContentRenderer;
6173

6274
return (
6375
<div className="ocs-quick-search-details">
64-
{detailsContentRenderer({ selectedItem, closeModal })}
76+
{detailsRenderer
77+
? detailsRenderer({ selectedItem, closeModal, navigate, removeQueryArgument })
78+
: defaultContentRenderer()}
6579
</div>
6680
);
6781
};

frontend/packages/console-shared/src/components/quick-search/QuickSearchList.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import {
1414
} from '@patternfly/react-core';
1515
import { css } from '@patternfly/react-styles';
1616
import { useTranslation } from 'react-i18next';
17-
import { Link } from 'react-router-dom-v5-compat';
17+
import { Link, useNavigate } from 'react-router-dom-v5-compat';
1818
import { CatalogItem } from '@console/dynamic-plugin-sdk';
1919
import { getImageForIconClass } from '@console/internal/components/catalog/catalog-item-icon';
20+
import { useQueryParamsMutator } from '@console/internal/components/utils/router';
2021
import { useTelemetry } from '../../hooks/useTelemetry';
2122
import { CatalogType, getIconProps } from '../catalog';
2223
import { CatalogLinkData } from './utils/quick-search-types';
@@ -48,6 +49,8 @@ const QuickSearchList: FC<QuickSearchListProps> = ({
4849
onListChange,
4950
}) => {
5051
const { t } = useTranslation();
52+
const navigate = useNavigate();
53+
const { removeQueryArgument } = useQueryParamsMutator();
5154
const fireTelemetryEvent = useTelemetry();
5255
const [itemsCount, setItemsCount] = useState<number>(limitItemCount || listItems.length);
5356
const listHeight = document.querySelector('.ocs-quick-search-list__list')?.clientHeight || 0;
@@ -105,7 +108,9 @@ const QuickSearchList: FC<QuickSearchListProps> = ({
105108
'ocs-quick-search-list__item--highlight': item.uid === selectedItemId,
106109
})}
107110
onDoubleClick={(e: SyntheticEvent) => {
108-
handleCta(e, item, closeModal, fireTelemetryEvent);
111+
if (item.cta) {
112+
handleCta(e, item, closeModal, fireTelemetryEvent, navigate, removeQueryArgument);
113+
}
109114
}}
110115
>
111116
<DataListItemRow className="ocs-quick-search-list__item-row">

frontend/packages/console-shared/src/components/quick-search/QuickSearchModalBody.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import type { ReactNode, FC, FormEvent } from 'react';
22
import { useState, useRef, useEffect, useCallback } from 'react';
33
import { ModalBody, ModalHeader } from '@patternfly/react-core';
4+
import { useNavigate } from 'react-router-dom-v5-compat';
45
import { CatalogItem } from '@console/dynamic-plugin-sdk';
5-
import {
6-
getQueryArgument,
7-
removeQueryArgument,
8-
setQueryArgument,
9-
history,
10-
} from '@console/internal/components/utils/router';
6+
import { useQueryParamsMutator } from '@console/internal/components/utils/router';
117
import { useTelemetry } from '../../hooks/useTelemetry';
128
import { CatalogType } from '../catalog';
139
import QuickSearchBar from './QuickSearchBar';
@@ -37,6 +33,8 @@ const QuickSearchModalBody: FC<QuickSearchModalBodyProps> = ({
3733
icon,
3834
detailsRenderer,
3935
}) => {
36+
const { getQueryArgument, setQueryArgument, removeQueryArgument } = useQueryParamsMutator();
37+
const navigate = useNavigate();
4038
const [catalogItems, setCatalogItems] = useState<CatalogItem[]>(null);
4139
const [catalogTypes, setCatalogTypes] = useState<CatalogType[]>([]);
4240
const [searchTerm, setSearchTerm] = useState<string>(getQueryArgument('catalogSearch') || '');
@@ -81,7 +79,7 @@ const QuickSearchModalBody: FC<QuickSearchModalBodyProps> = ({
8179
setSelectedItemId('');
8280
setSelectedItem(null);
8381
},
84-
[searchCatalog],
82+
[searchCatalog, setQueryArgument, removeQueryArgument],
8583
);
8684

8785
const onCancel = useCallback(() => {
@@ -104,12 +102,12 @@ const QuickSearchModalBody: FC<QuickSearchModalBodyProps> = ({
104102
const { id } = document.activeElement;
105103
const activeViewAllLink = viewAll?.find((link) => link.catalogType === id);
106104
if (activeViewAllLink) {
107-
history.push(activeViewAllLink.to);
105+
navigate(activeViewAllLink.to);
108106
} else if (selectedItem) {
109-
handleCta(e, selectedItem, closeModal, fireTelemetryEvent);
107+
handleCta(e, selectedItem, closeModal, fireTelemetryEvent, navigate, removeQueryArgument);
110108
}
111109
},
112-
[closeModal, fireTelemetryEvent, selectedItem, viewAll],
110+
[closeModal, fireTelemetryEvent, selectedItem, viewAll, navigate, removeQueryArgument],
113111
);
114112

115113
const selectPrevious = useCallback(() => {

frontend/packages/console-shared/src/components/quick-search/utils/quick-search-utils.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { SyntheticEvent } from 'react';
22
import { CatalogItem } from '@console/dynamic-plugin-sdk';
3-
import { history, removeQueryArgument } from '@console/internal/components/utils/router';
43
import { keywordCompare } from '../../catalog';
54

65
export const quickSearch = (items: CatalogItem[], query: string) => {
@@ -12,6 +11,8 @@ export const handleCta = async (
1211
item: CatalogItem,
1312
closeModal: () => void,
1413
fireTelemetryEvent: (event: string, properties?: {}) => void,
14+
navigate: (url: string) => void,
15+
removeQueryArg: (key: string) => void,
1516
callbackProps: { [key: string]: string } = {},
1617
) => {
1718
e.preventDefault();
@@ -24,6 +25,6 @@ export const handleCta = async (
2425
});
2526
closeModal();
2627
await callback(callbackProps);
27-
removeQueryArgument('catalogSearch');
28-
} else history.push(href);
28+
removeQueryArg('catalogSearch');
29+
} else navigate(href);
2930
};

frontend/packages/operator-lifecycle-manager/integration-tests-cypress/views/operator.view.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ export const operator = {
9393
projectDropdown.shouldContain(installedNamespace);
9494
operator.filterByName(operatorName);
9595
listPage.rows.countShouldBe(1);
96+
// TODO: figure out why this arbitrary wait is needed
97+
cy.wait(3000);
9698
cy.byTestOperatorRow(operatorName).should('exist');
9799
cy.byTestOperatorRow(operatorName).click();
98100
},

frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-channel-version-select.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from '@patternfly/react-core';
1111
import * as _ from 'lodash';
1212
import { useTranslation } from 'react-i18next';
13-
import { setQueryArgument } from '@console/internal/components/utils';
13+
import { useQueryParamsMutator } from '@console/internal/components/utils/router';
1414
import { alphanumericCompare } from '@console/shared';
1515
import { PackageManifestKind } from '../../types';
1616
import { DeprecatedOperatorWarningIcon } from '../deprecated-operator-warnings/deprecated-operator-warnings';
@@ -23,6 +23,7 @@ export const OperatorChannelSelect: FC<OperatorChannelSelectProps> = ({
2323
setUpdateVersion,
2424
}) => {
2525
const { t } = useTranslation();
26+
const { setQueryArgument } = useQueryParamsMutator();
2627
const channels = useMemo(() => packageManifest?.status.channels ?? [], [packageManifest]);
2728
const [isChannelSelectOpen, setIsChannelSelectOpen] = useState(false);
2829
const { setDeprecatedChannel } = useDeprecatedOperatorWarnings();
@@ -63,7 +64,7 @@ export const OperatorChannelSelect: FC<OperatorChannelSelectProps> = ({
6364
'deprecation',
6465
),
6566
);
66-
}, [selectedChannel, channels, setDeprecatedChannel]);
67+
}, [selectedChannel, channels, setDeprecatedChannel, setQueryArgument]);
6768

6869
return (
6970
<>
@@ -113,6 +114,7 @@ export const OperatorVersionSelect: FC<OperatorVersionSelectProps> = ({
113114
showVersionAlert = false,
114115
}) => {
115116
const { t } = useTranslation();
117+
const { setQueryArgument } = useQueryParamsMutator();
116118
const { setDeprecatedVersion } = useDeprecatedOperatorWarnings();
117119
const [isVersionSelectOpen, setIsVersionSelectOpen] = useState(false);
118120
const [defaultVersionForChannel, setDefaultVersionForChannel] = useState('-');
@@ -163,7 +165,7 @@ export const OperatorVersionSelect: FC<OperatorVersionSelectProps> = ({
163165
'deprecation',
164166
),
165167
);
166-
}, [selectedUpdateVersion, selectedChannelVersions, setDeprecatedVersion]);
168+
}, [selectedUpdateVersion, selectedChannelVersions, setDeprecatedVersion, setQueryArgument]);
167169

168170
return (
169171
<>

frontend/packages/operator-lifecycle-manager/src/components/subscription.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import {
4444
ResourceSummary,
4545
SectionHeading,
4646
} from '@console/internal/components/utils';
47-
import { removeQueryArgument } from '@console/internal/components/utils/router';
47+
import { useQueryParamsMutator } from '@console/internal/components/utils/router';
4848
import {
4949
k8sGet,
5050
k8sKill,
@@ -419,6 +419,7 @@ export const SubscriptionDetails: FC<SubscriptionDetailsProps> = ({
419419
subscriptions = [],
420420
}) => {
421421
const { t } = useTranslation();
422+
const { removeQueryArgument } = useQueryParamsMutator();
422423
const { source, sourceNamespace } = obj?.spec ?? {};
423424
const catalogHealth = obj?.status?.catalogHealth?.find(
424425
(ch) => ch.catalogSourceRef.name === source,

frontend/packages/topology/src/__tests__/TopologyPage.spec.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { configure, render, screen } from '@testing-library/react';
22
import * as Router from 'react-router-dom-v5-compat';
3+
import * as RouterUtils from '@console/internal/components/utils/router';
34
import { useQueryParams, useUserSettingsCompatibility } from '@console/shared/src';
45
import { TopologyPage } from '../components/page/TopologyPage';
56
import { TopologyViewType } from '../topology-types';
@@ -41,6 +42,11 @@ jest.mock('react-router-dom-v5-compat', () => ({
4142
useParams: jest.fn(),
4243
}));
4344

45+
jest.mock('@console/internal/components/utils/router', () => ({
46+
...jest.requireActual('@console/internal/components/utils/router'),
47+
useQueryParamsMutator: jest.fn(),
48+
}));
49+
4450
jest.mock('../filters/FilterProvider', () => ({
4551
...jest.requireActual('../filters/FilterProvider'),
4652
FilterProvider: 'FilterProvider',
@@ -66,6 +72,17 @@ describe('TopologyPage view logic', () => {
6672
beforeEach(() => {
6773
jest.clearAllMocks();
6874
(Router.useParams as jest.Mock).mockReturnValue({ name: 'default' });
75+
76+
// Mock useQueryParamsMutator
77+
(RouterUtils.useQueryParamsMutator as jest.Mock).mockReturnValue({
78+
getQueryArgument: jest.fn(),
79+
setQueryArgument: jest.fn(),
80+
setQueryArguments: jest.fn(),
81+
setAllQueryArguments: jest.fn(),
82+
removeQueryArgument: jest.fn(),
83+
removeQueryArguments: jest.fn(),
84+
setOrRemoveQueryArgument: jest.fn(),
85+
});
6986
});
7087

7188
it('should default to graph view', () => {

0 commit comments

Comments
 (0)