diff --git a/frontend/packages/console-app/locales/en/console-app.json b/frontend/packages/console-app/locales/en/console-app.json index 57bb8ade81d..79085ca94e0 100644 --- a/frontend/packages/console-app/locales/en/console-app.json +++ b/frontend/packages/console-app/locales/en/console-app.json @@ -199,7 +199,6 @@ "Whether or not the plugin might have violated the Console Content Security Policy.": "Whether or not the plugin might have violated the Console Content Security Policy.", "Backend Service": "Backend Service", "Proxy Services": "Proxy Services", - "Start Job": "Start Job", "Add Health Checks": "Add Health Checks", "Edit Health Checks": "Edit Health Checks", "Add HorizontalPodAutoscaler": "Add HorizontalPodAutoscaler", @@ -214,6 +213,7 @@ "Edit tolerations": "Edit tolerations", "Edit taints": "Edit taints", "Add storage": "Add storage", + "Start Job": "Start Job", "Edit update strategy": "Edit update strategy", "Resume rollouts": "Resume rollouts", "Pause rollouts": "Pause rollouts", diff --git a/frontend/packages/console-app/src/actions/creators/cronjob-factory.ts b/frontend/packages/console-app/src/actions/creators/cronjob-factory.ts deleted file mode 100644 index d28b1877307..00000000000 --- a/frontend/packages/console-app/src/actions/creators/cronjob-factory.ts +++ /dev/null @@ -1,67 +0,0 @@ -import i18next from 'i18next'; -import { resourceObjPath } from '@console/internal/components/utils/resource-link'; -import { history } from '@console/internal/components/utils/router'; -import { JobModel } from '@console/internal/models'; -import { - K8sKind, - k8sCreate, - CronJobKind, - JobKind, - referenceFor, - K8sResourceCommon, -} from '@console/internal/module/k8s'; -import { ResourceActionFactory } from './types'; - -const startJob = (obj: CronJobKind): Promise => { - const reqPayload = { - apiVersion: 'batch/v1', - kind: 'Job', - metadata: { - name: `${obj.metadata?.name}-${Date.now()}`, - namespace: obj.metadata?.namespace, - annotations: obj.metadata?.annotations, - ownerReferences: [ - { - apiVersion: 'batch/v1', - controller: true, - kind: 'CronJob', - name: obj.metadata?.name, - uid: obj.metadata?.uid, - }, - ], - }, - spec: { - ...obj.spec.jobTemplate.spec, - }, - }; - - return k8sCreate(JobModel, reqPayload as K8sResourceCommon); -}; - -export const CronJobActionFactory: ResourceActionFactory = { - StartJob: (kind: K8sKind, obj: CronJobKind) => ({ - id: 'start-job', - label: i18next.t('console-app~Start Job'), - cta: () => { - startJob(obj) - .then((job) => { - const path = resourceObjPath(job, referenceFor(job)); - if (path) { - history.push(path); - } - }) - .catch((error) => { - // TODO: Show error in notification in the follow on tech-debt. - // eslint-disable-next-line no-console - console.error('Failed to start a Job.', error); - }); - }, - accessReview: { - group: kind.apiGroup, - resource: kind.plural, - name: obj.metadata?.name, - namespace: obj.metadata?.namespace, - verb: 'create', - }, - }), -}; diff --git a/frontend/packages/console-app/src/actions/hooks/types.ts b/frontend/packages/console-app/src/actions/hooks/types.ts index 83b3e81829a..54bdc6773e7 100644 --- a/frontend/packages/console-app/src/actions/hooks/types.ts +++ b/frontend/packages/console-app/src/actions/hooks/types.ts @@ -77,3 +77,7 @@ export enum BuildConfigActionCreator { StartBuild = 'StartBuild', StartLastRun = 'StartLastRun', } + +export enum CronJobActionCreator { + StartJob = 'StartJob', +} diff --git a/frontend/packages/console-app/src/actions/hooks/useCronJobActions.ts b/frontend/packages/console-app/src/actions/hooks/useCronJobActions.ts new file mode 100644 index 00000000000..289f975b36b --- /dev/null +++ b/frontend/packages/console-app/src/actions/hooks/useCronJobActions.ts @@ -0,0 +1,126 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; +import { Action } from '@console/dynamic-plugin-sdk'; +import { useDeepCompareMemoize } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks/useDeepCompareMemoize'; +import { resourceObjPath } from '@console/internal/components/utils/resource-link'; +import { JobModel } from '@console/internal/models'; +import { + k8sCreate, + CronJobKind, + JobKind, + referenceFor, + K8sResourceCommon, +} from '@console/internal/module/k8s'; +import { CronJobActionCreator } from './types'; + +const startJob = (obj: CronJobKind): Promise => { + const reqPayload = { + apiVersion: 'batch/v1', + kind: 'Job', + metadata: { + name: `${obj.metadata?.name}-${Date.now()}`, + namespace: obj.metadata?.namespace, + annotations: obj.metadata?.annotations, + ownerReferences: [ + { + apiVersion: 'batch/v1', + controller: true, + kind: 'CronJob', + name: obj.metadata?.name, + uid: obj.metadata?.uid, + }, + ], + }, + spec: { + ...obj.spec.jobTemplate.spec, + }, + }; + + return k8sCreate(JobModel, reqPayload as K8sResourceCommon); +}; + +const startJobAccessReview = (obj: CronJobKind) => ({ + group: 'batch', + resource: 'jobs', + name: obj.metadata?.name, + namespace: obj.metadata?.namespace, + verb: 'create' as const, +}); + +const getStartJobPath = (job: JobKind): string | null => { + const path = resourceObjPath(job, referenceFor(job)); + return path || null; +}; + +/** + * A React hook for retrieving actions related to a CronJob resource. + * + * @param {CronJobKind} obj - The specific CronJob resource instance for which to generate actions. + * @param {CronJobActionCreator[]} [filterActions] - Optional. If provided, the returned `actions` array will contain + * only the specified actions. If omitted, it will contain all CronJob actions. Invalid action creators in + * `filterActions` are ignored; the reduce logic in `memoizedFilterActions` checks each creator via `factory` and + * only includes actions where `typeof fn === 'function'`, so the returned array contains only valid actions. + * + * This hook is robust to inline arrays/objects for the `filterActions` argument, so you do not need to memoize or define + * the array outside your component. The actions will only update if the actual contents of `filterActions` change, not just the reference. + * + * @returns {Action[]} An array containing the generated action(s). + * + * @example + * // Getting all actions for CronJob resource + * const MyCronJobComponent = ({ obj }) => { + * const actions = useCronJobActions(obj); + * return ; + * }; + */ +export const useCronJobActions = ( + obj: CronJobKind, + filterActions?: CronJobActionCreator[], +): Action[] => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const memoizedFilterActions = useDeepCompareMemoize(filterActions); + + const factory = useMemo( + () => ({ + [CronJobActionCreator.StartJob]: () => ({ + id: 'start-job', + label: t('console-app~Start Job'), + cta: () => { + startJob(obj) + .then((job) => { + const path = getStartJobPath(job); + if (path) { + navigate(path); + } + }) + .catch((error) => { + // TODO: Show error in notification in the follow on tech-debt. + // eslint-disable-next-line no-console + console.error('Failed to start a Job.', error); + }); + }, + accessReview: startJobAccessReview(obj), + }), + }), + [obj, navigate, t], + ); + + // filter and initialize requested actions or construct list of all CronJobActions + const actions = useMemo(() => { + if (memoizedFilterActions) { + return memoizedFilterActions.reduce((acc, creator) => { + const fn = factory[creator]; + if (typeof fn === 'function') { + acc.push(fn()); + } + return acc; + }, []); + } + return [factory[CronJobActionCreator.StartJob]()]; + }, [factory, memoizedFilterActions]); + + return actions; +}; diff --git a/frontend/packages/console-app/src/actions/providers/cronjob-provider.ts b/frontend/packages/console-app/src/actions/providers/cronjob-provider.ts index d4c4603a88c..62d5085f83f 100644 --- a/frontend/packages/console-app/src/actions/providers/cronjob-provider.ts +++ b/frontend/packages/console-app/src/actions/providers/cronjob-provider.ts @@ -1,18 +1,21 @@ import { useMemo } from 'react'; import { CronJobKind, referenceFor } from '@console/internal/module/k8s'; import { useK8sModel } from '@console/shared/src/hooks/useK8sModel'; -import { CronJobActionFactory } from '../creators/cronjob-factory'; import { useCommonResourceActions } from '../hooks/useCommonResourceActions'; +import { useCronJobActions } from '../hooks/useCronJobActions'; import { usePDBActions } from '../hooks/usePDBActions'; export const useCronJobActionsProvider = (resource: CronJobKind) => { const [kindObj, inFlight] = useK8sModel(referenceFor(resource)); const [pdbActions] = usePDBActions(kindObj, resource); + const cronJobActions = useCronJobActions(resource); const commonActions = useCommonResourceActions(kindObj, resource); - const actions = useMemo( - () => [CronJobActionFactory.StartJob(kindObj, resource), ...pdbActions, ...commonActions], - [kindObj, pdbActions, resource, commonActions], - ); + + const actions = useMemo(() => [...cronJobActions, ...pdbActions, ...commonActions], [ + cronJobActions, + pdbActions, + commonActions, + ]); return [actions, !inFlight, undefined]; }; diff --git a/frontend/packages/console-app/src/components/cluster-configuration/ClusterConfigurationPage.tsx b/frontend/packages/console-app/src/components/cluster-configuration/ClusterConfigurationPage.tsx index 9f4619ec12e..2f2a5c9fe58 100644 --- a/frontend/packages/console-app/src/components/cluster-configuration/ClusterConfigurationPage.tsx +++ b/frontend/packages/console-app/src/components/cluster-configuration/ClusterConfigurationPage.tsx @@ -15,8 +15,7 @@ import { import { ExclamationTriangleIcon } from '@patternfly/react-icons'; import { LockIcon } from '@patternfly/react-icons/dist/esm/icons/lock-icon'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom-v5-compat'; -import { history } from '@console/internal/components/utils/router'; +import { useParams, useNavigate } from 'react-router-dom-v5-compat'; import { LoadingBox } from '@console/internal/components/utils/status-box'; import { DocumentTitle } from '@console/shared/src/components/document-title/DocumentTitle'; import { PageHeading } from '@console/shared/src/components/heading/PageHeading'; @@ -32,6 +31,7 @@ import './ClusterConfigurationPage.scss'; const ClusterConfigurationPage: FC = () => { const { t } = useTranslation(); const params = useParams(); + const navigate = useNavigate(); const initialGroupId = params.group || 'general'; const [activeTabId, setActiveTabId] = useState(initialGroupId); @@ -42,7 +42,7 @@ const ClusterConfigurationPage: FC = () => { event.preventDefault(); setActiveTabId(newGroupId); const path = `/cluster-configuration/${newGroupId}`; - history.replace(path); + navigate(path, { replace: true }); }; const [ diff --git a/frontend/packages/console-app/src/components/file-upload/file-upload-context.ts b/frontend/packages/console-app/src/components/file-upload/file-upload-context.ts index e79462b941b..bb7c02aba9b 100644 --- a/frontend/packages/console-app/src/components/file-upload/file-upload-context.ts +++ b/frontend/packages/console-app/src/components/file-upload/file-upload-context.ts @@ -2,6 +2,7 @@ import { createContext, useState, useMemo, useCallback } from 'react'; import { AlertVariant } from '@patternfly/react-core'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { FileUpload, isFileUpload, useResolvedExtensions } from '@console/dynamic-plugin-sdk'; import { useToast } from '@console/shared/src/components/toast'; import { useActiveNamespace } from '@console/shared/src/hooks/useActiveNamespace'; @@ -26,6 +27,7 @@ export const useValuesFileUploadContext = (): FileUploadContextType => { const [fileUploadExtensions, resolved] = useResolvedExtensions(isFileUpload); const toastContext = useToast(); const [namespace] = useActiveNamespace(); + const navigate = useNavigate(); const [file, setFile] = useState(undefined); const fileExtensions = useMemo( () => @@ -45,7 +47,10 @@ export const useValuesFileUploadContext = (): FileUploadContextType => { const requiredFileExtension = getRequiredFileUploadExtension(fileUploadExtensions, f.name); if (requiredFileExtension) { setFile(f); - requiredFileExtension.properties.handler(f, namespace); + const path = requiredFileExtension.properties.handler(f, namespace); + if (path) { + navigate(path); + } } else { toastContext.addToast({ variant: AlertVariant.warning, @@ -63,7 +68,7 @@ export const useValuesFileUploadContext = (): FileUploadContextType => { } } }, - [setFile, fileExtensions, t, namespace, toastContext, fileUploadExtensions], + [setFile, fileExtensions, t, namespace, toastContext, fileUploadExtensions, navigate], ); return { diff --git a/frontend/packages/console-app/src/components/lightspeed/Lightspeed.tsx b/frontend/packages/console-app/src/components/lightspeed/Lightspeed.tsx index 7f5ff93e211..66c5a897e35 100644 --- a/frontend/packages/console-app/src/components/lightspeed/Lightspeed.tsx +++ b/frontend/packages/console-app/src/components/lightspeed/Lightspeed.tsx @@ -12,9 +12,9 @@ import { Tooltip, } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { useHideLightspeed } from '@console/app/src/components/user-preferences/lightspeed/useHideLightspeed'; import { k8sGetResource } from '@console/dynamic-plugin-sdk/src/utils/k8s'; -import { history } from '@console/internal/components/utils/router'; import { ConsolePluginModel } from '@console/internal/models'; import { FLAGS } from '@console/shared/src/constants/common'; import { useFlag } from '@console/shared/src/hooks/flag'; @@ -42,6 +42,7 @@ export const lightspeedOperatorURL = const Lightspeed: FC = () => { const { t } = useTranslation(); + const navigate = useNavigate(); const [hideLightspeed] = useHideLightspeed(); const [isReady, setIsReady] = useState(false); const [lightspeedIsInstalled, setLightspeedIsInstalled] = useState(false); @@ -67,12 +68,12 @@ const Lightspeed: FC = () => { }; const onInstallClick = () => { setIsOpen(false); - history.push(lightspeedOperatorURL); + navigate(lightspeedOperatorURL); fireTelemetryEvent('Console capability LightspeedButton Get started button clicked'); }; const onDismissClick = () => { setIsOpen(false); - history.push( + navigate( '/user-preferences/general?spotlight=[data-test="console.hideLightspeedButton%20field"]', ); }; diff --git a/frontend/packages/console-app/src/components/pdb/PDBForm.tsx b/frontend/packages/console-app/src/components/pdb/PDBForm.tsx index ea5ec699402..77d64168c38 100644 --- a/frontend/packages/console-app/src/components/pdb/PDBForm.tsx +++ b/frontend/packages/console-app/src/components/pdb/PDBForm.tsx @@ -27,11 +27,11 @@ import { import i18next from 'i18next'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { MatchLabels } from '@console/dynamic-plugin-sdk/src/api/common-types'; import { ButtonBar } from '@console/internal/components/utils/button-bar'; import { FieldLevelHelp } from '@console/internal/components/utils/field-level-help'; import { resourcePathFromModel } from '@console/internal/components/utils/resource-link'; -import { history } from '@console/internal/components/utils/router'; import { SelectorInput } from '@console/internal/components/utils/selector-input'; import { k8sCreate } from '@console/internal/module/k8s'; import PaneBody from '@console/shared/src/components/layout/PaneBody'; @@ -70,6 +70,8 @@ const PDBForm: FC = ({ replicasCount, }) => { const { t } = useTranslation(); + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const initialFormValues = initialValuesFromK8sResource(formData); const [formValues, setFormValues] = useState(initialFormValues); const [error, setError] = useState(''); @@ -150,8 +152,9 @@ const PDBForm: FC = ({ return response .then(() => { setInProgress(false); - history.push( + navigate( resourcePathFromModel(PodDisruptionBudgetModel, formValues.name, formValues.namespace), + { replace: true }, ); }) .catch((err) => { @@ -301,7 +304,7 @@ const PDBForm: FC = ({ > {existingResource ? t('console-app~Save') : t('console-app~Create')} - diff --git a/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx b/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx index f04dc375eac..d59c11ee8b0 100644 --- a/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx +++ b/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx @@ -10,7 +10,7 @@ import { PageSection, } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom-v5-compat'; +import { useParams, useNavigate } from 'react-router-dom-v5-compat'; import { useResolvedExtensions, UserPreferenceGroup, @@ -18,7 +18,6 @@ import { UserPreferenceItem, isUserPreferenceItem, } from '@console/dynamic-plugin-sdk'; -import { history } from '@console/internal/components/utils/router'; import { LoadingBox } from '@console/internal/components/utils/status-box'; import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; import { DocumentTitle } from '@console/shared/src/components/document-title/DocumentTitle'; @@ -40,6 +39,7 @@ import './UserPreferencePage.scss'; const UserPreferencePage: FC = () => { // resources and calls to hooks const { t } = useTranslation(); + const navigate = useNavigate(); const userPreferenceGroupExtensions = useExtensions(isUserPreferenceGroup); const sortedUserPreferenceGroups = orderExtensionBasedOnInsertBeforeAndAfter< @@ -124,7 +124,7 @@ const UserPreferencePage: FC = () => { } event.preventDefault(); setActiveTabId(eventKey); - history.replace(`${USER_PREFERENCES_BASE_URL}/${eventKey}`); + navigate(`${USER_PREFERENCES_BASE_URL}/${eventKey}`, { replace: true }); }; const activeTab = sortedUserPreferenceGroups.find((group) => group.id === activeTabId)?.label; return ( diff --git a/frontend/packages/console-app/src/components/volume-snapshot/create-volume-snapshot/create-volume-snapshot.tsx b/frontend/packages/console-app/src/components/volume-snapshot/create-volume-snapshot/create-volume-snapshot.tsx index 3568ee5d7cc..75eb29d47cb 100644 --- a/frontend/packages/console-app/src/components/volume-snapshot/create-volume-snapshot/create-volume-snapshot.tsx +++ b/frontend/packages/console-app/src/components/volume-snapshot/create-volume-snapshot/create-volume-snapshot.tsx @@ -14,7 +14,7 @@ import { ContentVariants, } from '@patternfly/react-core'; import { Trans, useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom-v5-compat'; +import { useParams, useNavigate } from 'react-router-dom-v5-compat'; import { PVCStatusComponent } from '@console/internal/components/persistent-volume-claim'; import { getAccessModeOptions, @@ -30,7 +30,6 @@ import { ListDropdown } from '@console/internal/components/utils/list-dropdown'; import { PVCDropdown } from '@console/internal/components/utils/pvc-dropdown'; import { ResourceIcon } from '@console/internal/components/utils/resource-icon'; import { resourceObjPath } from '@console/internal/components/utils/resource-link'; -import { history } from '@console/internal/components/utils/router'; import { convertToBaseValue, humanizeBinaryBytes } from '@console/internal/components/utils/units'; import { PersistentVolumeClaimModel, @@ -160,6 +159,8 @@ const CreateSnapshotForm = (props: SnapshotResourceProps) => { const [handlePromise, inProgress, errorMessage] = usePromiseHandler(); const { t } = useTranslation(); + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const [selectedPVCName, setSelectedPVCName] = useState(pvcName); const [pvcObj, setPVCObj] = useState(null); const [snapshotName, setSnapshotName] = useState(`${pvcName || 'pvc'}-snapshot`); @@ -241,7 +242,7 @@ const CreateSnapshotForm = (props: SnapshotResourceProps) => { handlePromise(k8sCreate(VolumeSnapshotModel, snapshotTemplate)) .then((resource) => { - history.push(resourceObjPath(resource, referenceFor(resource))); + navigate(resourceObjPath(resource, referenceFor(resource))); }) .catch(() => {}); }; @@ -337,7 +338,7 @@ const CreateSnapshotForm = (props: SnapshotResourceProps) => { > {t('console-app~Create')} - diff --git a/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md b/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md index 7aa81cfc084..ca3a465fbd2 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md +++ b/frontend/packages/console-dynamic-plugin-sdk/CHANGELOG-core.md @@ -24,6 +24,7 @@ table in [Console dynamic plugins README](./README.md). - **Type breaking**: Fix inaccurate types in `console.topology/details/resource-link` and `console.topology/details/tab-section`. ([CONSOLE-4630], [#15893]) - **Type breaking**: Fix inaccurate types in `console.catalog/item-type`. ([CONSOLE-4402], [#14869]) +- **Type breaking**: Replace `LocationDescriptor` type from `history` package with `To` type from `react-router-dom-v5-compat` in `UseDeleteModal` hook ([CONSOLE-4990], [#15959]) - Add support for the updated `React.FC` type in `@types/react` version 18 ([CONSOLE-4630], [#15893]) - Make all Console-provided shared modules optional peer dependencies ([CONSOLE-5050], [#15934]) @@ -172,6 +173,7 @@ table in [Console dynamic plugins README](./README.md). [CONSOLE-4796]: https://issues.redhat.com/browse/CONSOLE-4796 [CONSOLE-4806]: https://issues.redhat.com/browse/CONSOLE-4806 [CONSOLE-4840]: https://issues.redhat.com/browse/CONSOLE-4840 +[CONSOLE-4990]: https://issues.redhat.com/browse/CONSOLE-4990 [CONSOLE-5039]: https://issues.redhat.com/browse/CONSOLE-5039 [CONSOLE-5050]: https://issues.redhat.com/browse/CONSOLE-5050 [OCPBUGS-19048]: https://issues.redhat.com/browse/OCPBUGS-19048 @@ -243,3 +245,4 @@ table in [Console dynamic plugins README](./README.md). [#15778]: https://github.com/openshift/console/pull/15778 [#15893]: https://github.com/openshift/console/pull/15893 [#15934]: https://github.com/openshift/console/pull/15934 +[#15959]: https://github.com/openshift/console/pull/15959 diff --git a/frontend/packages/console-dynamic-plugin-sdk/release-notes/4.22.md b/frontend/packages/console-dynamic-plugin-sdk/release-notes/4.22.md index 3949f31fae3..1a6d63f8b6b 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/release-notes/4.22.md +++ b/frontend/packages/console-dynamic-plugin-sdk/release-notes/4.22.md @@ -18,6 +18,32 @@ The `@openshift-console/plugin-shared` package has been removed from the Console corresponding npm package is deprecated. Plugins should remove this dependency from their `package.json` if present. +## Migration from `history` package to React Router types + +Console has migrated from using the `history` package to React Router's native types. This affects +the following public API types: + +- `UseDeleteModal` hook's `redirectTo` parameter type has changed from `LocationDescriptor` (from `history`) + to `To` (from `react-router-dom-v5-compat`) + +### Migration guide + +If your plugin uses the `useDeleteModal` hook and passes a `redirectTo` parameter, update your imports: + +```diff +- import { LocationDescriptor } from 'history'; ++ import { To } from 'react-router-dom-v5-compat'; + + const MyComponent = ({ resource }) => { +- const launchDeleteModal = useDeleteModal(resource, redirectTo as LocationDescriptor); ++ const launchDeleteModal = useDeleteModal(resource, redirectTo as To); + // ... + }; +``` + +The `To` type is compatible with the previous `LocationDescriptor` type (accepts strings and location +objects), so most plugins should only need to update their imports. + ## React 18 upgrade tips Console now uses React 18. The following guidance highlights common update considerations diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts index 99e5bebcc9c..d1947f1a6d8 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts @@ -10,9 +10,9 @@ import { QuickStartContextValues } from '@patternfly/quickstarts'; import { CodeEditorProps as PfCodeEditorProps } from '@patternfly/react-code-editor'; import { ButtonProps } from '@patternfly/react-core'; import { ICell, OnSelect, SortByDirection, TableGridBreakpoint } from '@patternfly/react-table'; -import { LocationDescriptor } from 'history'; import type { TFunction } from 'i18next'; import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import { To } from 'react-router-dom-v5-compat'; import type { ExtensionK8sGroupKindModel, K8sModel, @@ -774,7 +774,7 @@ export type UseAnnotationsModal = (resource: K8sResourceCommon) => () => void; export type UseDeleteModal = ( resource: K8sResourceCommon, - redirectTo?: LocationDescriptor, + redirectTo?: To, message?: JSX.Element, btnText?: ReactNode, deleteAllResources?: () => Promise, diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/file-upload.ts b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/file-upload.ts index d3c47f85037..d5b5a54eee0 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/file-upload.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/file-upload.ts @@ -17,4 +17,4 @@ export const isFileUpload = (e: Extension): e is FileUpload => e.type === 'conso // Support types -export type FileUploadHandler = (file: File, namespace: string) => void; +export type FileUploadHandler = (file: File, namespace: string) => string | void; diff --git a/frontend/packages/console-shared/src/components/actions/menu/ActionMenuItem.tsx b/frontend/packages/console-shared/src/components/actions/menu/ActionMenuItem.tsx index 0ba6efb11fc..47b7ddaaed5 100644 --- a/frontend/packages/console-shared/src/components/actions/menu/ActionMenuItem.tsx +++ b/frontend/packages/console-shared/src/components/actions/menu/ActionMenuItem.tsx @@ -4,9 +4,9 @@ import { DropdownItemProps, KeyTypes, MenuItem, Tooltip } from '@patternfly/reac import { css } from '@patternfly/react-styles'; import * as _ from 'lodash'; import { connect } from 'react-redux'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { Action, ImpersonateKind, impersonateStateToProps } from '@console/dynamic-plugin-sdk'; import { useAccessReview } from '@console/internal/components/utils/rbac'; -import { history } from '@console/internal/components/utils/router'; export type ActionMenuItemProps = { action: Action; @@ -24,6 +24,7 @@ const ActionItem: FC = ({ isAllowed, component, }) => { + const navigate = useNavigate(); const { label, icon, disabled, cta } = action; const { href, external } = cta as { href: string; external?: boolean }; const isDisabled = !isAllowed || disabled; @@ -36,13 +37,13 @@ const ActionItem: FC = ({ cta(); } else if (_.isObject(cta)) { if (!cta.external) { - history.push(cta.href); + navigate(cta.href); } } onClick && onClick(); event.stopPropagation(); }, - [cta, onClick], + [cta, onClick, navigate], ); const handleKeyDown = (event) => { diff --git a/frontend/packages/console-shared/src/components/catalog/CatalogTile.tsx b/frontend/packages/console-shared/src/components/catalog/CatalogTile.tsx index a3c2dd67d6c..9c917475fb8 100644 --- a/frontend/packages/console-shared/src/components/catalog/CatalogTile.tsx +++ b/frontend/packages/console-shared/src/components/catalog/CatalogTile.tsx @@ -7,8 +7,8 @@ import { import { Badge } from '@patternfly/react-core'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { CatalogItem } from '@console/dynamic-plugin-sdk/src/extensions'; -import { history } from '@console/internal/components/utils/router'; import { isModifiedEvent } from '../../utils'; import CatalogBadges from './CatalogBadges'; import { getIconProps } from './utils/catalog-utils'; @@ -25,6 +25,7 @@ type CatalogTileProps = { const CatalogTile: FC = ({ item, catalogTypes, onClick, href }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const { uid, name, title, provider, description, type, typeLabel, badges } = item; const vendor = provider ? t('console-shared~Provided by {{provider}}', { provider }) : null; const catalogType = _.find(catalogTypes, ['value', type]); @@ -46,7 +47,7 @@ const CatalogTile: FC = ({ item, catalogTypes, onClick, href } if (onClick) { onClick(item); } else if (href) { - history.push(href); + navigate(href); } }} href={href} diff --git a/frontend/packages/console-shared/src/components/catalog/catalog-view/CatalogView.tsx b/frontend/packages/console-shared/src/components/catalog/catalog-view/CatalogView.tsx index b8f5fec71c1..d327cb86764 100644 --- a/frontend/packages/console-shared/src/components/catalog/catalog-view/CatalogView.tsx +++ b/frontend/packages/console-shared/src/components/catalog/catalog-view/CatalogView.tsx @@ -2,6 +2,7 @@ import type { ReactNode, FC } from 'react'; import { useMemo, useState, useRef, useCallback, useEffect } from 'react'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { CatalogCategory } from '@console/dynamic-plugin-sdk/src'; import { CatalogItem } from '@console/dynamic-plugin-sdk/src/extensions'; import { isModalOpen } from '@console/internal/components/modals'; @@ -77,6 +78,7 @@ const CatalogView: FC = ({ sortFilterGroups, }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const queryParams = useQueryParams(); const activeCategoryId = queryParams.get(CatalogQueryParams.CATEGORY) ?? ALL_CATEGORY; const activeSearchKeyword = queryParams.get(CatalogQueryParams.KEYWORD) ?? ''; @@ -111,37 +113,49 @@ const CatalogView: FC = ({ const clearFilters = useCallback(() => { const params = new URLSearchParams(); catalogType && items.length > 0 && params.set('catalogType', catalogType); - setURLParams(params); + setURLParams(params, navigate); // Don't take focus if a modal was opened while the page was loading. if (!isModalOpen()) { catalogToolbarRef.current && catalogToolbarRef.current.focus({ preventScroll: true }); } - }, [catalogType, items.length]); + }, [catalogType, items.length, navigate]); - const handleCategoryChange = (categoryId: string) => { - updateURLParams(CatalogQueryParams.CATEGORY, categoryId); - }; + const handleCategoryChange = useCallback( + (categoryId: string) => { + updateURLParams(CatalogQueryParams.CATEGORY, categoryId, navigate); + }, + [navigate], + ); const handleFilterChange = useCallback( (filterType, id, value) => { const updatedFilters = _.set(activeFilters, [filterType, id, 'active'], value); - updateURLParams(filterType, getFilterSearchParam(updatedFilters[filterType])); + updateURLParams(filterType, getFilterSearchParam(updatedFilters[filterType]), navigate); }, - [activeFilters], + [activeFilters, navigate], ); - const handleSearchKeywordChange = useCallback((searchKeyword) => { - updateURLParams(CatalogQueryParams.KEYWORD, searchKeyword); - }, []); + const handleSearchKeywordChange = useCallback( + (searchKeyword) => { + updateURLParams(CatalogQueryParams.KEYWORD, searchKeyword, navigate); + }, + [navigate], + ); - const handleGroupingChange = useCallback((grouping) => { - updateURLParams(CatalogQueryParams.GROUPING, grouping); - }, []); + const handleGroupingChange = useCallback( + (grouping) => { + updateURLParams(CatalogQueryParams.GROUPING, grouping, navigate); + }, + [navigate], + ); - const handleSortOrderChange = useCallback((order) => { - updateURLParams(CatalogQueryParams.SORT_ORDER, order); - }, []); + const handleSortOrderChange = useCallback( + (order) => { + updateURLParams(CatalogQueryParams.SORT_ORDER, order, navigate); + }, + [navigate], + ); const handleShowAllToggle = useCallback((groupName) => { setFilterGroupsShowAll((showAll) => { diff --git a/frontend/packages/console-shared/src/components/catalog/utils/catalog-utils.tsx b/frontend/packages/console-shared/src/components/catalog/utils/catalog-utils.tsx index cdc4664199a..6db0901953c 100644 --- a/frontend/packages/console-shared/src/components/catalog/utils/catalog-utils.tsx +++ b/frontend/packages/console-shared/src/components/catalog/utils/catalog-utils.tsx @@ -7,7 +7,6 @@ import { CatalogItemMetadataProviderFunction, } from '@console/dynamic-plugin-sdk/src/extensions'; import { normalizeIconClass } from '@console/internal/components/catalog/catalog-item-icon'; -import { history } from '@console/internal/components/utils/router'; import catalogImg from '@console/internal/imgs/logos/catalog-icon.svg'; import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; import { CatalogSortOrder, CatalogType, CatalogTypeCounts } from './types'; @@ -297,14 +296,21 @@ export const getIconProps = (item: CatalogItem) => { return { iconImg: catalogImg, iconClass: null }; }; -export const setURLParams = (params: URLSearchParams) => { +export const setURLParams = ( + params: URLSearchParams, + navigate: (to: string, options?: { replace?: boolean }) => void, +) => { const url = new URL(window.location.href); const searchParams = `?${params.toString()}${url.hash}`; - history.replace(`${url.pathname}${searchParams}`); + navigate(`${url.pathname}${searchParams}`, { replace: true }); }; -export const updateURLParams = (paramName: string, value: string | string[]) => { +export const updateURLParams = ( + paramName: string, + value: string | string[], + navigate: (to: string, options?: { replace?: boolean }) => void, +) => { const params = new URLSearchParams(window.location.search); if (value) { @@ -312,7 +318,7 @@ export const updateURLParams = (paramName: string, value: string | string[]) => } else { params.delete(paramName); } - setURLParams(params); + setURLParams(params, navigate); }; export const getURLWithParams = (paramName: string, value: string | string[]): string => { diff --git a/frontend/packages/console-shared/src/components/dynamic-form/index.tsx b/frontend/packages/console-shared/src/components/dynamic-form/index.tsx index ca92d2019f9..9c84ed715d3 100644 --- a/frontend/packages/console-shared/src/components/dynamic-form/index.tsx +++ b/frontend/packages/console-shared/src/components/dynamic-form/index.tsx @@ -1,10 +1,11 @@ import type { FC } from 'react'; +import { useCallback } from 'react'; import { Accordion, ActionGroup, Button, Alert } from '@patternfly/react-core'; import Form, { FormProps } from '@rjsf/core'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { ErrorBoundaryFallbackProps } from '@console/dynamic-plugin-sdk'; -import { history } from '@console/internal/components/utils/router'; import { ErrorBoundary } from '@console/shared/src/components/error'; import { K8S_UI_SCHEMA } from './const'; import defaultFields from './fields'; @@ -41,6 +42,8 @@ export const DynamicForm: FC = ({ ...restProps }) => { const { t } = useTranslation(); + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const schemaErrors = getSchemaErrors(schema); // IF the top level schema is unsupported, don't render a form at all. if (schemaErrors.length) { @@ -111,7 +114,7 @@ export const DynamicForm: FC = ({ - diff --git a/frontend/packages/console-shared/src/components/error/error-boundary.tsx b/frontend/packages/console-shared/src/components/error/error-boundary.tsx index 428f635bc8c..3d405280ace 100644 --- a/frontend/packages/console-shared/src/components/error/error-boundary.tsx +++ b/frontend/packages/console-shared/src/components/error/error-boundary.tsx @@ -1,11 +1,12 @@ import type { ComponentType, ReactNode, FC } from 'react'; import { Component } from 'react'; +import { useLocation } from 'react-router-dom-v5-compat'; import { ErrorBoundaryFallbackProps } from '@console/dynamic-plugin-sdk'; -import { history } from '@console/internal/components/utils/router'; type ErrorBoundaryProps = { FallbackComponent?: ComponentType; children?: ReactNode; + locationPathname?: string; }; /** Needed for tests -- should not be imported by application logic */ @@ -17,9 +18,7 @@ export type ErrorBoundaryState = { const DefaultFallback: FC = () =>
; -class ErrorBoundary extends Component { - unlisten: () => void = () => {}; - +class ErrorBoundaryInner extends Component { readonly defaultState: ErrorBoundaryState = { hasError: false, error: { @@ -37,15 +36,16 @@ class ErrorBoundary extends Component { this.state = this.defaultState; } - componentDidMount() { - this.unlisten = history.listen(() => { - // reset state to default when location changes + componentDidUpdate(prevProps: ErrorBoundaryProps) { + // Reset error state when location changes + if ( + this.state.hasError && + prevProps.locationPathname && + this.props.locationPathname !== prevProps.locationPathname + ) { + // eslint-disable-next-line react/no-did-update-set-state this.setState(this.defaultState); - }); - } - - componentWillUnmount() { - this.unlisten(); + } } componentDidCatch(error, errorInfo) { @@ -75,4 +75,15 @@ class ErrorBoundary extends Component { } } +// Functional wrapper to handle location changes +const ErrorBoundary: FC = ({ children, FallbackComponent }) => { + const location = useLocation(); + + return ( + + {children} + + ); +}; + export default ErrorBoundary; diff --git a/frontend/packages/console-shared/src/components/modals/DeleteResourceModal.tsx b/frontend/packages/console-shared/src/components/modals/DeleteResourceModal.tsx index 2d4112d7c2a..3615c79d492 100644 --- a/frontend/packages/console-shared/src/components/modals/DeleteResourceModal.tsx +++ b/frontend/packages/console-shared/src/components/modals/DeleteResourceModal.tsx @@ -2,13 +2,13 @@ import type { FC } from 'react'; import { TextInputTypes } from '@patternfly/react-core'; import { Formik, FormikProps, FormikValues } from 'formik'; import { useTranslation, Trans } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { createModalLauncher, ModalTitle, ModalBody, ModalSubmitFooter, } from '@console/internal/components/factory/modal'; -import { history } from '@console/internal/components/utils/router'; import { K8sResourceKind } from '@console/internal/module/k8s'; import { usePromiseHandler } from '../../hooks/promise-handler'; import { InputField } from '../formik-fields'; @@ -73,6 +73,7 @@ const DeleteResourceForm: FC & DeleteResourceModalProp const DeleteResourceModal: FC = (props) => { const [handlePromise] = usePromiseHandler(); + const navigate = useNavigate(); const handleSubmit = (values: FormikValues, actions) => { const { onSubmit, close, redirect } = props; @@ -82,7 +83,7 @@ const DeleteResourceModal: FC = (props) => { handlePromise(onSubmit(values)) .then(() => { close(); - redirect && history.push(redirect); + redirect && navigate(redirect); }) .catch((errorMessage) => { actions.setStatus({ submitError: errorMessage }); diff --git a/frontend/packages/console-shared/src/components/multi-tab-list/MultiTabListPage.tsx b/frontend/packages/console-shared/src/components/multi-tab-list/MultiTabListPage.tsx index 6ac5ec7c1cc..cd7b9b8bde8 100644 --- a/frontend/packages/console-shared/src/components/multi-tab-list/MultiTabListPage.tsx +++ b/frontend/packages/console-shared/src/components/multi-tab-list/MultiTabListPage.tsx @@ -2,9 +2,8 @@ import type { ReactNode, FC } from 'react'; import { ActionListItem, Button } from '@patternfly/react-core'; import { SimpleDropdown } from '@patternfly/react-templates'; import { useTranslation } from 'react-i18next'; -import { useParams, Link } from 'react-router-dom-v5-compat'; +import { useParams, Link, useNavigate } from 'react-router-dom-v5-compat'; import { HorizontalNav, Page } from '@console/internal/components/utils/horizontal-nav'; -import { history } from '@console/internal/components/utils/router'; import { referenceForModel } from '@console/internal/module/k8s'; import { PageHeading } from '@console/shared/src/components/heading/PageHeading'; import { PageTitleContext } from '../pagetitle/PageTitleContext'; @@ -28,6 +27,7 @@ const MultiTabListPage: FC = ({ telemetryPrefix, }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const { ns } = useParams(); const onSelectCreateAction = (actionName: string): void => { const selectedMenuItem: MenuAction = menuActions[actionName]; @@ -41,7 +41,7 @@ const MultiTabListPage: FC = ({ url = selectedMenuItem.onSelection(actionName, selectedMenuItem, url); } if (url) { - history.push(url); + navigate(url); } }; diff --git a/frontend/packages/console-shared/src/components/status/LinkStatus.tsx b/frontend/packages/console-shared/src/components/status/LinkStatus.tsx index 89d1e6e2e4f..9bc0b5483ce 100644 --- a/frontend/packages/console-shared/src/components/status/LinkStatus.tsx +++ b/frontend/packages/console-shared/src/components/status/LinkStatus.tsx @@ -1,6 +1,5 @@ import type { FC, ComponentProps } from 'react'; -import * as History from 'history'; -import { Link } from 'react-router-dom-v5-compat'; +import { Link, To } from 'react-router-dom-v5-compat'; import { StatusIconAndText } from '@console/dynamic-plugin-sdk'; const LinkStatus: FC = ({ linkTitle, linkTo, ...other }) => @@ -14,7 +13,7 @@ const LinkStatus: FC = ({ linkTitle, linkTo, ...other }) => type LinkStatusProps = ComponentProps & { linkTitle?: string; - linkTo?: History.LocationDescriptor; + linkTo?: To; }; export default LinkStatus; diff --git a/frontend/packages/dev-console/src/components/add/AddCardItem.tsx b/frontend/packages/dev-console/src/components/add/AddCardItem.tsx index 0d3f3b1683e..ab2006b7750 100644 --- a/frontend/packages/dev-console/src/components/add/AddCardItem.tsx +++ b/frontend/packages/dev-console/src/components/add/AddCardItem.tsx @@ -1,6 +1,7 @@ import type { FC, SyntheticEvent } from 'react'; import { isValidElement } from 'react'; import { SimpleListItem, Title, Content } from '@patternfly/react-core'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { ResolvedExtension, AddAction } from '@console/dynamic-plugin-sdk'; import { useToast } from '@console/shared/src'; import { useTelemetry } from '@console/shared/src/hooks/useTelemetry'; @@ -19,6 +20,7 @@ const AddCardItem: FC = ({ }, namespace, }) => { + const navigate = useNavigate(); const fireTelemetryEvent = useTelemetry(); const [showDetails] = useShowAddCardItemDetails(); const toast = useToast(); @@ -58,7 +60,7 @@ const AddCardItem: FC = ({ name: label, }); if (href) { - navigateTo(e, resolvedHref(href, namespace)); + navigateTo(e, resolvedHref(href, namespace), navigate); } else if (callback) { callback({ namespace, toast }); } diff --git a/frontend/packages/dev-console/src/components/buildconfig/EditBuildConfig.tsx b/frontend/packages/dev-console/src/components/buildconfig/EditBuildConfig.tsx index 417161ffc43..57e1c30fcd3 100644 --- a/frontend/packages/dev-console/src/components/buildconfig/EditBuildConfig.tsx +++ b/frontend/packages/dev-console/src/components/buildconfig/EditBuildConfig.tsx @@ -1,8 +1,9 @@ import type { FC } from 'react'; -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { Formik, FormikHelpers } from 'formik'; import { useTranslation } from 'react-i18next'; -import { history, resourcePathFromModel } from '@console/internal/components/utils'; +import { useNavigate } from 'react-router-dom-v5-compat'; +import { resourcePathFromModel } from '@console/internal/components/utils'; import { k8sCreate, k8sUpdate } from '@console/internal/module/k8s'; import { EditorType } from '@console/shared/src/components/synced-editor/editor-toggle'; import { safeJSToYAML, safeYAMLToJS } from '@console/shared/src/utils/yaml'; @@ -29,6 +30,7 @@ const EditBuildConfig: FC = ({ buildConfig: watchedBuildConfig, }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const [initialValues] = useState(() => { const values = convertBuildConfigToFormData(watchedBuildConfig); @@ -68,7 +70,7 @@ const EditBuildConfig: FC = ({ ? await k8sCreate(BuildConfigModel, changedBuildConfig) : await k8sUpdate(BuildConfigModel, changedBuildConfig, namespace, name); - history.push( + navigate( resourcePathFromModel( BuildConfigModel, updatedBuildConfig.metadata.name, @@ -80,7 +82,7 @@ const EditBuildConfig: FC = ({ } }; - const handleCancel = () => history.goBack(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); return ( ({ ...jest.requireActual('react-router-dom-v5-compat'), useParams: jest.fn(), + useNavigate: jest.fn(() => jest.fn()), })); const useK8sWatchResourceMock = useK8sWatchResource as jest.Mock; diff --git a/frontend/packages/dev-console/src/components/deployments/EditDeployment.tsx b/frontend/packages/dev-console/src/components/deployments/EditDeployment.tsx index ac6ab05d4e4..15d343bc8c6 100644 --- a/frontend/packages/dev-console/src/components/deployments/EditDeployment.tsx +++ b/frontend/packages/dev-console/src/components/deployments/EditDeployment.tsx @@ -1,10 +1,10 @@ import type { FC } from 'react'; -import { useRef } from 'react'; +import { useRef, useCallback } from 'react'; import { FormikBag, Formik } from 'formik'; import { safeLoad } from 'js-yaml'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { k8sCreateResource, k8sUpdateResource } from '@console/dynamic-plugin-sdk/src/utils/k8s'; -import { history } from '@console/internal/components/utils'; import { DeploymentConfigModel, DeploymentModel } from '@console/internal/models'; import { K8sResourceKind } from '@console/internal/module/k8s'; import { EditorType } from '@console/shared/src/components/synced-editor/editor-toggle'; @@ -25,6 +25,7 @@ export interface EditDeploymentProps { const EditDeployment: FC = ({ heading, resource, namespace, name }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const isNew = !name; const initialValues = useRef({ @@ -90,14 +91,14 @@ const EditDeployment: FC = ({ heading, resource, namespace, }), }); } - history.push(`/k8s/ns/${namespace}/${model.plural}/${res.metadata.name}`); + navigate(`/k8s/ns/${namespace}/${model.plural}/${res.metadata.name}`); }) .catch((err) => { actions.setStatus({ submitSuccess: '', submitError: err.message }); }); }; - const handleCancel = () => history.goBack(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); return ( = ({ appName, resources: appResources, }) => { + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const { t } = useTranslation(); const [perspective] = useActivePerspective(); const perspectiveExtensions = usePerspectives(); @@ -91,7 +93,7 @@ const EditApplication: FC = ({ return updateResources(values) .then(() => { actions.setStatus({ submitError: '' }); - handleRedirect(namespace, perspective, perspectiveExtensions); + return handleRedirect(namespace, perspective, perspectiveExtensions, navigate); }) .catch((err) => { actions.setStatus({ submitError: err.message }); @@ -151,7 +153,7 @@ const EditApplication: FC = ({ {renderForm} diff --git a/frontend/packages/dev-console/src/components/health-checks/AddHealthChecks.tsx b/frontend/packages/dev-console/src/components/health-checks/AddHealthChecks.tsx index 131eb86e8c5..ee7ca0a0ac0 100644 --- a/frontend/packages/dev-console/src/components/health-checks/AddHealthChecks.tsx +++ b/frontend/packages/dev-console/src/components/health-checks/AddHealthChecks.tsx @@ -4,11 +4,11 @@ import { Form } from '@patternfly/react-core'; import { FormikProps, FormikValues } from 'formik'; import * as _ from 'lodash'; import { useTranslation, Trans } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { ContainerSelect, documentationURLs, getDocumentationURL, - history, isManaged, ResourceLink, } from '@console/internal/components/utils'; @@ -44,6 +44,7 @@ const AddHealthChecks: FC & AddHealthChecksProps> = ({ }) => { const viewOnly = useViewOnlyAccess(resource); const { t } = useTranslation(); + const navigate = useNavigate(); const [currentKey, setCurrentKey] = useState(currentContainer); const containers = resource?.spec?.template?.spec?.containers; const healthCheckAdded = _.every( @@ -72,8 +73,9 @@ const AddHealthChecks: FC & AddHealthChecksProps> = ({ setCurrentKey(containerName); setFieldValue('containerName', containerName); setFieldValue('healthChecks', getHealthChecksData(resource, containerIndex)); - history.replace( + navigate( `/k8s/ns/${namespace}/${resourceKind}/${name}/containers/${containerName}/health-checks`, + { replace: true }, ); }; diff --git a/frontend/packages/dev-console/src/components/health-checks/AddHealthChecksForm.tsx b/frontend/packages/dev-console/src/components/health-checks/AddHealthChecksForm.tsx index ced8be554d6..a119f4c993f 100644 --- a/frontend/packages/dev-console/src/components/health-checks/AddHealthChecksForm.tsx +++ b/frontend/packages/dev-console/src/components/health-checks/AddHealthChecksForm.tsx @@ -1,9 +1,11 @@ import type { FC } from 'react'; +import { useCallback } from 'react'; import { Formik } from 'formik'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import * as yup from 'yup'; -import { FirehoseResult, LoadingBox, StatusBox, history } from '@console/internal/components/utils'; +import { FirehoseResult, LoadingBox, StatusBox } from '@console/internal/components/utils'; import { K8sResourceKind, k8sUpdate, modelFor, referenceFor } from '@console/internal/module/k8s'; import { getResourcesType } from '../edit-application/edit-application-utils'; import AddHealthChecks from './AddHealthChecks'; @@ -17,6 +19,8 @@ type AddHealthChecksFormProps = { }; const AddHealthChecksForm: FC = ({ resource, currentContainer }) => { + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const { t } = useTranslation(); if (!resource.loaded && _.isEmpty(resource.loadError)) { return ; @@ -41,7 +45,7 @@ const AddHealthChecksForm: FC = ({ resource, currentCo return k8sUpdate(modelFor(referenceFor(resource.data)), updatedResource) .then(() => { actions.setStatus({ error: '' }); - history.goBack(); + navigate(-1); }) .catch((err) => { actions.setStatus({ errors: err }); @@ -64,7 +68,7 @@ const AddHealthChecksForm: FC = ({ resource, currentCo healthChecks: healthChecksProbesValidationSchema(t), })} onSubmit={handleSubmit} - onReset={history.goBack} + onReset={handleCancel} > {(formikProps) => ( = ({ existingHPA, targetResource }) => { + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const { t } = useTranslation(); const initialValues: HPAFormValues = { showCanUseYAMLMessage: true, @@ -46,7 +49,7 @@ const HPAFormikForm: FC = ({ existingHPA, targetResource }) ) => Promise = existingHPA ? k8sUpdate : k8sCreate; return method(HorizontalPodAutoscalerModel, hpa) .then(() => { - history.goBack(); + navigate(-1); }) .catch((error) => { helpers.setStatus({ @@ -59,7 +62,7 @@ const HPAFormikForm: FC = ({ existingHPA, targetResource }) {(props: FormikProps) => ( diff --git a/frontend/packages/dev-console/src/components/import/DeployImage.tsx b/frontend/packages/dev-console/src/components/import/DeployImage.tsx index dbf91d95e50..b0e13503ad0 100644 --- a/frontend/packages/dev-console/src/components/import/DeployImage.tsx +++ b/frontend/packages/dev-console/src/components/import/DeployImage.tsx @@ -1,9 +1,10 @@ import type { FC } from 'react'; +import { useCallback } from 'react'; import { Formik, FormikHelpers } from 'formik'; import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { ImportStrategy } from '@console/git-service/src/types'; -import { history } from '@console/internal/components/utils'; import { K8sResourceKind } from '@console/internal/module/k8s'; import { getActiveApplication } from '@console/internal/reducers/ui'; import { RootState } from '@console/internal/redux'; @@ -33,6 +34,8 @@ type Props = DeployImageProps & StateProps; const DeployImage: FC = ({ namespace, projects, activeApplication, contextualSource }) => { const postFormCallback = useResourceConnectionHandler(); const { t } = useTranslation(); + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const initialValues: DeployImageFormData = { project: { name: namespace || '', @@ -181,7 +184,7 @@ const DeployImage: FC = ({ namespace, projects, activeApplication, contex .then((res) => { const selectId = filterDeployedResources(res)[0]?.metadata?.uid; - history.push(`/topology/ns/${projectName}${selectId ? `?selectId=${selectId}` : ''}`); + navigate(`/topology/ns/${projectName}${selectId ? `?selectId=${selectId}` : ''}`); }) .catch((err) => { helpers.setStatus({ submitError: err.message }); @@ -192,7 +195,7 @@ const DeployImage: FC = ({ namespace, projects, activeApplication, contex {(formikProps) => } diff --git a/frontend/packages/dev-console/src/components/import/ImportForm.tsx b/frontend/packages/dev-console/src/components/import/ImportForm.tsx index bede633f593..7517bcb9824 100644 --- a/frontend/packages/dev-console/src/components/import/ImportForm.tsx +++ b/frontend/packages/dev-console/src/components/import/ImportForm.tsx @@ -1,12 +1,14 @@ import type { FC } from 'react'; +import { useCallback } from 'react'; import { ValidatedOptions, AlertVariant } from '@patternfly/react-core'; import { Formik, FormikProps } from 'formik'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { useActivePerspective } from '@console/dynamic-plugin-sdk'; import { GitProvider, ImportStrategy } from '@console/git-service/src'; -import { history, AsyncComponent, StatusBox } from '@console/internal/components/utils'; +import { AsyncComponent, StatusBox } from '@console/internal/components/utils'; import { RouteModel } from '@console/internal/models'; import { RouteKind } from '@console/internal/module/k8s'; import { getActiveApplication } from '@console/internal/reducers/ui'; @@ -75,6 +77,8 @@ const ImportForm: FC = ({ projects, }) => { const { t } = useTranslation(); + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const fireTelemetryEvent = useTelemetry(); const [perspective] = useActivePerspective(); const perspectiveExtensions = usePerspectives(); @@ -242,7 +246,13 @@ const ImportForm: FC = ({ fireTelemetryEvent('Git Import', getTelemetryImport(values)); - handleRedirect(projectName, perspective, perspectiveExtensions, redirectSearchParams); + return handleRedirect( + projectName, + perspective, + perspectiveExtensions, + navigate, + redirectSearchParams, + ); }) .catch((err) => { // eslint-disable-next-line no-console @@ -271,7 +281,7 @@ const ImportForm: FC = ({ {renderForm} diff --git a/frontend/packages/dev-console/src/components/import/ImportSamplePage.tsx b/frontend/packages/dev-console/src/components/import/ImportSamplePage.tsx index c24d1c45c26..f4f0f03af88 100644 --- a/frontend/packages/dev-console/src/components/import/ImportSamplePage.tsx +++ b/frontend/packages/dev-console/src/components/import/ImportSamplePage.tsx @@ -1,9 +1,9 @@ import type { FC } from 'react'; -import { useMemo } from 'react'; +import { useMemo, useCallback } from 'react'; import { Formik } from 'formik'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom-v5-compat'; -import { FirehoseResource, LoadingBox, history } from '@console/internal/components/utils'; +import { useParams, useNavigate } from 'react-router-dom-v5-compat'; +import { FirehoseResource, LoadingBox } from '@console/internal/components/utils'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { ImageStreamModel } from '@console/internal/models'; import { K8sResourceKind } from '@console/internal/module/k8s'; @@ -28,6 +28,8 @@ import ImportSampleForm from './ImportSampleForm'; const ImportSamplePage: FC = () => { const { t } = useTranslation(); + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const { ns: namespace, is: imageStreamName, isNs: imageStreamNamespace } = useParams(); const imageStreamResource: FirehoseResource = useMemo( @@ -118,7 +120,7 @@ const ImportSamplePage: FC = () => { return resourceActions .then(() => { - history.push(`/topology/ns/${namespace}`); + navigate(`/topology/ns/${namespace}`); }) .catch((err) => { actions.setStatus({ submitError: err.message }); @@ -132,7 +134,7 @@ const ImportSamplePage: FC = () => { {(formikProps) => } diff --git a/frontend/packages/dev-console/src/components/import/import-submit-utils.ts b/frontend/packages/dev-console/src/components/import/import-submit-utils.ts index 7e534bd6f4c..9fb31896fd6 100644 --- a/frontend/packages/dev-console/src/components/import/import-submit-utils.ts +++ b/frontend/packages/dev-console/src/components/import/import-submit-utils.ts @@ -1,9 +1,9 @@ import * as GitUrlParse from 'git-url-parse'; import * as _ from 'lodash'; +import { NavigateFunction } from 'react-router-dom-v5-compat'; import { Perspective, ConsoleTFunction } from '@console/dynamic-plugin-sdk'; import { GitProvider } from '@console/git-service/src'; import { SecretType } from '@console/internal/components/secrets/create-secret'; -import { history } from '@console/internal/components/utils'; import { BuildStrategyType } from '@console/internal/components/utils/build-utils'; import { ImageStreamModel, @@ -1005,15 +1005,16 @@ export const handleRedirect = async ( project: string, perspective: string, perspectiveExtensions: Perspective[], + navigate: NavigateFunction, searchParamOverrides?: URLSearchParams, ) => { const perspectiveData = perspectiveExtensions.find((item) => item.properties.id === perspective); const redirectURL = (await perspectiveData.properties.importRedirectURL())(project); if (searchParamOverrides) { - history.push(addSearchParamsToRelativeURL(redirectURL, searchParamOverrides)); + navigate(addSearchParamsToRelativeURL(redirectURL, searchParamOverrides)); } else { - history.push(redirectURL); + navigate(redirectURL); } }; diff --git a/frontend/packages/dev-console/src/components/import/jar/UploadJar.tsx b/frontend/packages/dev-console/src/components/import/jar/UploadJar.tsx index 5f978281db8..e27b2342e05 100644 --- a/frontend/packages/dev-console/src/components/import/jar/UploadJar.tsx +++ b/frontend/packages/dev-console/src/components/import/jar/UploadJar.tsx @@ -1,9 +1,10 @@ import type { FunctionComponent } from 'react'; +import { useCallback } from 'react'; import { Formik, FormikHelpers } from 'formik'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { WatchK8sResultsObject, useActivePerspective } from '@console/dynamic-plugin-sdk'; -import { history } from '@console/internal/components/utils'; import { K8sResourceKind } from '@console/internal/module/k8s'; import { ALL_APPLICATIONS_KEY, usePerspectives } from '@console/shared/src'; import { useResourceConnectionHandler } from '@console/shared/src/hooks/useResourceConnectionHandler'; @@ -32,6 +33,8 @@ const UploadJar: FunctionComponent = ({ forApplication, contextualSource, }) => { + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const postFormCallback = useResourceConnectionHandler(); const toastCallback = useUploadJarFormToast(); const { t } = useTranslation(); @@ -86,6 +89,7 @@ const UploadJar: FunctionComponent = ({ projectName, perspective, perspectiveExtensions, + navigate, new URLSearchParams({ selectId: filterDeployedResources(resp)[0]?.metadata?.uid, }), @@ -100,7 +104,7 @@ const UploadJar: FunctionComponent = ({ {(formikProps) => ( diff --git a/frontend/packages/dev-console/src/components/import/jar/__tests__/UploadJar.spec.tsx b/frontend/packages/dev-console/src/components/import/jar/__tests__/UploadJar.spec.tsx index e379bdf1258..e805d96ddb9 100644 --- a/frontend/packages/dev-console/src/components/import/jar/__tests__/UploadJar.spec.tsx +++ b/frontend/packages/dev-console/src/components/import/jar/__tests__/UploadJar.spec.tsx @@ -1,6 +1,7 @@ -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { ImageTag } from '@console/dev-console/src/utils/imagestream-utils'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; +import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import UploadJar from '../UploadJar'; jest.mock('formik', () => ({ @@ -124,7 +125,7 @@ describe('UploadJar', () => { it('Should render formik', () => { (useK8sWatchResource as jest.Mock).mockReturnValue([]); - render(); + renderWithProviders(); const formikText = screen.getByText(/Formik initialValues=/); expect(formikText).toBeInTheDocument(); diff --git a/frontend/packages/dev-console/src/components/import/jar/useUploadJarFormToast.ts b/frontend/packages/dev-console/src/components/import/jar/useUploadJarFormToast.ts index 1ba33a678dd..acac9ffe4d0 100644 --- a/frontend/packages/dev-console/src/components/import/jar/useUploadJarFormToast.ts +++ b/frontend/packages/dev-console/src/components/import/jar/useUploadJarFormToast.ts @@ -1,8 +1,9 @@ import { useMemo, useCallback } from 'react'; import { AlertVariant } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { WatchK8sResource } from '@console/dynamic-plugin-sdk'; -import { history, resourcePathFromModel } from '@console/internal/components/utils'; +import { resourcePathFromModel } from '@console/internal/components/utils'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { BuildConfigModel, BuildModel } from '@console/internal/models'; import { K8sResourceKind } from '@console/internal/module/k8s'; @@ -11,6 +12,7 @@ import { useActiveNamespace, useToast, getOwnedResources } from '@console/shared export const useUploadJarFormToast = () => { const toast = useToast(); const { t } = useTranslation(); + const navigate = useNavigate(); const [namespace] = useActiveNamespace(); const buildsResource: WatchK8sResource = useMemo( () => ({ @@ -42,11 +44,11 @@ export const useUploadJarFormToast = () => { { dismiss: true, label: t('devconsole~View build logs'), - callback: () => history.push(link), + callback: () => navigate(link), }, ], }); }, - [builds, namespace, t, toast], + [builds, namespace, navigate, t, toast], ); }; diff --git a/frontend/packages/dev-console/src/components/import/serverless-function/AddServerlessFunction.tsx b/frontend/packages/dev-console/src/components/import/serverless-function/AddServerlessFunction.tsx index 3b221a2c8e1..80519105dff 100644 --- a/frontend/packages/dev-console/src/components/import/serverless-function/AddServerlessFunction.tsx +++ b/frontend/packages/dev-console/src/components/import/serverless-function/AddServerlessFunction.tsx @@ -1,15 +1,17 @@ import type { FC } from 'react'; +import { useCallback } from 'react'; import { ValidatedOptions } from '@patternfly/react-core'; import { Formik } from 'formik'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { useActivePerspective, WatchK8sResults, WatchK8sResultsObject, } from '@console/dynamic-plugin-sdk'; import { GitProvider } from '@console/git-service/src'; -import { LoadingBox, history } from '@console/internal/components/utils'; +import { LoadingBox } from '@console/internal/components/utils'; import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import { ImageStreamModel, ProjectModel } from '@console/internal/models'; import { K8sResourceKind } from '@console/internal/module/k8s'; @@ -40,6 +42,8 @@ type AddServerlessFunctionProps = { }; const AddServerlessFunction: FC = ({ namespace, forApplication }) => { + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const { t } = useTranslation(); const postFormCallback = useResourceConnectionHandler(); const [perspective] = useActivePerspective(); @@ -164,6 +168,7 @@ const AddServerlessFunction: FC = ({ namespace, forA projectName, perspective, perspectiveExtensions, + navigate, new URLSearchParams({ selectId }), ); }) @@ -177,7 +182,7 @@ const AddServerlessFunction: FC = ({ namespace, forA {(formikProps) => ( diff --git a/frontend/packages/dev-console/src/components/jar-file-upload/jar-file-upload-utils.ts b/frontend/packages/dev-console/src/components/jar-file-upload/jar-file-upload-utils.ts index 6f671051515..e2e7401fb60 100644 --- a/frontend/packages/dev-console/src/components/jar-file-upload/jar-file-upload-utils.ts +++ b/frontend/packages/dev-console/src/components/jar-file-upload/jar-file-upload-utils.ts @@ -1,5 +1,3 @@ -import { history } from '@console/internal/components/utils/router'; - -export const jarFileUploadHandler = (file: File, namespace: string) => { - history.push(`/upload-jar/ns/${namespace}`); +export const jarFileUploadHandler = (file: File, namespace: string): string => { + return `/upload-jar/ns/${namespace}`; }; diff --git a/frontend/packages/dev-console/src/components/monitoring/MonitoringPage.tsx b/frontend/packages/dev-console/src/components/monitoring/MonitoringPage.tsx index a5008349a72..c96061864ea 100644 --- a/frontend/packages/dev-console/src/components/monitoring/MonitoringPage.tsx +++ b/frontend/packages/dev-console/src/components/monitoring/MonitoringPage.tsx @@ -1,8 +1,8 @@ import type { FC } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom-v5-compat'; +import { useParams, useNavigate } from 'react-router-dom-v5-compat'; import { withStartGuide } from '@console/internal/components/start-guide'; -import { HorizontalNav, history } from '@console/internal/components/utils'; +import { HorizontalNav } from '@console/internal/components/utils'; import { ALL_NAMESPACES_KEY } from '@console/shared'; import { PageHeading } from '@console/shared/src/components/heading/PageHeading'; import { PageTitleContext } from '@console/shared/src/components/pagetitle/PageTitleContext'; @@ -12,9 +12,9 @@ import MonitoringEvents from './events/MonitoringEvents'; export const MONITORING_ALL_NS_PAGE_URI = '/dev-monitoring/all-namespaces'; -const handleNamespaceChange = (newNamespace: string): void => { +const handleNamespaceChange = (newNamespace: string, navigate: (url: string) => void): void => { if (newNamespace === ALL_NAMESPACES_KEY) { - history.push(MONITORING_ALL_NS_PAGE_URI); + navigate(MONITORING_ALL_NS_PAGE_URI); } }; @@ -58,11 +58,12 @@ export const PageContents: FC = () => { const PageContentsWithStartGuide = withStartGuide(PageContents); export const MonitoringPage = (props) => { + const navigate = useNavigate(); return ( handleNamespaceChange(newNamespace, navigate)} > diff --git a/frontend/packages/dev-console/src/components/project-access/ProjectAccess.tsx b/frontend/packages/dev-console/src/components/project-access/ProjectAccess.tsx index e373d02fa2f..5835588c577 100644 --- a/frontend/packages/dev-console/src/components/project-access/ProjectAccess.tsx +++ b/frontend/packages/dev-console/src/components/project-access/ProjectAccess.tsx @@ -3,11 +3,10 @@ import { Content, ContentVariants } from '@patternfly/react-core'; import { Formik } from 'formik'; import * as _ from 'lodash'; import { useTranslation, Trans } from 'react-i18next'; -import { Link } from 'react-router-dom-v5-compat'; +import { Link, useNavigate } from 'react-router-dom-v5-compat'; import { documentationURLs, getDocumentationURL, - history, isManaged, LoadingBox, StatusBox, @@ -43,6 +42,7 @@ const ProjectAccess: FC = ({ fullFormView, }) => { const { t } = useTranslation(); + const navigate = useNavigate(); if ((!roleBindings.loaded && _.isEmpty(roleBindings.loadError)) || !roles.loaded) { return ; } @@ -157,7 +157,7 @@ const ProjectAccess: FC = ({ {...formikProps} roles={roles.data} roleBindings={initialValues} - onCancel={fullFormView ? history.goBack : null} + onCancel={fullFormView ? () => navigate(-1) : null} /> )} diff --git a/frontend/packages/dev-console/src/components/project-access/__tests__/ProjectAccess.spec.tsx b/frontend/packages/dev-console/src/components/project-access/__tests__/ProjectAccess.spec.tsx index ee4417b167f..ce634269cbc 100644 --- a/frontend/packages/dev-console/src/components/project-access/__tests__/ProjectAccess.spec.tsx +++ b/frontend/packages/dev-console/src/components/project-access/__tests__/ProjectAccess.spec.tsx @@ -7,7 +7,6 @@ jest.mock('@console/internal/components/utils', () => ({ StatusBox: () => 'StatusBox', documentationURLs: { usingRBAC: 'rbac-url' }, getDocumentationURL: jest.fn(() => 'http://example.com/rbac'), - history: { goBack: jest.fn() }, isManaged: jest.fn(() => false), })); @@ -22,6 +21,7 @@ jest.mock('@patternfly/react-core', () => ({ jest.mock('react-router-dom-v5-compat', () => ({ Link: () => 'Link', + useNavigate: () => jest.fn(), })); jest.mock('@console/shared/src/components/document-title/DocumentTitle', () => ({ diff --git a/frontend/packages/dev-console/src/components/projects/details/ProjectDetailsPage.tsx b/frontend/packages/dev-console/src/components/projects/details/ProjectDetailsPage.tsx index 8a048c0b271..96177d8dde5 100644 --- a/frontend/packages/dev-console/src/components/projects/details/ProjectDetailsPage.tsx +++ b/frontend/packages/dev-console/src/components/projects/details/ProjectDetailsPage.tsx @@ -1,11 +1,11 @@ import type { FC } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom-v5-compat'; +import { useParams, useNavigate } from 'react-router-dom-v5-compat'; import { ProjectDashboard } from '@console/internal/components/dashboard/project-dashboard/project-dashboard'; import { DetailsPage } from '@console/internal/components/factory'; import { NamespaceDetails } from '@console/internal/components/namespace'; import { withStartGuide } from '@console/internal/components/start-guide'; -import { history, useAccessReview, Page } from '@console/internal/components/utils'; +import { useAccessReview, Page } from '@console/internal/components/utils'; import { ProjectModel, RoleBindingModel } from '@console/internal/models'; import { referenceForModel } from '@console/internal/module/k8s'; import LazyActionMenu from '@console/shared/src/components/actions/LazyActionMenu'; @@ -22,9 +22,9 @@ interface MonitoringPageProps { noProjectsAvailable?: boolean; } -const handleNamespaceChange = (newNamespace: string): void => { +const handleNamespaceChange = (newNamespace: string, navigate: (url: string) => void): void => { if (newNamespace === ALL_NAMESPACES_KEY) { - history.push(PROJECT_DETAILS_ALL_NS_PAGE_URI); + navigate(PROJECT_DETAILS_ALL_NS_PAGE_URI); } }; @@ -114,13 +114,14 @@ const PageContentsWithStartGuide = withStartGuide(PageConte export const ProjectDetailsPage: FC = (props) => { const { t } = useTranslation(); + const navigate = useNavigate(); return ( <> {t('devconsole~Project Details')} handleNamespaceChange(newNamespace, navigate)} > diff --git a/frontend/packages/dev-console/src/components/projects/details/__tests__/ProjectDetailsPage.spec.tsx b/frontend/packages/dev-console/src/components/projects/details/__tests__/ProjectDetailsPage.spec.tsx index 270ea56aec6..7ef037b9135 100644 --- a/frontend/packages/dev-console/src/components/projects/details/__tests__/ProjectDetailsPage.spec.tsx +++ b/frontend/packages/dev-console/src/components/projects/details/__tests__/ProjectDetailsPage.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react'; -import { useParams } from 'react-router-dom-v5-compat'; +import { useParams, MemoryRouter } from 'react-router-dom-v5-compat'; import { useAccessReview } from '@console/internal/components/utils/rbac'; import { ProjectDetailsPage, PageContents } from '../ProjectDetailsPage'; @@ -58,18 +58,18 @@ jest.mock('@console/shared/src/components/breadcrumbs/Breadcrumbs', () => ({ Breadcrumbs: () => 'Breadcrumbs', })); -jest.mock('../../../NamespacedPage', () => ({ - __esModule: true, - default: (props) => props.children, - NamespacedPageVariants: { light: 'light' }, -})); - jest.mock('../../CreateProjectListPage', () => ({ __esModule: true, default: () => 'CreateProjectListPage', CreateAProjectButton: () => 'CreateAProjectButton', })); +jest.mock('../../../NamespacedPage', () => ({ + __esModule: true, + default: (props) => props.children, + NamespacedPageVariants: { light: 'light' }, +})); + jest.mock('../../../project-access/ProjectAccessPage', () => ({ __esModule: true, default: () => 'ProjectAccessPage', @@ -97,7 +97,11 @@ describe('ProjectDetailsPage', () => { (useAccessReview as jest.Mock).mockReturnValue(true); (useParams as jest.Mock).mockReturnValue({}); - render(); + render( + + + , + ); expect(screen.getByText(/CreateProjectListPage/)).toBeInTheDocument(); }); @@ -106,7 +110,11 @@ describe('ProjectDetailsPage', () => { (useAccessReview as jest.Mock).mockReturnValue(true); (useParams as jest.Mock).mockReturnValue({ ns: 'test-project' }); - render(); + render( + + + , + ); expect(screen.getByText(/DetailsPage/)).toBeInTheDocument(); }); @@ -115,7 +123,11 @@ describe('ProjectDetailsPage', () => { (useAccessReview as jest.Mock).mockReturnValue(true); (useParams as jest.Mock).mockReturnValue({ ns: 'test-project' }); - render(); + render( + + + , + ); expect(screen.queryByText(/Breadcrumbs/)).not.toBeInTheDocument(); }); @@ -124,7 +136,11 @@ describe('ProjectDetailsPage', () => { (useAccessReview as jest.Mock).mockReturnValue(false); (useParams as jest.Mock).mockReturnValue({ ns: 'test-project' }); - render(); + render( + + + , + ); expect(screen.getByText(/DetailsPage/)).toBeInTheDocument(); }); diff --git a/frontend/packages/dev-console/src/utils/add-page-utils.ts b/frontend/packages/dev-console/src/utils/add-page-utils.ts index 0ec013789d7..f17ff2bd77f 100644 --- a/frontend/packages/dev-console/src/utils/add-page-utils.ts +++ b/frontend/packages/dev-console/src/utils/add-page-utils.ts @@ -1,5 +1,6 @@ +import type { SyntheticEvent } from 'react'; +import { NavigateFunction } from 'react-router-dom-v5-compat'; import { AddAction, AddActionGroup, ResolvedExtension } from '@console/dynamic-plugin-sdk'; -import { history } from '@console/internal/components/utils'; import { ALL_NAMESPACES_KEY } from '@console/shared'; import { AddGroup } from '../components/types'; @@ -35,12 +36,12 @@ export const getAddGroups = ( return populatedActionGroups.filter((group) => group.items.length); }; -export const navigateTo = (e: React.SyntheticEvent, url: string) => { - history.push(url); +export const navigateTo = (e: SyntheticEvent, url: string, navigate: NavigateFunction) => { + navigate(url); e.preventDefault(); }; -export const resolvedHref = (href: string, namespace: string): string => +export const resolvedHref = (href: string, namespace: string): string | null => href && namespace ? href.replace(/:namespace\b/g, namespace) : null; export const filterNamespaceScopedUrl = ( diff --git a/frontend/packages/helm-plugin/src/components/details-page/HelmReleaseDetailsPage.tsx b/frontend/packages/helm-plugin/src/components/details-page/HelmReleaseDetailsPage.tsx index 56aad1948fe..fdd88efcd1b 100644 --- a/frontend/packages/helm-plugin/src/components/details-page/HelmReleaseDetailsPage.tsx +++ b/frontend/packages/helm-plugin/src/components/details-page/HelmReleaseDetailsPage.tsx @@ -1,20 +1,22 @@ import type { FC } from 'react'; +import { useNavigate } from 'react-router-dom-v5-compat'; import NamespacedPage, { NamespacedPageVariants, } from '@console/dev-console/src/components/NamespacedPage'; -import { history } from '@console/internal/components/utils'; import { ALL_NAMESPACES_KEY } from '@console/shared'; import HelmReleaseDetails from './HelmReleaseDetails'; -const handleNamespaceChange = (newNamespace: string): void => { - if (newNamespace === ALL_NAMESPACES_KEY) { - history.push('/helm-releases/all-namespaces'); - } else { - history.push('/helm-releases/ns/:ns'); - } -}; - const HelmReleaseDetailsPage: FC = () => { + const navigate = useNavigate(); + + const handleNamespaceChange = (newNamespace: string): void => { + if (newNamespace === ALL_NAMESPACES_KEY) { + navigate('/helm-releases/all-namespaces'); + } else { + navigate(`/helm-releases/ns/${newNamespace}`); + } + }; + return ( = ({ showScopeType, existingRepoName, }) => { + const navigate = useNavigate(); const queryParams = useQueryParams(); const resourceKind: K8sResourceKindReference = queryParams.get('kind'); const isEditForm = !!existingRepoName; @@ -147,7 +149,7 @@ const CreateHelmChartRepository: FC = ({ hcr: HelmChartRepositoryRes.kind, }), }); - history.push(redirectURL); + navigate(redirectURL); }) .catch((err) => { actions.setStatus({ @@ -157,7 +159,7 @@ const CreateHelmChartRepository: FC = ({ }); }; - const handleCancel = () => history.goBack(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); if (isEditForm && hcrLoaded && !hcr) { return ; diff --git a/frontend/packages/helm-plugin/src/components/forms/HelmChartRepository/CreateHelmChartRepositoryPage.tsx b/frontend/packages/helm-plugin/src/components/forms/HelmChartRepository/CreateHelmChartRepositoryPage.tsx index bec9affe7b7..96f83a9117c 100644 --- a/frontend/packages/helm-plugin/src/components/forms/HelmChartRepository/CreateHelmChartRepositoryPage.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/HelmChartRepository/CreateHelmChartRepositoryPage.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom-v5-compat'; +import { useParams, useNavigate } from 'react-router-dom-v5-compat'; import NamespacedPage, { NamespacedPageVariants, } from '@console/dev-console/src/components/NamespacedPage'; @@ -13,13 +13,13 @@ import { HelmChartRepositoryModel, ProjectHelmChartRepositoryModel, } from '@console/helm-plugin/src/models'; -import { history } from '@console/internal/components/utils'; import { kindForReference } from '@console/internal/module/k8s'; import { ALL_NAMESPACES_KEY, useQueryParams } from '@console/shared/src'; import { DocumentTitle } from '@console/shared/src/components/document-title/DocumentTitle'; import CreateHelmChartRepository from './CreateHelmChartRepository'; const CreateHelmChartRepositoryPage: FC = () => { + const navigate = useNavigate(); const { t } = useTranslation(); const params = useParams(); const queryParams = useQueryParams(); @@ -44,9 +44,9 @@ const CreateHelmChartRepositoryPage: FC = () => { const handleNamespaceChange = (ns: string) => { if (ns === ALL_NAMESPACES_KEY) { - history.push(`/helm/all-namespaces/repositories`); + navigate(`/helm/all-namespaces/repositories`); } else if (ns !== namespace) { - history.push(`/helm/ns/${ns}/repositories`); + navigate(`/helm/ns/${ns}/repositories`); } }; diff --git a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradePage.tsx b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradePage.tsx index 235588acc31..d28c3d0d739 100644 --- a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradePage.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/HelmInstallUpgradePage.tsx @@ -1,17 +1,17 @@ import type { FunctionComponent } from 'react'; -import { useState, useMemo, useEffect } from 'react'; +import { useState, useMemo, useEffect, useCallback } from 'react'; import { Formik } from 'formik'; import { safeDump, safeLoad } from 'js-yaml'; import { JSONSchema7 } from 'json-schema'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; -import { useParams, useLocation } from 'react-router-dom-v5-compat'; +import { useParams, useLocation, useNavigate } from 'react-router-dom-v5-compat'; import NamespacedPage, { NamespacedPageVariants, } from '@console/dev-console/src/components/NamespacedPage'; import { useActivePerspective } from '@console/dynamic-plugin-sdk/src'; import { coFetchJSON } from '@console/internal/co-fetch'; -import { history, LoadingBox } from '@console/internal/components/utils'; +import { LoadingBox } from '@console/internal/components/utils'; import { ALL_NAMESPACES_KEY } from '@console/shared/src'; import { DocumentTitle } from '@console/shared/src/components/document-title/DocumentTitle'; import { prune } from '@console/shared/src/components/dynamic-form/utils'; @@ -37,6 +37,8 @@ import HelmChartMetaDescription from './HelmChartMetaDescription'; import HelmInstallUpgradeForm, { HelmInstallUpgradeFormData } from './HelmInstallUpgradeForm'; const HelmInstallUpgradePage: FunctionComponent = () => { + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const location = useLocation(); const params = useParams(); const searchParams = new URLSearchParams(location.search); @@ -203,7 +205,7 @@ const HelmInstallUpgradePage: FunctionComponent = () => { redirect = `/helm-releases/ns/${namespace}/release/${releaseName}`; } - history.push(redirect); + navigate(redirect); }) .catch((err) => { actions.setStatus({ submitError: err.message }); @@ -212,9 +214,9 @@ const HelmInstallUpgradePage: FunctionComponent = () => { const handleNamespaceChange = (ns: string) => { if (ns === ALL_NAMESPACES_KEY) { - history.push(`/helm/all-namespaces`); + navigate(`/helm/all-namespaces`); } else if (ns !== namespace) { - history.push(`/helm/ns/${ns}`); + navigate(`/helm/ns/${ns}`); } }; @@ -237,7 +239,7 @@ const HelmInstallUpgradePage: FunctionComponent = () => { {(formikProps) => ( diff --git a/frontend/packages/helm-plugin/src/components/forms/rollback/HelmReleaseRollbackPage.tsx b/frontend/packages/helm-plugin/src/components/forms/rollback/HelmReleaseRollbackPage.tsx index c5db389eaea..ea38d3f08c8 100644 --- a/frontend/packages/helm-plugin/src/components/forms/rollback/HelmReleaseRollbackPage.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/rollback/HelmReleaseRollbackPage.tsx @@ -1,12 +1,12 @@ import type { FC } from 'react'; -import { useState, useMemo, useEffect } from 'react'; +import { useState, useMemo, useEffect, useCallback } from 'react'; import { Formik } from 'formik'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom-v5-compat'; +import { useParams, useNavigate } from 'react-router-dom-v5-compat'; import NamespacedPage, { NamespacedPageVariants, } from '@console/dev-console/src/components/NamespacedPage'; -import { history, getQueryArgument } from '@console/internal/components/utils'; +import { getQueryArgument } from '@console/internal/components/utils'; import { DocumentTitle } from '@console/shared/src/components/document-title/DocumentTitle'; import { HelmRelease, HelmActionType, HelmActionOrigins } from '../../../types/helm-types'; import { fetchHelmReleaseHistory, getHelmActionConfig } from '../../../utils/helm-utils'; @@ -17,6 +17,8 @@ type HelmRollbackFormData = { }; const HelmReleaseRollbackPage: FC = () => { + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const { t } = useTranslation(); const { releaseName, ns: namespace } = useParams(); const actionOrigin = getQueryArgument('actionOrigin') as HelmActionOrigins; @@ -61,7 +63,7 @@ const HelmReleaseRollbackPage: FC = () => { return config .fetch('/api/helm/release', payload, null, -1) .then(() => { - history.push(config.redirectURL); + navigate(config.redirectURL); }) .catch((err) => { actions.setStatus({ submitError: err.message }); @@ -71,7 +73,7 @@ const HelmReleaseRollbackPage: FC = () => { return ( {config.title} - + {(props) => ( = ({ sinkKind = '', kameletSink, }) => { + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const perpectiveExtension = usePerspectives(); const [perspective] = useActivePerspective(); const { t } = useTranslation(); @@ -149,7 +152,7 @@ const EventSink: FC = ({ return eventSinkRequest .then(() => { - handleRedirect(projectName, perspective, perpectiveExtension); + handleRedirect(projectName, perspective, perpectiveExtension, navigate); }) .catch((err) => { actions.setStatus({ submitError: err.message }); @@ -160,7 +163,7 @@ const EventSink: FC = ({ = ({ sourceKind = '', kameletSource, }) => { + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const perpectiveExtension = usePerspectives(); const [perspective] = useActivePerspective(); const { t } = useTranslation(); @@ -161,7 +164,7 @@ export const EventSource: FC = ({ return eventSrcRequest .then(() => { - handleRedirect(projectName, perspective, perpectiveExtension); + handleRedirect(projectName, perspective, perpectiveExtension, navigate); }) .catch((err) => { actions.setStatus({ submitError: err.message }); @@ -172,7 +175,7 @@ export const EventSource: FC = ({ { }; it('should render form with proper initialvalues if contextSource is not passed', () => { - const { container } = render( + const { container } = renderWithProviders( { it('should render form with proper initialvalues for sink if contextSource is passed', () => { const contextSourceData = 'serving.knative.dev~v1~Service/svc-display'; - const { container } = render( + const { container } = renderWithProviders( = ({ namespace, selectedApplication }) => { + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const perspectiveExtension = usePerspectives(); const [perspective] = useActivePerspective(); const { t } = useTranslation(); @@ -56,7 +59,7 @@ const AddBroker: FC = ({ namespace, selectedApplication }) => { ) => { return createResources(values, actions) .then(() => { - handleRedirect(values.formData.project.name, perspective, perspectiveExtension); + handleRedirect(values.formData.project.name, perspective, perspectiveExtension, navigate); }) .catch((err) => { actions.setStatus({ submitError: err.message }); @@ -67,7 +70,7 @@ const AddBroker: FC = ({ namespace, selectedApplication }) => { {(formikProps) => } diff --git a/frontend/packages/knative-plugin/src/components/add/channels/AddChannel.tsx b/frontend/packages/knative-plugin/src/components/add/channels/AddChannel.tsx index 0f193f31fa6..f8a2bea5b9a 100644 --- a/frontend/packages/knative-plugin/src/components/add/channels/AddChannel.tsx +++ b/frontend/packages/knative-plugin/src/components/add/channels/AddChannel.tsx @@ -1,9 +1,10 @@ import type { FC } from 'react'; +import { useCallback } from 'react'; import { Formik } from 'formik'; import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { useActivePerspective } from '@console/dynamic-plugin-sdk'; -import { history } from '@console/internal/components/utils'; import { K8sResourceKind, k8sCreate, modelFor, referenceFor } from '@console/internal/module/k8s'; import { getActiveApplication } from '@console/internal/reducers/ui'; import { RootState } from '@console/internal/redux'; @@ -31,6 +32,8 @@ interface StateProps { type Props = ChannelProps & StateProps; const AddChannel: FC = ({ namespace, channels, activeApplication }) => { + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const [perspective] = useActivePerspective(); const { t } = useTranslation(); const initialFormData: AddChannelFormData = { @@ -83,7 +86,7 @@ const AddChannel: FC = ({ namespace, channels, activeApplication }) => { const handleSubmit = (values, actions) => { return createResources(values) .then(() => { - handleRedirect(values.formData.namespace, perspective, perspectiveExtension); + handleRedirect(values.formData.namespace, perspective, perspectiveExtension, navigate); }) .catch((err) => { actions.setStatus({ submitError: err.message }); @@ -94,7 +97,7 @@ const AddChannel: FC = ({ namespace, channels, activeApplication }) => { = ({ source, target = { metadata: { name: '' } } }) => { + const navigate = useNavigate(); const { t } = useTranslation(); const { apiVersion: sourceApiVersion, @@ -118,7 +120,7 @@ const Subscribe: FC = ({ source, target = { metadata: { name: '' return k8sCreate(getResourceModel(), sanitizeResourceName(values.formData)) .then((resource) => { action.setStatus({ subscriberAvailable: true, error: '' }); - history.push(`/topology/ns/${resource.metadata.namespace}`); + navigate(`/topology/ns/${resource.metadata.namespace}`); }) .catch((err) => { const errMessage = err.message || t('knative-plugin~An error occurred. Please try again'); @@ -129,7 +131,7 @@ const Subscribe: FC = ({ source, target = { metadata: { name: '' }); }; - const handleCancel = () => history.goBack(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); return loaded ? ( > = () => { + const navigate = useNavigate(); const params = useParams(); const handleNamespaceChange = (newNamespace: string): void => { if (newNamespace === ALL_NAMESPACES_KEY) { - history.push('/functions/all-namespaces'); + navigate('/functions/all-namespaces'); } else { - history.push('/functions/ns/:ns'); + navigate(`/functions/ns/${newNamespace}`); } }; return ( diff --git a/frontend/packages/knative-plugin/src/components/knatify/CreateKnatifyPage.tsx b/frontend/packages/knative-plugin/src/components/knatify/CreateKnatifyPage.tsx index ec2adeefce0..c8163c6fc1a 100644 --- a/frontend/packages/knative-plugin/src/components/knatify/CreateKnatifyPage.tsx +++ b/frontend/packages/knative-plugin/src/components/knatify/CreateKnatifyPage.tsx @@ -1,8 +1,8 @@ import type { FunctionComponent } from 'react'; -import { useMemo } from 'react'; +import { useMemo, useCallback } from 'react'; import { Formik, FormikHelpers } from 'formik'; import { useTranslation } from 'react-i18next'; -import { useParams, useLocation } from 'react-router-dom-v5-compat'; +import { useParams, useLocation, useNavigate } from 'react-router-dom-v5-compat'; import { deployValidationSchema } from '@console/dev-console/src/components/import/deployImage-validation-utils'; import { handleRedirect } from '@console/dev-console/src/components/import/import-submit-utils'; import { DeployImageFormData } from '@console/dev-console/src/components/import/import-types'; @@ -14,7 +14,7 @@ import { WatchK8sResultsObject, useActivePerspective, } from '@console/dynamic-plugin-sdk'; -import { LoadingBox, history } from '@console/internal/components/utils'; +import { LoadingBox } from '@console/internal/components/utils'; import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import { ProjectModel, ServiceModel } from '@console/internal/models'; import { k8sGet, K8sResourceKind } from '@console/internal/module/k8s'; @@ -37,6 +37,8 @@ const CreateKnatifyPage: FunctionComponent = () => { const { t } = useTranslation(); const { ns: namespace } = useParams(); const location = useLocation(); + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const queryParams = new URLSearchParams(location.search); const kind = queryParams.get('kind'); const appName = queryParams.get('name'); @@ -98,16 +100,18 @@ const CreateKnatifyPage: FunctionComponent = () => { }, ), }); + return undefined; } + return undefined; } catch { const resourceActions = knatifyResources(values, appName, true).then(() => knatifyResources(values, appName), ); - resourceActions + return resourceActions .then(() => { helpers.setStatus({ submitError: '' }); - handleRedirect(namespace, perspective, perspectiveExtensions); + return handleRedirect(namespace, perspective, perspectiveExtensions, navigate); }) .catch((err) => { helpers.setStatus({ submitError: err.message }); @@ -134,7 +138,7 @@ const CreateKnatifyPage: FunctionComponent = () => { )} validationSchema={deployValidationSchema(t)} onSubmit={handleSubmit} - onReset={history.goBack} + onReset={handleCancel} > {(formikProps) => ( = ({ loaded, resources, revision, cancel, close }) => { const { t } = useTranslation(); + const navigate = useNavigate(); if (!loaded) { return null; } @@ -131,7 +132,7 @@ const Controller: FC = ({ loaded, resources, revision, cancel, // If we are currently on the deleted revision's page, redirect to the list page const re = new RegExp(`/${revision.metadata.name}(/|$)`); if (re.test(window.location.pathname)) { - history.push(resourceListPathFromModel(RevisionModel, revision.metadata.namespace)); + navigate(resourceListPathFromModel(RevisionModel, revision.metadata.namespace)); } }) .catch((err) => { diff --git a/frontend/packages/knative-plugin/src/utils/create-eventsources-utils.ts b/frontend/packages/knative-plugin/src/utils/create-eventsources-utils.ts index 9decb317a00..7c9a20f0a14 100644 --- a/frontend/packages/knative-plugin/src/utils/create-eventsources-utils.ts +++ b/frontend/packages/knative-plugin/src/utils/create-eventsources-utils.ts @@ -1,10 +1,11 @@ import * as _ from 'lodash'; +import { NavigateFunction } from 'react-router-dom-v5-compat'; import { getAppLabels, getCommonAnnotations, } from '@console/dev-console/src/utils/resource-label-utils'; import { Perspective } from '@console/dynamic-plugin-sdk'; -import { checkAccess, history } from '@console/internal/components/utils'; +import { checkAccess } from '@console/internal/components/utils'; import { K8sResourceKind, referenceForModel, @@ -346,10 +347,31 @@ export const handleRedirect = async ( project: string, perspective: string, perspectiveExtensions: Perspective[], + navigate: NavigateFunction, ) => { const perspectiveData = perspectiveExtensions.find((item) => item.properties.id === perspective); - const redirectURL = (await perspectiveData.properties.importRedirectURL())(project); - history.push(redirectURL); + if (!perspectiveData || !perspectiveData.properties?.importRedirectURL) { + // eslint-disable-next-line no-console + console.warn( + `Unable to redirect: perspective data not found or importRedirectURL missing for perspective: ${perspective}`, + ); + return; + } + + try { + const redirectURL = (await perspectiveData.properties.importRedirectURL())(project); + if (!redirectURL) { + // eslint-disable-next-line no-console + console.warn( + `Skipping navigation: importRedirectURL returned empty/undefined for perspective ${perspective}`, + ); + return; + } + navigate(redirectURL); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to redirect for perspective ${perspective}:`, error); + } }; export const sanitizeSourceToForm = ( diff --git a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHost.tsx b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHost.tsx index b45ba311bd7..0d440903632 100644 --- a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHost.tsx +++ b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHost.tsx @@ -3,14 +3,10 @@ import { useMemo, useState, useEffect } from 'react'; import { Formik, FormikHelpers } from 'formik'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import * as Yup from 'yup'; import { WatchK8sResource } from '@console/dynamic-plugin-sdk'; -import { - history, - resourcePathFromModel, - LoadingBox, - LoadError, -} from '@console/internal/components/utils'; +import { resourcePathFromModel, LoadingBox, LoadError } from '@console/internal/components/utils'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { SecretModel } from '@console/internal/models'; import { referenceForModel, SecretKind } from '@console/internal/module/k8s'; @@ -60,6 +56,7 @@ type AddBareMetalHostProps = { const AddBareMetalHost: FC = ({ namespace, name, enablePowerMgmt }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const bmhResource = useMemo( () => name @@ -171,7 +168,7 @@ const AddBareMetalHost: FC = ({ namespace, name, enablePo return promise .then(() => { - history.push(resourcePathFromModel(BareMetalHostModel, values.name, namespace)); + navigate(resourcePathFromModel(BareMetalHostModel, values.name, namespace)); }) .catch((error) => { actions.setStatus({ submitError: error.message }); diff --git a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHostForm.tsx b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHostForm.tsx index 499b9a56f4d..ef22af1f443 100644 --- a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHostForm.tsx +++ b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/add-baremetal-host/AddBareMetalHostForm.tsx @@ -1,9 +1,10 @@ import type { FC } from 'react'; +import { useCallback } from 'react'; import { Form, TextInputTypes } from '@patternfly/react-core'; import { FormikProps } from 'formik'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; -import { history } from '@console/internal/components/utils'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { InputField, TextAreaField, @@ -30,7 +31,9 @@ const AddBareMetalHostForm: FC = ({ showUpdated, values, }) => { + const navigate = useNavigate(); const { t } = useTranslation(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); return (
= ({ ({ jest.mock('react-router-dom-v5-compat', () => ({ ...jest.requireActual('react-router-dom-v5-compat'), - useParams: jest.fn(), - useLocation: jest.fn(), + useParams: jest.fn(() => ({})), + useLocation: jest.fn(() => ({ pathname: '/', search: '', hash: '', state: null })), })); jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({ diff --git a/frontend/packages/operator-lifecycle-manager/src/components/create-catalog-source.tsx b/frontend/packages/operator-lifecycle-manager/src/components/create-catalog-source.tsx index 4c246a898c1..3852cc79cce 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/create-catalog-source.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/create-catalog-source.tsx @@ -11,8 +11,9 @@ import { TextInput, } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { RadioGroup, RadioGroupItems } from '@console/internal/components/radio'; -import { ButtonBar, history, NsDropdown } from '@console/internal/components/utils'; +import { ButtonBar, NsDropdown } from '@console/internal/components/utils'; import { k8sCreate } from '@console/internal/module/k8s'; import { DocumentTitle } from '@console/shared/src/components/document-title/DocumentTitle'; import { PageHeading } from '@console/shared/src/components/heading/PageHeading'; @@ -26,6 +27,8 @@ enum AvailabilityValue { } export const CreateCatalogSource = () => { + const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const [handlePromise, inProgress, errorMessage] = usePromiseHandler(); const [availability, setAvailability] = useState(AvailabilityValue.ALL_NAMESPACES); const [image, setImage] = useState(''); @@ -55,10 +58,10 @@ export const CreateCatalogSource = () => { }, }), ).then(() => { - history.goBack(); + navigate(-1); }); }, - [availability, displayName, handlePromise, image, name, namespace, publisher], + [availability, displayName, handlePromise, image, name, namespace, publisher, navigate], ); const onNamespaceChange = useCallback((value: string) => { @@ -176,7 +179,7 @@ export const CreateCatalogSource = () => { - diff --git a/frontend/packages/operator-lifecycle-manager/src/components/install-plan.tsx b/frontend/packages/operator-lifecycle-manager/src/components/install-plan.tsx index a8a20957461..ed73d3a58a7 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/install-plan.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/install-plan.tsx @@ -20,7 +20,7 @@ import { Map as ImmutableMap, Set as ImmutableSet, fromJS } from 'immutable'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useParams, Link } from 'react-router-dom-v5-compat'; +import { useParams, Link, useNavigate } from 'react-router-dom-v5-compat'; import { getUser, GreenCheckCircleIcon } from '@console/dynamic-plugin-sdk'; import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; import { Conditions } from '@console/internal/components/conditions'; @@ -39,7 +39,6 @@ import { ResourceIcon, navFactory, ResourceSummary, - history, useAccessReview, } from '@console/internal/components/utils'; import { authSvc } from '@console/internal/module/auth'; @@ -409,6 +408,7 @@ export const InstallPlanDetails: FC = ({ obj }) => { export const InstallPlanPreview: FC = ({ obj, hideApprovalBlock }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const launchModal = useOverlay(); const [needsApproval, setNeedsApproval] = useState( obj.spec.approval === InstallPlanApproval.Manual && obj.spec.approved === false, @@ -467,7 +467,7 @@ export const InstallPlanPreview: FC = ({ obj, hideAppro variant="secondary" isDisabled={false} onClick={() => - history.push( + navigate( `/k8s/ns/${obj.metadata.namespace}/${referenceForModel( SubscriptionModel, )}/${subscription.name}?showDelete=true`, diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx index 0c3919d6188..2471cca79bb 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx @@ -12,6 +12,7 @@ import { } from '@patternfly/react-core'; import * as _ from 'lodash'; import { Trans, useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; import { k8sGetResource } from '@console/dynamic-plugin-sdk/src/utils/k8s'; @@ -28,7 +29,6 @@ import { ModalWrapper, } from '@console/internal/components/factory/modal'; import { - history, LinkifyExternal, ResourceLink, resourceListPathFromModel, @@ -73,6 +73,7 @@ export const UninstallOperatorModal: FC = ({ subscription, }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const [ handleOperatorUninstallPromise, operatorUninstallInProgress, @@ -237,9 +238,9 @@ export const UninstallOperatorModal: FC = ({ window.location.pathname.split('/').includes(subscription.metadata.name) || window.location.pathname.split('/').includes(subscription?.status?.installedCSV) ) { - history.push(resourceListPathFromModel(ClusterServiceVersionModel, getActiveNamespace())); + navigate(resourceListPathFromModel(ClusterServiceVersionModel, getActiveNamespace())); } - }, [close, subscription]); + }, [close, navigate, subscription]); useEffect(() => { if (isSubmitFinished && !hasSubmitErrors) { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx index 7d0b42809e9..28ce8533d3b 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx @@ -26,13 +26,12 @@ import * as Immutable from 'immutable'; import { JSONSchema6, JSONSchema6TypeName } from 'json-schema'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom-v5-compat'; +import { useParams, useNavigate } from 'react-router-dom-v5-compat'; import { SyncMarkdownView } from '@console/internal/components/markdown-view'; import { ConfigureUpdateStrategy } from '@console/internal/components/modals/configure-update-strategy-modal'; import { RadioGroup } from '@console/internal/components/radio'; import { NumberSpinner, - history, SelectorInput, ListDropdown, useScrollToTopOnMount, @@ -514,6 +513,7 @@ export const DEPRECATED_CreateOperandForm: FC = ({ const postFormCallback = useResourceConnectionHandler(); const { t } = useTranslation(); const params = useParams(); + const navigate = useNavigate(); const immutableFormData = Immutable.fromJS(formData); const handleFormDataUpdate = (path: string, value: any): void => { const { regexMatch, index, pathBeforeIndex, pathAfterIndex } = parseArrayPath(path); @@ -681,7 +681,7 @@ export const DEPRECATED_CreateOperandForm: FC = ({ : immutableFormData.toJS(), ) .then((res) => postFormCallback(res)) - .then(() => next && history.push(next)) + .then(() => next && navigate(next)) .catch((err: Error) => setError(err.message || 'Unknown error.')); }; @@ -1175,7 +1175,7 @@ export const DEPRECATED_CreateOperandForm: FC = ({ - diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx index 5a2c7bd495c..ae99930fb4b 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx @@ -3,13 +3,9 @@ import { useState, useMemo } from 'react'; import { Grid, GridItem } from '@patternfly/react-core'; import { JSONSchema7 } from 'json-schema'; import * as _ from 'lodash'; -import { useParams } from 'react-router-dom-v5-compat'; +import { useParams, useNavigate } from 'react-router-dom-v5-compat'; import { SyncMarkdownView } from '@console/internal/components/markdown-view'; -import { - history, - resourcePathFromModel, - useScrollToTopOnMount, -} from '@console/internal/components/utils'; +import { resourcePathFromModel, useScrollToTopOnMount } from '@console/internal/components/utils'; import { k8sCreate, K8sKind, K8sResourceKind } from '@console/internal/module/k8s'; import { DynamicForm } from '@console/shared/src/components/dynamic-form'; import PaneBody from '@console/shared/src/components/layout/PaneBody'; @@ -31,6 +27,7 @@ export const OperandForm: FC = ({ }) => { const [errors, setErrors] = useState([]); const params = useParams(); + const navigate = useNavigate(); const postFormCallback = useResourceConnectionHandler(); const processFormData = ({ metadata, ...rest }) => { const data = { @@ -46,21 +43,22 @@ export const OperandForm: FC = ({ const handleSubmit = ({ formData: submitFormData }) => { k8sCreate(model, processFormData(submitFormData)) .then((res) => postFormCallback(res)) - .then(() => next && history.push(next)) + .then(() => next && navigate(next)) .catch((e) => setErrors([e.message])); }; const handleCancel = () => { if (new URLSearchParams(window.location.search).has('useInitializationResource')) { - history.replace( + navigate( resourcePathFromModel( ClusterServiceVersionModel, csv.metadata.name, csv.metadata.namespace, ), + { replace: true }, ); } else { - history.goBack(); + navigate(-1); } }; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx index 6880bb3babb..0107ec20424 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx @@ -12,9 +12,8 @@ import { import { css } from '@patternfly/react-styles'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; -import { Link, useSearchParams } from 'react-router-dom-v5-compat'; -import { getQueryArgument } from '@console/internal/components/utils'; -import { history } from '@console/internal/components/utils/router'; +import { Link, useNavigate, useSearchParams } from 'react-router-dom-v5-compat'; +import { useQueryParamsMutator } from '@console/internal/components/utils'; import { TileViewPage } from '@console/internal/components/utils/tile-view-page'; import i18n from '@console/internal/i18n'; import { @@ -405,13 +404,6 @@ export const keywordCompareWithScore = ( // Flag to indicate this function uses scoring keywordCompareWithScore.useScoring = true; -const setURLParams = (params: URLSearchParams): void => { - const url = new URL(window.location.href); - const searchParams = `?${params.toString()}${url.hash}`; - - history.replace(`${url.pathname}${searchParams}`); -}; - const getRedHatPriority = (item: OperatorHubItem): number => { // Check metadata.labels.provider const metadataProvider = _.get(item, 'obj.metadata.labels.provider', ''); @@ -572,6 +564,8 @@ const OperatorHubTile: FC = ({ item, onClick }) => { export const OperatorHubTileView: FC = (props) => { const { t } = useTranslation(); + const navigate = useNavigate(); + const { setQueryArgument, removeQueryArgument, getQueryArgument } = useQueryParamsMutator(); const [detailsItem, setDetailsItem] = useState(null); const [showDetails, setShowDetails] = useState(false); const [ignoreOperatorWarning, setIgnoreOperatorWarning, loaded] = useUserSettingsCompatibility< @@ -748,9 +742,7 @@ export const OperatorHubTileView: FC = (props) => { }, [filteredItems, searchParams]); const showCommunityOperator = (item: OperatorHubItem) => (ignoreWarning = false) => { - const params = new URLSearchParams(window.location.search); - params.set('details-item', item.uid); - setURLParams(params); + setQueryArgument('details-item', item.uid); setDetailsItem(item); setShowDetails(true); @@ -760,11 +752,9 @@ export const OperatorHubTileView: FC = (props) => { }; const closeOverlay = () => { - const params = new URLSearchParams(window.location.search); - params.delete('details-item'); - params.delete('channel'); - params.delete('version'); - setURLParams(params); + removeQueryArgument('details-item'); + removeQueryArgument('channel'); + removeQueryArgument('version'); setDetailsItem(null); setShowDetails(false); // reset version and channel state so that switching between operator cards does not carry over previous selections @@ -779,9 +769,7 @@ export const OperatorHubTileView: FC = (props) => { showCommunityOperators: (ignore) => showCommunityOperator(item)(ignore), }); } else { - const params = new URLSearchParams(window.location.search); - params.set('details-item', item.uid); - setURLParams(params); + setQueryArgument('details-item', item.uid); setDetailsItem(item); setShowDetails(true); } @@ -810,10 +798,15 @@ export const OperatorHubTileView: FC = (props) => { const installLink = detailsItem && detailsItem.obj && `/operatorhub/subscribe?${installParamsURL}`; - const uninstallLink = () => - detailsItem && - detailsItem.subscription && - `/k8s/ns/${detailsItem.subscription.metadata.namespace}/${SubscriptionModel.plural}/${detailsItem.subscription.metadata.name}?showDelete=true`; + const handleUninstallClick = useCallback(() => { + const link = + detailsItem && + detailsItem.subscription && + `/k8s/ns/${detailsItem.subscription.metadata.namespace}/${SubscriptionModel.plural}/${detailsItem.subscription.metadata.name}?showDelete=true`; + if (link) { + navigate(link); + } + }, [navigate, detailsItem]); if (_.isEmpty(filteredItems)) { return ( @@ -1079,7 +1072,7 @@ export const OperatorHubTileView: FC = (props) => { className="co-catalog-page__overlay-action" data-test-id="operator-uninstall-btn" isDisabled={!detailsItem.installed} - onClick={() => history.push(uninstallLink())} + onClick={handleUninstallClick} variant="secondary" > {t('olm~Uninstall')} diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-subscribe.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-subscribe.tsx index b4badc7a813..05f97c940c0 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-subscribe.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-subscribe.tsx @@ -25,7 +25,6 @@ import { Firehose, getDocumentationURL, getURLSearchParams, - history, isManaged, ConsoleEmptyState, NsDropdown, @@ -125,6 +124,7 @@ const InputField: FC = ({ export const OperatorHubSubscribeForm: FC = (props) => { const packageManifest = props.packageManifest?.data?.[0]; const navigate = useNavigate(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); const [activeNamespace] = useActiveNamespace(); const { name: pkgName } = packageManifest?.metadata ?? {}; const { provider, channels = [], packageName, catalogSource, catalogSourceNamespace } = @@ -339,11 +339,11 @@ export const OperatorHubSubscribeForm: FC = (prop const navigateToInstallPage = useCallback( (csvName: string) => { - history.push( + navigate( `/operatorhub/install/${catalogNamespace}/${catalog}/${pkg}/${csvName}/to/${selectedTargetNamespace}`, ); }, - [catalog, catalogNamespace, pkg, selectedTargetNamespace], + [catalog, catalogNamespace, navigate, pkg, selectedTargetNamespace], ); if (!supportsSingle && !supportsGlobal) { @@ -1179,7 +1179,7 @@ export const OperatorHubSubscribeForm: FC = (prop > {t('olm~Install')} - diff --git a/frontend/packages/operator-lifecycle-manager/src/components/subscription.tsx b/frontend/packages/operator-lifecycle-manager/src/components/subscription.tsx index 92cc029428a..d51f32b49ea 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/subscription.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/subscription.tsx @@ -21,7 +21,7 @@ import { css } from '@patternfly/react-styles'; import { sortable } from '@patternfly/react-table'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; -import { Link, useParams } from 'react-router-dom-v5-compat'; +import { Link, useLocation, useParams } from 'react-router-dom-v5-compat'; import { ResourceStatus, StatusIconAndText } from '@console/dynamic-plugin-sdk'; import { getGroupVersionKindForModel, @@ -420,6 +420,7 @@ export const SubscriptionDetails: FC = ({ subscriptions = [], }) => { const { t } = useTranslation(); + const location = useLocation(); const { removeQueryArgument } = useQueryParamsMutator(); const { source, sourceNamespace } = obj?.spec ?? {}; const catalogHealth = obj?.status?.catalogHealth?.find( @@ -434,10 +435,13 @@ export const SubscriptionDetails: FC = ({ k8sPatch, subscription: obj, }); - if (new URLSearchParams(window.location.search).has('showDelete')) { - uninstallOperatorModal(); - removeQueryArgument('showDelete'); - } + + useEffect(() => { + if (new URLSearchParams(location.search).has('showDelete')) { + uninstallOperatorModal(); + removeQueryArgument('showDelete'); + } + }, [location.search, uninstallOperatorModal, removeQueryArgument]); const { deprecatedPackage, deprecatedChannel, deprecatedVersion } = findDeprecatedOperator(obj); diff --git a/frontend/packages/shipwright-plugin/src/components/build-form/EditBuild.tsx b/frontend/packages/shipwright-plugin/src/components/build-form/EditBuild.tsx index 0758f765b97..1c66254de04 100644 --- a/frontend/packages/shipwright-plugin/src/components/build-form/EditBuild.tsx +++ b/frontend/packages/shipwright-plugin/src/components/build-form/EditBuild.tsx @@ -1,9 +1,10 @@ import type { FC } from 'react'; -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { Formik, FormikHelpers } from 'formik'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; -import { history, resourcePathFromModel } from '@console/internal/components/utils'; +import { useNavigate } from 'react-router-dom-v5-compat'; +import { resourcePathFromModel } from '@console/internal/components/utils'; import { k8sCreate, k8sUpdate } from '@console/internal/module/k8s'; import { EditorType } from '@console/shared/src/components/synced-editor/editor-toggle'; import { safeJSToYAML, safeYAMLToJS } from '@console/shared/src/utils/yaml'; @@ -22,6 +23,7 @@ type EditBuildProps = { }; const EditBuild: FC = ({ heading, build: watchedBuild, namespace, name }) => { + const navigate = useNavigate(); const { t } = useTranslation(); const [initialValues] = useState(() => { const values = convertBuildToFormData(watchedBuild); @@ -65,7 +67,7 @@ const EditBuild: FC = ({ heading, build: watchedBuild, namespace ? await k8sCreate(BuildModel, changedBuild) : await k8sUpdate(BuildModel, changedBuild, namespace, name); - history.push( + navigate( resourcePathFromModel( BuildModel, updatedBuildConfig.metadata.name, @@ -77,7 +79,7 @@ const EditBuild: FC = ({ heading, build: watchedBuild, namespace } }; - const handleCancel = () => history.goBack(); + const handleCancel = useCallback(() => navigate(-1), [navigate]); return ( = ({ name, namespace, onViewLog }) => { + const navigate = useNavigate(); const { t } = useTranslation(); const [job, jobLoaded] = useK8sWatchResource({ kind: JobModel.kind, @@ -50,7 +52,7 @@ const ExportViewLogButton: FC = ({ name, namespace, on return; } e.preventDefault(); - history.push(path); + navigate(path); onViewLog?.(); }; diff --git a/frontend/packages/topology/src/components/export-app/__tests__/ExportViewLogButton.spec.tsx b/frontend/packages/topology/src/components/export-app/__tests__/ExportViewLogButton.spec.tsx index 6182bc80936..fc131e010aa 100644 --- a/frontend/packages/topology/src/components/export-app/__tests__/ExportViewLogButton.spec.tsx +++ b/frontend/packages/topology/src/components/export-app/__tests__/ExportViewLogButton.spec.tsx @@ -1,9 +1,8 @@ -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Provider } from 'react-redux'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { JobModel, PodModel } from '@console/internal/models'; -import store from '@console/internal/redux'; +import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import ExportViewLogButton from '../ExportViewLogButton'; jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({ @@ -44,21 +43,15 @@ describe('ExportViewLogButton', () => { }); it('should render a link and correct href path', () => { - render( - - - , - ); + renderWithProviders(); const logButton = screen.getByTestId('export-view-log-btn'); expect(logButton).toHaveAttribute('href', '/k8s/ns/test/pods/test/logs'); }); it('should call onViewLog callback', async () => { const viewLogCallback = jest.fn(); - render( - - - , + renderWithProviders( + , ); const logButton = screen.getByTestId('export-view-log-btn'); await userEvent.click(logButton); @@ -66,11 +59,7 @@ describe('ExportViewLogButton', () => { }); it('should not render a disabled button', () => { - render( - - - , - ); + renderWithProviders(); const logButton = screen.getByTestId('export-view-log-btn'); expect(logButton).not.toHaveAttribute('aria-disabled'); }); @@ -95,11 +84,7 @@ describe('ExportViewLogButton', () => { return [null, true, null]; } }); - render( - - - , - ); + renderWithProviders(); const logButton = screen.getByTestId('export-view-log-btn'); expect(logButton).toHaveAttribute('aria-disabled', 'true'); }); @@ -125,11 +110,7 @@ describe('ExportViewLogButton', () => { } }); - render( - - - , - ); + renderWithProviders(); const logButton = screen.getByTestId('export-view-log-btn'); await userEvent.hover(logButton); diff --git a/frontend/public/components/QuickCreate.tsx b/frontend/public/components/QuickCreate.tsx index ce6336d9bb1..2a7a2645332 100644 --- a/frontend/public/components/QuickCreate.tsx +++ b/frontend/public/components/QuickCreate.tsx @@ -6,14 +6,13 @@ import { MenuToggleElement, Tooltip, } from '@patternfly/react-core'; -import { history } from '@console/internal/components/utils/router'; import { PlusCircleIcon } from '@patternfly/react-icons'; import { ALL_NAMESPACES_KEY } from '@console/shared/src/constants/common'; import { formatNamespacedRouteForResource } from '@console/shared/src/utils/namespace'; import { useFlag } from '@console/shared/src/hooks/flag'; import { useTelemetry } from '@console/shared/src/hooks/useTelemetry'; import type { FC, Ref } from 'react'; -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { FLAGS } from '@console/shared/src/constants'; import { useAccessReview } from '@console/dynamic-plugin-sdk/src'; @@ -70,6 +69,7 @@ const useCanCreateResource = () => { const QuickCreate: FC = ({ namespace }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const fireTelemetryEvent = useTelemetry(); const opeshiftStartGuideEnable = useFlag(FLAGS.SHOW_OPENSHIFT_START_GUIDE); @@ -117,7 +117,7 @@ const QuickCreate: FC = ({ namespace }) => { onClick={(ev: any) => { ev.preventDefault(); fireTelemetryEvent('quick-create-import-yaml'); - history.push(importYAMLURL); + navigate(importYAMLURL); }} tooltipProps={{ content: t('public~Create resources from their YAML or JSON definitions'), @@ -136,7 +136,7 @@ const QuickCreate: FC = ({ namespace }) => { onClick={(ev: any) => { ev.preventDefault(); fireTelemetryEvent('quick-create-import-from-git'); - history.push(getImportFromGitURL(namespace)); + navigate(getImportFromGitURL(namespace)); }} tooltipProps={{ content: t('public~Import code from your Git repository to be built and deployed'), @@ -153,7 +153,7 @@ const QuickCreate: FC = ({ namespace }) => { onClick={(ev: any) => { ev.preventDefault(); fireTelemetryEvent('quick-create-container-images'); - history.push(getContainerImageURL(namespace)); + navigate(getContainerImageURL(namespace)); }} tooltipProps={{ content: t( @@ -180,14 +180,14 @@ export const QuickCreateImportFromGit = ({ namespace, className }) => { const opeshiftStartGuideEnable = useFlag(FLAGS.SHOW_OPENSHIFT_START_GUIDE); const canCreate = useCanCreateResource(); + const handleClick = useCallback(() => { + navigate(getImportFromGitURL(namespace)); + }, [navigate, namespace]); + return ( canCreate && !opeshiftStartGuideEnable && ( - ) @@ -200,14 +200,14 @@ export const QuickCreateContainerImages = ({ namespace, className }) => { const opeshiftStartGuideEnable = useFlag(FLAGS.SHOW_OPENSHIFT_START_GUIDE); const canCreate = useCanCreateResource(); + const handleClick = useCallback(() => { + navigate(getContainerImageURL(namespace)); + }, [navigate, namespace]); + return ( canCreate && !opeshiftStartGuideEnable && ( - ) diff --git a/frontend/public/components/modals/delete-modal.tsx b/frontend/public/components/modals/delete-modal.tsx index 15302e393f1..d4f6b361734 100644 --- a/frontend/public/components/modals/delete-modal.tsx +++ b/frontend/public/components/modals/delete-modal.tsx @@ -3,7 +3,7 @@ import type { FC, ReactNode } from 'react'; import { useState, useCallback, useEffect } from 'react'; import { Alert, Backdrop, Checkbox, Modal, ModalVariant } from '@patternfly/react-core'; import { Trans, useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom-v5-compat'; +import { useNavigate, To } from 'react-router-dom-v5-compat'; import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; import { ModalTitle, @@ -25,7 +25,6 @@ import { YellowExclamationTriangleIcon } from '@console/shared/src/components/st import { ClusterServiceVersionModel } from '@console/operator-lifecycle-manager/src/models'; import { findOwner } from '../../module/k8s/managed-by'; -import { LocationDescriptor } from 'history'; import { usePromiseHandler } from '@console/shared/src/hooks/promise-handler'; //Modal for resource deletion and allows cascading deletes if propagationPolicy is provided for the enum @@ -191,7 +190,7 @@ export const DeleteModalOverlay: OverlayComponent = (props) => export type DeleteModalProps = { kind: K8sModel; resource: K8sResourceKind; - redirectTo?: LocationDescriptor; + redirectTo?: To; message?: JSX.Element; btnText?: ReactNode; deleteAllResources?: () => Promise; diff --git a/frontend/public/components/storage/attach-storage.tsx b/frontend/public/components/storage/attach-storage.tsx index 6d4c31af3bf..4a16de82866 100644 --- a/frontend/public/components/storage/attach-storage.tsx +++ b/frontend/public/components/storage/attach-storage.tsx @@ -19,7 +19,6 @@ import { connectToPlural } from '../../kinds'; export type AttachStorageFormProps = { kindObj: K8sKind; kindsInFlight: any; - history: History; }; type StorageProviderMap = { diff --git a/frontend/public/components/utils/telemetry.ts b/frontend/public/components/utils/telemetry.ts index d1b326f544c..af0351606f8 100644 --- a/frontend/public/components/utils/telemetry.ts +++ b/frontend/public/components/utils/telemetry.ts @@ -1,4 +1,4 @@ -import { Location } from 'history'; +import { Location } from 'react-router-dom-v5-compat'; import { getBrandingDetails } from './branding'; /**