From 811da758e2ff6d0c55bc48ba7a80255bee84eedc Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 20 Oct 2023 21:40:05 -0400 Subject: [PATCH 01/14] build(deps): compatibility router set up --- package.json | 1 + src/app/index.tsx | 13 ++++++++----- yarn.lock | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d501a6aa9..d311c0937 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "react-i18next": "^12.3.1", "react-joyride": "^2.5.3", "react-redux": "^8.0.5", + "react-router-dom-v5-compat": "^6.17.0", "rxjs": "^7.8.0", "semver": "^7.5.4", "showdown": "^2.1.0" diff --git a/src/app/index.tsx b/src/app/index.tsx index 41027a80f..3e37dd48f 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -29,17 +29,20 @@ import * as React from 'react'; import { Provider } from 'react-redux'; import { BrowserRouter as Router } from 'react-router-dom'; import { JoyrideProvider } from './Joyride/JoyrideProvider'; +import { CompatRouter } from 'react-router-dom-v5-compat'; export const App: React.FC = () => ( - - - - - + + + + + + + diff --git a/yarn.lock b/yarn.lock index 1683bb04a..00d815ba2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1389,6 +1389,13 @@ __metadata: languageName: node linkType: hard +"@remix-run/router@npm:1.10.0": + version: 1.10.0 + resolution: "@remix-run/router@npm:1.10.0" + checksum: f8f9fcd5f08465a7e0a05378398ff6df2c5c5ef5766df3490a134d64260b3b16f1bd490bb0c3f5925c2671a0c1d8d1fa01dfbdc7ecc3b2447dc6eafe6b73bcc2 + languageName: node + linkType: hard + "@sideway/address@npm:^4.1.3": version: 4.1.4 resolution: "@sideway/address@npm:4.1.4" @@ -4174,6 +4181,7 @@ __metadata: react-joyride: ^2.5.3 react-redux: ^8.0.5 react-router-dom: ^5.3.4 + react-router-dom-v5-compat: ^6.17.0 react-test-renderer: ^17.0.2 regenerator-runtime: ^0.13.11 rimraf: ^4.1.2 @@ -6943,7 +6951,7 @@ __metadata: languageName: node linkType: hard -"history@npm:^5.0.0": +"history@npm:^5.0.0, history@npm:^5.3.0": version: 5.3.0 resolution: "history@npm:5.3.0" dependencies: @@ -11218,6 +11226,20 @@ __metadata: languageName: node linkType: hard +"react-router-dom-v5-compat@npm:^6.17.0": + version: 6.17.0 + resolution: "react-router-dom-v5-compat@npm:6.17.0" + dependencies: + history: ^5.3.0 + react-router: 6.17.0 + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + react-router-dom: 4 || 5 + checksum: f47a3101168bd0c0de5af044931b263192ab98e6df6f229ffc4f1cc7884fdaa95fb9d477fcca8136b5e305f941d5cc55b3c289827a7c0b1bbff33a0cc71772b7 + languageName: node + linkType: hard + "react-router-dom@npm:^5.3.4": version: 5.3.4 resolution: "react-router-dom@npm:5.3.4" @@ -11254,6 +11276,17 @@ __metadata: languageName: node linkType: hard +"react-router@npm:6.17.0": + version: 6.17.0 + resolution: "react-router@npm:6.17.0" + dependencies: + "@remix-run/router": 1.10.0 + peerDependencies: + react: ">=16.8" + checksum: 99c30d94fbb34657e4c8c3ef1aaae33b143167d3869b442e06c83b4006f35200fde810029180e209654bef2f47f0b27a928f77cc2d859a358a2722cc9d494f03 + languageName: node + linkType: hard + "react-shallow-renderer@npm:^16.13.1": version: 16.15.0 resolution: "react-shallow-renderer@npm:16.15.0" From 7cca5c4c5704d5d719329348ac439b51f1f7a8fe Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 20 Oct 2023 22:28:12 -0400 Subject: [PATCH 02/14] chore(router): update to useNavigate v6 --- src/app/AppLayout/AppLayout.tsx | 26 ++++++++----------- src/app/AppLayout/SslErrorModal.tsx | 10 +++---- src/app/Archives/Archives.tsx | 9 ++++--- .../CreateRecording/CustomRecordingForm.tsx | 14 +++++----- .../CreateRecording/SnapshotRecordingForm.tsx | 10 +++---- .../Charts/jfr/JFRMetricsChartCard.tsx | 12 ++++----- src/app/Dashboard/Dashboard.tsx | 6 ++--- src/app/Dashboard/DashboardSolo.tsx | 9 ++++--- src/app/Events/EventTemplates.tsx | 9 +++---- src/app/Events/Events.tsx | 15 ++++++----- src/app/Recordings/ActiveRecordingsTable.tsx | 10 +++---- src/app/Recordings/Recordings.tsx | 8 +++--- src/app/Rules/CreateRule.tsx | 8 +++--- src/app/Rules/Rules.tsx | 10 +++---- src/app/Settings/Settings.tsx | 9 ++++--- src/app/Topology/Actions/CreateTarget.tsx | 10 +++---- src/app/Topology/Actions/NodeActions.tsx | 12 ++++----- src/app/Topology/Actions/QuickSearchPanel.tsx | 15 ++++++----- src/app/Topology/Actions/WarningResolver.tsx | 9 ++++--- src/app/Topology/Actions/types.ts | 4 +-- src/app/Topology/Actions/utils.tsx | 16 ++++++------ src/app/index.tsx | 2 +- src/app/routes.tsx | 7 ++--- src/app/utils/utils.ts | 6 ++--- 24 files changed, 123 insertions(+), 123 deletions(-) diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index add47198d..9cfb73d74 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -81,7 +81,8 @@ import { import _ from 'lodash'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { Link, matchPath, NavLink, useHistory, useLocation } from 'react-router-dom'; +import { Link, matchPath, NavLink, useLocation } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { map } from 'rxjs/operators'; export interface AppLayoutProps { @@ -92,7 +93,6 @@ export const AppLayout: React.FC = ({ children }) => { const serviceContext = React.useContext(ServiceContext); const notificationsContext = React.useContext(NotificationsContext); const addSubscription = useSubscriptions(); - const routerHistory = useHistory(); const { t } = useTranslation(); const { setState: setJoyState, @@ -117,6 +117,7 @@ export const AppLayout: React.FC = ({ children }) => { const [errorNotificationsCount, setErrorNotificationsCount] = React.useState(0); const [activeLevel, setActiveLevel] = React.useState(FeatureLevel.PRODUCTION); const location = useLocation(); + const navigate = useNavigate(); const [theme] = useTheme(); React.useEffect(() => { @@ -251,10 +252,6 @@ export const AppLayout: React.FC = ({ children }) => { [isMobileView, setIsNavOpen], ); - const handleSettingsButtonClick = React.useCallback(() => { - routerHistory.push('/settings'); - }, [routerHistory]); - const handleNotificationCenterToggle = React.useCallback(() => { notificationsContext.setDrawerState(!isNotificationDrawerExpanded); }, [isNotificationDrawerExpanded, notificationsContext]); @@ -272,13 +269,12 @@ export const AppLayout: React.FC = ({ children }) => { }, [serviceContext.login, addSubscription]); const handleLanguagePref = React.useCallback(() => { - if (routerHistory.location.pathname === '/settings') { + if (location.pathname === '/settings') { selectTab(SettingTab.GENERAL); } else { - const query = new URLSearchParams({ tab: tabAsParam(SettingTab.GENERAL) }); - routerHistory.push(`/settings?${query}`); + navigate(`/settings?${new URLSearchParams({ tab: tabAsParam(SettingTab.GENERAL) })}`); } - }, [routerHistory]); + }, [location, navigate]); const handleUserInfoToggle = React.useCallback(() => setShowUserInfoDropdown((v) => !v), [setShowUserInfoDropdown]); @@ -400,13 +396,14 @@ export const AppLayout: React.FC = ({ children }) => { = ({ children }) => { unreadNotificationsCount, errorNotificationsCount, handleNotificationCenterToggle, - handleSettingsButtonClick, handleHelpToggle, setShowUserInfoDropdown, showUserIcon, @@ -489,7 +485,7 @@ export const AppLayout: React.FC = ({ children }) => { const isActiveRoute = React.useCallback( (route: IAppRoute): boolean => { const match = matchPath(location.pathname, route.path); - if (match && match.isExact) { + if (match) { return true; } else if (route.children) { let childMatch = false; diff --git a/src/app/AppLayout/SslErrorModal.tsx b/src/app/AppLayout/SslErrorModal.tsx index 49a0d7f5d..a5b34151c 100644 --- a/src/app/AppLayout/SslErrorModal.tsx +++ b/src/app/AppLayout/SslErrorModal.tsx @@ -16,7 +16,7 @@ import { portalRoot } from '@app/utils/utils'; import { Button, Modal, ModalVariant, Text } from '@patternfly/react-core'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; export interface SslErrorModalProps { visible: boolean; @@ -24,12 +24,12 @@ export interface SslErrorModalProps { } export const SslErrorModal: React.FC = ({ visible, onDismiss }) => { - const routerHistory = useHistory(); + const navigate = useNavigate(); - const handleClick = () => { - routerHistory.push('/security'); + const handleClick = React.useCallback(() => { + navigate('/security'); onDismiss(); - }; + }, [navigate, onDismiss]); return ( = ({ ...props }) => { const { search, pathname } = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const context = React.useContext(ServiceContext); const addSubscription = useSubscriptions(); @@ -64,8 +65,8 @@ export const Archives: React.FC = ({ ...props }) => { const onTabSelect = React.useCallback( (_: React.MouseEvent, key: string | number) => - switchTab(history, pathname, search, { tabKey: 'tab', tabValue: `${key}` }), - [history, pathname, search], + switchTab(navigate, pathname, search, { tabKey: 'tab', tabValue: `${key}` }), + [navigate, pathname, search], ); const uploadTargetAsObs = React.useMemo(() => of(uploadAsTarget), []); diff --git a/src/app/CreateRecording/CustomRecordingForm.tsx b/src/app/CreateRecording/CustomRecordingForm.tsx index ee83c7a5b..20bdc2510 100644 --- a/src/app/CreateRecording/CustomRecordingForm.tsx +++ b/src/app/CreateRecording/CustomRecordingForm.tsx @@ -48,7 +48,8 @@ import { } from '@patternfly/react-core'; import { HelpIcon } from '@patternfly/react-icons'; import * as React from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { forkJoin } from 'rxjs'; import { first } from 'rxjs/operators'; import { EventTemplateIdentifier, CustomRecordingFormData } from './types'; @@ -57,9 +58,9 @@ import { isDurationValid, isRecordingNameValid } from './utils'; export const CustomRecordingForm: React.FC = () => { const context = React.useContext(ServiceContext); const notifications = React.useContext(NotificationsContext); - const history = useHistory(); + const navigate = useNavigate(); const addSubscription = useSubscriptions(); - const location = useLocation | undefined>(); + const location = useLocation(); const [formData, setFormData] = React.useState({ name: '', @@ -82,7 +83,7 @@ export const CustomRecordingForm: React.FC = () => { const [loading, setLoading] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState(''); - const exitForm = React.useCallback(() => history.goBack(), [history]); + const exitForm = React.useCallback(() => navigate(-1), [navigate]); const handleCreateRecording = React.useCallback( (recordingAttributes: RecordingAttributes) => { @@ -357,6 +358,7 @@ export const CustomRecordingForm: React.FC = () => { }, [addSubscription, context.target, refreshFormOptions]); React.useEffect(() => { + const prefilled: Partial = location.state || {}; const { name, restart, @@ -370,7 +372,7 @@ export const CustomRecordingForm: React.FC = () => { maxSizeUnit, continuous, skipDurationCheck, - } = location.state || {}; + } = prefilled; setFormData((old) => ({ ...old, name: name ?? '', @@ -631,7 +633,7 @@ export const CustomRecordingForm: React.FC = () => { > {loading ? 'Creating' : 'Create'} - diff --git a/src/app/CreateRecording/SnapshotRecordingForm.tsx b/src/app/CreateRecording/SnapshotRecordingForm.tsx index 3fcfed52d..6cbbf724d 100644 --- a/src/app/CreateRecording/SnapshotRecordingForm.tsx +++ b/src/app/CreateRecording/SnapshotRecordingForm.tsx @@ -20,13 +20,13 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { ActionGroup, Button, Form, Text, TextVariants } from '@patternfly/react-core'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { first } from 'rxjs'; export interface SnapshotRecordingFormProps {} export const SnapshotRecordingForm: React.FC = (_) => { - const history = useHistory(); + const navigate = useNavigate(); const addSubscription = useSubscriptions(); const context = React.useContext(ServiceContext); const [loading, setLoading] = React.useState(false); @@ -41,11 +41,11 @@ export const SnapshotRecordingForm: React.FC = (_) = .subscribe((success) => { setLoading(false); if (success) { - history.push('/recordings'); + navigate('/recordings'); } }), ); - }, [addSubscription, context.api, history, setLoading]); + }, [addSubscription, context.api, navigate, setLoading]); const createButtonLoadingProps = React.useMemo( () => @@ -117,7 +117,7 @@ export const SnapshotRecordingForm: React.FC = (_) = - diff --git a/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx b/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx index 7dba0895d..3ba324da6 100644 --- a/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx +++ b/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { CustomRecordingFormData } from '@app/CreateRecording/types'; import { DashboardCardTypeProps, DashboardCardFC, @@ -43,7 +42,7 @@ import { import { DataSourceIcon, ExternalLinkAltIcon, SyncAltIcon, TachometerAltIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { interval } from 'rxjs'; import { DashboardCard } from '../../DashboardCard'; import { ChartContext } from '../context'; @@ -91,7 +90,7 @@ export const JFRMetricsChartCard: DashboardCardFC = (p const [t] = useTranslation(); const serviceContext = React.useContext(ServiceContext); const controllerContext = React.useContext(ChartContext); - const history = useHistory(); + const navigate = useNavigate(); const addSubscription = useSubscriptions(); const [theme] = useTheme(); const [controllerState, setControllerState] = React.useState(ControllerState.NO_DATA); @@ -204,8 +203,7 @@ export const JFRMetricsChartCard: DashboardCardFC = (p }, [props.actions, props.chartKind, props.duration, props.period, t, controllerState, actions]); const handleCreateRecording = React.useCallback(() => { - history.push({ - pathname: '/recordings/create', + navigate('/recordings/create', { state: { name: RECORDING_NAME, template: { @@ -222,9 +220,9 @@ export const JFRMetricsChartCard: DashboardCardFC = (p maxAgeUnit: 1, // seconds maxSize: 100 * 1024 * 1024, maxSizeUnit: 1, // bytes - } as Partial, + }, }); - }, [history]); + }, [navigate]); return ( = (_) => { - const history = useHistory(); + const navigate = useNavigate(); const serviceContext = React.useContext(ServiceContext); const dispatch = useDispatch(); const { t } = useTranslation(); @@ -130,7 +130,7 @@ export const Dashboard: React.FC = (_) => { key={`${cfg.name}-actions`} onRemove={() => handleRemove(idx)} onResetSize={() => handleResetSize(idx)} - onView={() => history.push(`/d-solo?layout=${currLayout.name}&cardId=${cfg.id}`)} + onView={() => navigate(`/d-solo?layout=${currLayout.name}&cardId=${cfg.id}`)} />, ], }) diff --git a/src/app/Dashboard/DashboardSolo.tsx b/src/app/Dashboard/DashboardSolo.tsx index 6b0297af1..13632114a 100644 --- a/src/app/Dashboard/DashboardSolo.tsx +++ b/src/app/Dashboard/DashboardSolo.tsx @@ -19,7 +19,8 @@ import { Bullseye, Button, EmptyState, EmptyStateBody, EmptyStateIcon, Title } f import { MonitoringIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { useSelector } from 'react-redux'; -import { useHistory, useLocation, withRouter } from 'react-router-dom'; +import { useLocation, withRouter } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { CardConfig } from './types'; import { getCardDescriptorByName } from './utils'; @@ -27,7 +28,7 @@ export interface DashboardSoloProps {} const DashboardSolo: React.FC = () => { const { search } = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const dashboardConfigs = useSelector((state: RootState) => state.dashboardConfigs); @@ -59,7 +60,7 @@ const DashboardSolo: React.FC = () => { Provide valid layout and cardId query parameters and try again. - @@ -84,7 +85,7 @@ const DashboardSolo: React.FC = () => { ); } - }, [cardConfig, history]); + }, [cardConfig, navigate]); return content; }; diff --git a/src/app/Events/EventTemplates.tsx b/src/app/Events/EventTemplates.tsx index 97a062272..7d6c7ef12 100644 --- a/src/app/Events/EventTemplates.tsx +++ b/src/app/Events/EventTemplates.tsx @@ -58,7 +58,7 @@ import { Tr, } from '@patternfly/react-table'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { forkJoin, Observable, of } from 'rxjs'; import { catchError, concatMap, defaultIfEmpty, filter, first, tap } from 'rxjs/operators'; @@ -89,7 +89,7 @@ export interface EventTemplatesProps {} export const EventTemplates: React.FC = (_) => { const context = React.useContext(ServiceContext); - const history = useHistory(); + const navigate = useNavigate(); const [templates, setTemplates] = React.useState([]); const [filteredTemplates, setFilteredTemplates] = React.useState([]); @@ -256,8 +256,7 @@ export const EventTemplates: React.FC = (_) => { { title: 'Create Recording...', onClick: () => - history.push({ - pathname: '/recordings/create', + navigate('/recordings/create', { state: { template: { name: t.name, type: t.type } } as Partial, }), }, @@ -284,7 +283,7 @@ export const EventTemplates: React.FC = (_) => { } return actions; }, - [context.api, history, handleDeleteButton], + [context.api, navigate, handleDeleteButton], ); const handleUploadModalClose = React.useCallback(() => { diff --git a/src/app/Events/Events.tsx b/src/app/Events/Events.tsx index 033111242..48e06fd8b 100644 --- a/src/app/Events/Events.tsx +++ b/src/app/Events/Events.tsx @@ -21,7 +21,8 @@ import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { getActiveTab, switchTab } from '@app/utils/utils'; import { Card, CardBody, Stack, StackItem, Tab, Tabs, Tooltip } from '@patternfly/react-core'; import * as React from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { concatMap, filter } from 'rxjs'; import { EventTemplates } from './EventTemplates'; import { EventTypes } from './EventTypes'; @@ -58,7 +59,7 @@ enum EventTab { export const EventTabs: React.FC = () => { const { search, pathname } = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const activeTab = React.useMemo(() => { return getActiveTab(search, 'eventTab', Object.values(EventTab), EventTab.EVENT_TEMPLATE); @@ -66,8 +67,8 @@ export const EventTabs: React.FC = () => { const onTabSelect = React.useCallback( (_: React.MouseEvent, key: string | number) => - switchTab(history, pathname, search, { tabKey: 'eventTab', tabValue: `${key}` }), - [history, pathname, search], + switchTab(navigate, pathname, search, { tabKey: 'eventTab', tabValue: `${key}` }), + [navigate, pathname, search], ); return ( @@ -92,7 +93,7 @@ export const AgentTabs: React.FC = () => { const addSubscription = useSubscriptions(); const { search, pathname } = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const activeTab = React.useMemo(() => { return getActiveTab(search, 'agentTab', Object.values(AgentTab), AgentTab.AGENT_TEMPLATE); @@ -102,8 +103,8 @@ export const AgentTabs: React.FC = () => { const onTabSelect = React.useCallback( (_: React.MouseEvent, key: string | number) => - switchTab(history, pathname, search, { tabKey: 'agentTab', tabValue: `${key}` }), - [history, pathname, search], + switchTab(navigate, pathname, search, { tabKey: 'agentTab', tabValue: `${key}` }), + [navigate, pathname, search], ); React.useEffect(() => { diff --git a/src/app/Recordings/ActiveRecordingsTable.tsx b/src/app/Recordings/ActiveRecordingsTable.tsx index b1d4c861c..962d252bc 100644 --- a/src/app/Recordings/ActiveRecordingsTable.tsx +++ b/src/app/Recordings/ActiveRecordingsTable.tsx @@ -78,7 +78,7 @@ import { RedoIcon } from '@patternfly/react-icons'; import { ExpandableRowContent, SortByDirection, Tbody, Td, Tr } from '@patternfly/react-table'; import * as React from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useHistory, useRouteMatch } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { combineLatest, forkJoin, merge, Observable } from 'rxjs'; import { concatMap, filter, first } from 'rxjs/operators'; import { DeleteWarningModal } from '../Modal/DeleteWarningModal'; @@ -136,9 +136,7 @@ export interface ActiveRecordingsTableProps { export const ActiveRecordingsTable: React.FC = (props) => { const context = React.useContext(ServiceContext); - - const routerHistory = useHistory(); - const { url } = useRouteMatch(); + const navigate = useNavigate(); const addSubscription = useSubscriptions(); const dispatch = useDispatch(); @@ -187,8 +185,8 @@ export const ActiveRecordingsTable: React.FC = (prop ); const handleCreateRecording = React.useCallback(() => { - routerHistory.push(`${url}/create`); - }, [routerHistory, url]); + navigate('/recordings/create'); + }, [navigate]); const handleEditLabels = React.useCallback(() => { setShowDetailsPanel(true); diff --git a/src/app/Recordings/Recordings.tsx b/src/app/Recordings/Recordings.tsx index a6bbe1b7a..f72c3c1fb 100644 --- a/src/app/Recordings/Recordings.tsx +++ b/src/app/Recordings/Recordings.tsx @@ -19,7 +19,7 @@ import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { getActiveTab, switchTab } from '@app/utils/utils'; import { Card, CardBody, CardTitle, Tab, Tabs, TabTitleText } from '@patternfly/react-core'; import * as React from 'react'; -import { useHistory, useLocation } from 'react-router'; +import { useLocation, useNavigate } from 'react-router'; import { ActiveRecordingsTable } from './ActiveRecordingsTable'; import { ArchivedRecordingsTable } from './ArchivedRecordingsTable'; @@ -32,7 +32,7 @@ export interface RecordingsProps {} export const Recordings: React.FC = ({ ...props }) => { const { search, pathname } = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const context = React.useContext(ServiceContext); const addSubscription = useSubscriptions(); @@ -48,8 +48,8 @@ export const Recordings: React.FC = ({ ...props }) => { const onTabSelect = React.useCallback( (_: React.MouseEvent, key: string | number) => - switchTab(history, pathname, search, { tabKey: 'tab', tabValue: `${key}` }), - [history, pathname, search], + switchTab(navigate, pathname, search, { tabKey: 'tab', tabValue: `${key}` }), + [navigate, pathname, search], ); const targetAsObs = React.useMemo(() => context.target.target(), [context.target]); diff --git a/src/app/Rules/CreateRule.tsx b/src/app/Rules/CreateRule.tsx index 0033df2ac..72ab92f57 100644 --- a/src/app/Rules/CreateRule.tsx +++ b/src/app/Rules/CreateRule.tsx @@ -53,7 +53,7 @@ import { import { HelpIcon } from '@patternfly/react-icons'; import _ from 'lodash'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { combineLatest, forkJoin, iif, of, Subject } from 'rxjs'; import { catchError, debounceTime, map, switchMap, tap } from 'rxjs/operators'; import { RuleFormData } from './types'; @@ -64,7 +64,7 @@ export interface CreateRuleFormProps {} export const CreateRuleForm: React.FC = (_props) => { const context = React.useContext(ServiceContext); const notifications = React.useContext(NotificationsContext); - const history = useHistory(); + const navigate = useNavigate(); // Do not use useSearchExpression for display. This causes the cursor to jump to the end due to async updates. const matchExprService = useMatchExpressionSvc(); const addSubscription = useSubscriptions(); @@ -205,7 +205,7 @@ export const CreateRuleForm: React.FC = (_props) => { [setFormData], ); - const exitForm = React.useCallback(() => history.push('/rules'), [history]); + const exitForm = React.useCallback(() => navigate('/rules'), [navigate]); const handleSubmit = React.useCallback((): void => { const notificationMessages: string[] = []; @@ -637,7 +637,7 @@ enabled in the future.`} > {loading ? 'Creating' : 'Create'} - diff --git a/src/app/Rules/Rules.tsx b/src/app/Rules/Rules.tsx index f22f5203f..c10b7a832 100644 --- a/src/app/Rules/Rules.tsx +++ b/src/app/Rules/Rules.tsx @@ -51,7 +51,8 @@ import { Tr, } from '@patternfly/react-table'; import * as React from 'react'; -import { Link, useHistory, useRouteMatch } from 'react-router-dom'; +import { Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { first } from 'rxjs/operators'; import { RuleDeleteWarningModal } from './RuleDeleteWarningModal'; import { RuleUploadModal } from './RulesUploadModal'; @@ -118,10 +119,9 @@ export interface RulesTableProps {} export const RulesTable: React.FC = (_) => { const context = React.useContext(ServiceContext); - const routerHistory = useHistory(); + const navigate = useNavigate(); const addSubscription = useSubscriptions(); - const { url } = useRouteMatch(); const [isLoading, setIsLoading] = React.useState(false); const [sortBy, setSortBy] = React.useState({} as ISortBy); const [rules, setRules] = React.useState([] as Rule[]); @@ -202,8 +202,8 @@ export const RulesTable: React.FC = (_) => { }, [context.settings, refreshRules]); const handleCreateRule = React.useCallback(() => { - routerHistory.push(`${url}/create`); - }, [routerHistory, url]); + navigate('/rules/create'); + }, [navigate]); const handleUploadRule = React.useCallback(() => { setIsUploadModalOpen(true); diff --git a/src/app/Settings/Settings.tsx b/src/app/Settings/Settings.tsx index 92f75f91e..87305684f 100644 --- a/src/app/Settings/Settings.tsx +++ b/src/app/Settings/Settings.tsx @@ -37,7 +37,8 @@ import { } from '@patternfly/react-core'; import * as React from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { AutomatedAnalysis } from './Config/AutomatedAnalysis'; import { AutoRefresh } from './Config/AutoRefresh'; import { ChartCards } from './Config/ChartCards'; @@ -96,7 +97,7 @@ export const Settings: React.FC = (_) => { [t, loggedIn], ); - const history = useHistory(); + const navigate = useNavigate(); const { search, pathname } = useLocation(); const activeTab = React.useMemo(() => { @@ -112,8 +113,8 @@ export const Settings: React.FC = (_) => { const onTabSelect = React.useCallback( (_: React.MouseEvent, key: string | number) => - switchTab(history, pathname, search, { tabKey: 'tab', tabValue: `${tabAsParam(key as SettingTab)}` }), - [history, pathname, search], + switchTab(navigate, pathname, search, { tabKey: 'tab', tabValue: `${tabAsParam(key as SettingTab)}` }), + [navigate, pathname, search], ); const settingGroups = React.useMemo(() => { diff --git a/src/app/Topology/Actions/CreateTarget.tsx b/src/app/Topology/Actions/CreateTarget.tsx index f281f0c17..be9cb3a2b 100644 --- a/src/app/Topology/Actions/CreateTarget.tsx +++ b/src/app/Topology/Actions/CreateTarget.tsx @@ -58,7 +58,7 @@ import { CheckCircleIcon, ExclamationCircleIcon, PendingIcon, SyncAltIcon } from import { css } from '@patternfly/react-styles'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; export const isValidTargetConnectURL = (connectUrl?: string) => connectUrl && !connectUrl.match(/\s+/); @@ -74,7 +74,7 @@ export interface CreateTargetProps { export const CreateTarget: React.FC = ({ prefilled }) => { const addSubscription = useSubscriptions(); const context = React.useContext(ServiceContext); - const history = useHistory(); + const navigate = useNavigate(); const [t] = useTranslation(); const [example, setExample] = React.useState(''); @@ -184,7 +184,7 @@ export const CreateTarget: React.FC = ({ prefilled }) => { setLoading(false); const option = isHttpOk(status) ? ValidatedOptions.success : ValidatedOptions.error; if (option === ValidatedOptions.success) { - history.push('/topology'); + navigate('/topology'); } else { setValidation({ option: option, @@ -193,7 +193,7 @@ export const CreateTarget: React.FC = ({ prefilled }) => { } }), ); - }, [setLoading, setValidation, addSubscription, context.api, connectUrl, alias, history, credentials]); + }, [setLoading, setValidation, addSubscription, context.api, connectUrl, alias, navigate, credentials]); const testTarget = React.useCallback(() => { if (!isValidTargetConnectURL(connectUrl)) { @@ -223,7 +223,7 @@ export const CreateTarget: React.FC = ({ prefilled }) => { resetTestState(); }, [connectUrl, alias, credentials, addSubscription, context.api, resetTestState, setTesting]); - const handleCancel = React.useCallback(() => history.goBack(), [history]); + const handleCancel = React.useCallback(() => navigate(-1), [navigate]); React.useEffect(() => { if (prefilled) { diff --git a/src/app/Topology/Actions/NodeActions.tsx b/src/app/Topology/Actions/NodeActions.tsx index 86acb0d31..89a6bde63 100644 --- a/src/app/Topology/Actions/NodeActions.tsx +++ b/src/app/Topology/Actions/NodeActions.tsx @@ -21,7 +21,7 @@ import { Dropdown, DropdownItem, DropdownProps, DropdownToggle } from '@patternf import { css } from '@patternfly/react-styles'; import { ContextMenuItem as PFContextMenuItem } from '@patternfly/react-topology'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { Observable, Subject, switchMap } from 'rxjs'; import { GraphElement, ListElement } from '../Shared/types'; import { ActionUtils, MenuItemComponent, MenuItemVariant, NodeActionFunction } from './types'; @@ -45,7 +45,7 @@ export const ContextMenuItem: React.FC = ({ isDisabled, ...props }) => { - const history = useHistory(); + const navigate = useNavigate(); const addSubscription = useSubscriptions(); const services = React.useContext(ServiceContext); const notifications = React.useContext(NotificationsContext); @@ -57,9 +57,9 @@ export const ContextMenuItem: React.FC = ({ const handleOnclick = React.useCallback( (e: MouseEvent) => { e.stopPropagation(); - onClick && onClick(element, { history, services, notifications }); + onClick && onClick(element, { navigate, services, notifications }); }, - [onClick, history, services, notifications, element], + [onClick, navigate, services, notifications, element], ); React.useEffect(() => { @@ -69,13 +69,13 @@ export const ContextMenuItem: React.FC = ({ .pipe( switchMap((element) => { setDisabled(true); - return isDisabled(element, { services, notifications, history }); + return isDisabled(element, { services, notifications, navigate }); }), ) .subscribe(setDisabled), ); } - }, [addSubscription, elementSubj, isDisabled, setDisabled, services, notifications, history]); + }, [addSubscription, elementSubj, isDisabled, setDisabled, services, notifications, navigate]); React.useEffect(() => { elementSubj.next(element); diff --git a/src/app/Topology/Actions/QuickSearchPanel.tsx b/src/app/Topology/Actions/QuickSearchPanel.tsx index 35d716be8..aa4d67555 100644 --- a/src/app/Topology/Actions/QuickSearchPanel.tsx +++ b/src/app/Topology/Actions/QuickSearchPanel.tsx @@ -48,19 +48,20 @@ import { SearchIcon } from '@patternfly/react-icons'; import { css } from '@patternfly/react-styles'; import { useHover } from '@patternfly/react-topology'; import * as React from 'react'; -import { Link, useHistory } from 'react-router-dom'; +import { Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import QuickSearchIcon from '../../Shared/Components/QuickSearchIcon'; import quickSearches, { QuickSearchId, quickSearchIds } from './quicksearches/all-quick-searches'; import { QuickSearchItem } from './types'; export const QuickSearchTabContent: React.FC<{ item?: QuickSearchItem }> = ({ item, ...props }) => { - const history = useHistory(); + const navigate = useNavigate(); const services = React.useContext(ServiceContext); const notifications = React.useContext(NotificationsContext); const handleActionClick = React.useCallback(() => { - item?.createAction && item.createAction({ history, services, notifications }); - }, [item, history, services, notifications]); + item?.createAction && item.createAction({ navigate, services, notifications }); + }, [item, navigate, services, notifications]); return item ? ( @@ -285,7 +286,7 @@ export interface QuickSearchFlyoutMenuProps { } export const QuickSearchFlyoutMenu: React.FC = ({ isShow, ...props }) => { - const history = useHistory(); + const navigate = useNavigate(); const services = React.useContext(ServiceContext); const notifications = React.useContext(NotificationsContext); const activeLevel = useFeatureLevel(); @@ -307,12 +308,12 @@ export const QuickSearchFlyoutMenu: React.FC = ({ is {icon} } - onClick={() => createAction({ history, services, notifications })} + onClick={() => createAction({ navigate, services, notifications })} > {name} )); - }, [filteredQuicksearches, history, services, notifications]); + }, [filteredQuicksearches, navigate, services, notifications]); return isShow || hover ? ( {} @@ -43,13 +44,13 @@ export const WarningResolverAsActionButton: React.FC { - const history = useHistory(); + const navigate = useNavigate(); const services = React.useContext(ServiceContext); const notifications = React.useContext(NotificationsContext); const handleClick = React.useCallback(() => { - onClick && onClick(targetNode, { history, services, notifications }); - }, [onClick, targetNode, history, services, notifications]); + onClick && onClick(targetNode, { navigate, services, notifications }); + }, [onClick, targetNode, navigate, services, notifications]); return ( - - -
- - -
-
-
-
- - -
-
-
- , + errorElement +
+ prop on your route. +

, ] `; diff --git a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx index 581f1b9ea..e9dcd0c10 100644 --- a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx @@ -29,7 +29,7 @@ import { defaultServices } from '@app/Shared/Services/Services'; import '@testing-library/jest-dom'; import { cleanup, screen, waitFor } from '@testing-library/react'; import { of } from 'rxjs'; -import { basePreloadedState, renderWithServiceContextAndReduxStore, testT } from '../../Common'; +import { basePreloadedState, render, testT } from '../../utils'; jest.mock('@app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCardList', () => { return { @@ -223,8 +223,16 @@ describe('', () => { it('renders report generation error view correctly', async () => { jest.spyOn(defaultServices.api, 'graphql').mockReturnValueOnce(of(mockActiveRecordingsResponse)); jest.spyOn(defaultServices.reports, 'reportJson').mockReturnValueOnce(of()); - renderWithServiceContextAndReduxStore(, { - preloadState: preloadedState, + render({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + preloadedState: preloadedState, }); expect(screen.getByText(testT('AutomatedAnalysisCard.ERROR_TITLE'))).toBeInTheDocument(); // Error view @@ -240,8 +248,16 @@ describe('', () => { jest.spyOn(defaultServices.api, 'graphql').mockReturnValueOnce(of(mockEmptyArchivedRecordingsResponse)); const requestSpy = jest.spyOn(defaultServices.api, 'createRecording').mockReturnValueOnce(of()); - const { user } = renderWithServiceContextAndReduxStore(, { - preloadState: preloadedState, + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + preloadedState: preloadedState, }); expect(screen.getByText(testT('AutomatedAnalysisCard.ERROR_TITLE'))).toBeInTheDocument(); // Error view @@ -263,8 +279,16 @@ describe('', () => { jest.spyOn(defaultServices.api, 'graphql').mockReturnValueOnce(of(mockActiveRecordingsResponse)); jest.spyOn(defaultServices.reports, 'reportJson').mockReturnValueOnce(of(mockEvaluations)); - renderWithServiceContextAndReduxStore(, { - preloadState: preloadedState, + render({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + preloadedState: preloadedState, }); expect(screen.getByText(testT('AutomatedAnalysisCard.CARD_TITLE'))).toBeInTheDocument(); // Card title @@ -313,8 +337,16 @@ describe('', () => { jest.spyOn(defaultServices.api, 'graphql').mockReturnValueOnce(of(mockArchivedRecordingsResponse)); jest.spyOn(defaultServices.reports, 'reportJson').mockReturnValueOnce(of(mockEvaluations)); - renderWithServiceContextAndReduxStore(, { - preloadState: preloadedState, + render({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + preloadedState: preloadedState, }); expect(screen.getByText(testT('AutomatedAnalysisCard.CARD_TITLE'))).toBeInTheDocument(); // Card title @@ -367,6 +399,7 @@ describe('', () => { const newPreloadedState = { ...preloadedState, automatedAnalysisFilters: { + _version: '0', targetFilters: [], globalFilters: { filters: { @@ -376,8 +409,16 @@ describe('', () => { }, }; - renderWithServiceContextAndReduxStore(, { - preloadState: newPreloadedState, + render({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + preloadedState: newPreloadedState, }); expect(screen.getByText(testT('AutomatedAnalysisCard.CARD_TITLE'))).toBeInTheDocument(); // Card title @@ -422,8 +463,16 @@ describe('', () => { jest.spyOn(defaultServices.api, 'graphql').mockReturnValueOnce(of(mockActiveRecordingsResponse)); jest.spyOn(defaultServices.reports, 'reportJson').mockReturnValueOnce(of(mockFilteredEvaluations)); - const { user } = renderWithServiceContextAndReduxStore(, { - preloadState: preloadedState, // Filter score default = 100 + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + preloadedState: preloadedState, }); expect(screen.getByText(mockRuleEvaluation1.name)).toBeInTheDocument(); // Score: 100 @@ -481,8 +530,17 @@ describe('', () => { it('renders list view correctly', async () => { jest.spyOn(defaultServices.api, 'graphql').mockReturnValueOnce(of(mockActiveRecordingsResponse)); jest.spyOn(defaultServices.reports, 'reportJson').mockReturnValueOnce(of(mockFilteredEvaluations)); - const { user } = renderWithServiceContextAndReduxStore(, { - preloadState: preloadedState, // Filter score default = 100 + + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + preloadedState: preloadedState, }); const listViewSwitch = screen.getByRole('checkbox', { diff --git a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCardList.test.tsx b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCardList.test.tsx index d0170ec49..a8778e019 100644 --- a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCardList.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCardList.test.tsx @@ -14,12 +14,8 @@ * limitations under the License. */ import { AutomatedAnalysisCardList } from '@app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCardList'; -import { store } from '@app/Shared/Redux/ReduxStore'; import { AnalysisResult, CategorizedRuleEvaluations } from '@app/Shared/Services/api.types'; -import { NotificationsContext, NotificationsInstance } from '@app/Shared/Services/Notifications.service'; -import { defaultServices, ServiceContext } from '@app/Shared/Services/Services'; -import { Provider } from 'react-redux'; -import renderer, { act } from 'react-test-renderer'; +import { renderSnapshot } from '@test/utils'; const mockRuleEvaluation1: AnalysisResult = { name: 'rule1', @@ -86,17 +82,15 @@ const mockCategorizedEvaluations: CategorizedRuleEvaluations[] = [ describe('', () => { it('renders correctly', async () => { - let tree; - await act(async () => { - tree = renderer.create( - - - - - - - , - ); + const tree = renderSnapshot({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, }); expect(tree.toJSON()).toMatchSnapshot(); }); diff --git a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigDrawer.test.tsx b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigDrawer.test.tsx index d6c9ff7c8..7a452bc80 100644 --- a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigDrawer.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigDrawer.test.tsx @@ -20,7 +20,7 @@ import { defaultServices } from '@app/Shared/Services/Services'; import '@testing-library/jest-dom'; import { cleanup, screen } from '@testing-library/react'; import { of } from 'rxjs'; -import { renderWithServiceContext } from '../../Common'; +import { render } from '../../utils'; const drawerContent =
Drawer Content
; @@ -44,14 +44,23 @@ describe('', () => { }); it('renders default view correctly', async () => { - renderWithServiceContext( - undefined} - onError={() => undefined} - />, - ); + render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + undefined} + onError={() => undefined} + /> + ), + }, + ], + }, + }); expect(screen.getByText(/drawer content/i)).toBeInTheDocument(); const createRecording = screen.queryByRole('button', { @@ -65,14 +74,23 @@ describe('', () => { }); it('opens drawer when button clicked', async () => { - const { user } = renderWithServiceContext( - undefined} - onError={() => undefined} - />, - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + undefined} + onError={() => undefined} + /> + ), + }, + ], + }, + }); expect(screen.getByText(/drawer content/i)).toBeInTheDocument(); const recordingActions = screen.getByRole('button', { @@ -86,14 +104,23 @@ describe('', () => { it('creates a recording when Create Recording is clicked', async () => { const onCreateFunction = jest.fn(); const requestSpy = jest.spyOn(defaultServices.api, 'createRecording'); - const { user } = renderWithServiceContext( - undefined} - />, - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + undefined} + /> + ), + }, + ], + }, + }); const createRecording = screen.getByRole('button', { name: /create recording/i, diff --git a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.test.tsx b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.test.tsx index 7c2644491..76f56848c 100644 --- a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.test.tsx @@ -20,7 +20,7 @@ import { defaultServices } from '@app/Shared/Services/Services'; import '@testing-library/jest-dom'; import { cleanup, screen } from '@testing-library/react'; import { of } from 'rxjs'; -import { renderWithServiceContext, testT } from '../../Common'; +import { render, testT } from '../../utils'; const mockTarget = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' }; @@ -71,7 +71,11 @@ describe('', () => { afterEach(cleanup); it('renders default view correctly', async () => { - renderWithServiceContext(); + render({ + routerConfigs: { + routes: [{ path: '/', element: }], + }, + }); expect(screen.getByText(/Current Configuration/i)).toBeInTheDocument(); expect(screen.getByText('Template')).toBeInTheDocument(); @@ -80,7 +84,11 @@ describe('', () => { }); it('renders editing drawer view correctly', async () => { - const { user } = renderWithServiceContext(); + const { user } = render({ + routerConfigs: { + routes: [{ path: '/', element: }], + }, + }); expect(screen.getByText(/profiling recording configuration/i)).toBeInTheDocument(); // Form title @@ -101,7 +109,11 @@ describe('', () => { }); it('renders editing settings view correctly', async () => { - const { user } = renderWithServiceContext(); + const { user } = render({ + routerConfigs: { + routes: [{ path: '/', element: }], + }, + }); expect(screen.queryByText(/profiling recording configuration/i)).not.toBeInTheDocument(); // Form title @@ -119,7 +131,11 @@ describe('', () => { it('saves configuration', async () => { const setConfigRequestSpy = jest.spyOn(defaultServices.settings, 'setAutomatedAnalysisRecordingConfig'); - const { user } = renderWithServiceContext(); + const { user } = render({ + routerConfigs: { + routes: [{ path: '/', element: }], + }, + }); await user.click( screen.getByRole('button', { diff --git a/src/test/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel.test.tsx b/src/test/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel.test.tsx index 77496e631..98e1bb7bd 100644 --- a/src/test/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel.test.tsx @@ -17,7 +17,7 @@ import { ClickableAutomatedAnalysisLabel } from '@app/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel'; import { AnalysisResult } from '@app/Shared/Services/api.types'; import { act, cleanup, screen, within } from '@testing-library/react'; -import { renderDefault } from '../../Common'; +import { render } from '../../utils'; const mockRuleEvaluation1: AnalysisResult = { name: 'rule1', @@ -95,13 +95,21 @@ describe('', () => { afterEach(cleanup); it('displays label', async () => { - renderDefault(); + render({ + routerConfigs: { + routes: [{ path: '/', element: }], + }, + }); expect(screen.getByText(mockRuleEvaluation1.name)).toBeInTheDocument(); }); it('displays popover when critical label is clicked', async () => { - const { user } = renderDefault(); + const { user } = render({ + routerConfigs: { + routes: [{ path: '/', element: }], + }, + }); expect(screen.getByText(mockRuleEvaluation1.name)).toBeInTheDocument(); @@ -151,7 +159,11 @@ describe('', () => { }); it('displays popover when warning label is clicked', async () => { - const { user } = renderDefault(); + const { user } = render({ + routerConfigs: { + routes: [{ path: '/', element: }], + }, + }); expect(screen.getByText(mockRuleEvaluation2.name)).toBeInTheDocument(); @@ -201,7 +213,11 @@ describe('', () => { }); it('displays popover when ok label is clicked', async () => { - const { user } = renderDefault(); + const { user } = render({ + routerConfigs: { + routes: [{ path: '/', element: }], + }, + }); expect(screen.getByText(mockRuleEvaluation3.name)).toBeInTheDocument(); @@ -250,7 +266,11 @@ describe('', () => { }); it('displays popover when N/A label is clicked', async () => { - const { user } = renderDefault(); + const { user } = render({ + routerConfigs: { + routes: [{ path: '/', element: }], + }, + }); expect(screen.getByText(mockNaRuleEvaluation.name)).toBeInTheDocument(); diff --git a/src/test/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisNameFilter.test.tsx b/src/test/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisNameFilter.test.tsx index be1128673..84b6f1278 100644 --- a/src/test/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisNameFilter.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisNameFilter.test.tsx @@ -17,7 +17,7 @@ import { AutomatedAnalysisNameFilter } from '@app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisNameFilter'; import { AnalysisResult, CategorizedRuleEvaluations } from '@app/Shared/Services/api.types'; import { cleanup, screen, within } from '@testing-library/react'; -import { renderDefault } from '../../../Common'; +import { render } from '../../../utils'; const mockRuleEvaluation1: AnalysisResult = { name: 'rule1', @@ -118,13 +118,22 @@ describe('', () => { afterEach(cleanup); it('display name selections when text input is clicked', async () => { - const { user } = renderDefault( - , - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + + ), + }, + ], + }, + }); const nameInput = screen.getByLabelText('Filter by name...'); expect(nameInput).toBeInTheDocument(); expect(nameInput).toBeVisible(); @@ -143,13 +152,22 @@ describe('', () => { }); it('display name selections when dropdown arrow is clicked', async () => { - const { user } = renderDefault( - , - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + + ), + }, + ], + }, + }); const dropDownArrow = screen.getByRole('button', { name: 'Options menu' }); expect(dropDownArrow).toBeInTheDocument(); expect(dropDownArrow).toBeVisible(); @@ -168,13 +186,22 @@ describe('', () => { }); it('should close selection menu when toggled with dropdown arrow', async () => { - const { user } = renderDefault( - , - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + + ), + }, + ], + }, + }); const dropDownArrow = screen.getByRole('button', { name: 'Options menu' }); expect(dropDownArrow).toBeInTheDocument(); @@ -198,13 +225,22 @@ describe('', () => { }); it('should close selection menu when toggled with text input', async () => { - const { user } = renderDefault( - , - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + + ), + }, + ], + }, + }); const nameInput = screen.getByLabelText('Filter by name...'); expect(nameInput).toBeInTheDocument(); @@ -228,13 +264,22 @@ describe('', () => { }); it('should not display selected names', async () => { - const { user } = renderDefault( - , - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + + ), + }, + ], + }, + }); const nameInput = screen.getByLabelText('Filter by name...'); expect(nameInput).toBeInTheDocument(); @@ -253,13 +298,22 @@ describe('', () => { it('should select a name when a name option is clicked', async () => { const submitNameInput = jest.fn((nameInput) => emptyFilteredNames.push(nameInput)); - const { user } = renderDefault( - , - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + + ), + }, + ], + }, + }); const nameInput = screen.getByLabelText('Filter by name...'); expect(nameInput).toBeInTheDocument(); diff --git a/src/test/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisScoreFilter.test.tsx b/src/test/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisScoreFilter.test.tsx index dd92e575e..814eac12f 100644 --- a/src/test/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisScoreFilter.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisScoreFilter.test.tsx @@ -18,7 +18,7 @@ import { AutomatedAnalysisScoreFilter } from '@app/Dashboard/AutomatedAnalysis/F import { RootState } from '@app/Shared/Redux/ReduxStore'; import { cleanup, screen } from '@testing-library/react'; -import { basePreloadedState, renderWithReduxStore } from '../../../Common'; +import { basePreloadedState, render } from '../../../utils'; const onlyShowingText = (value: number) => `Only showing analysis results with severity scores ≥ ${value}:`; @@ -43,9 +43,18 @@ describe('', () => { afterEach(cleanup); it('resets to 0 and 100 when clicking reset buttons', async () => { - const { user } = renderWithReduxStore(, { - preloadState: preloadedState, + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + preloadedState: preloadedState, }); + const resetTo0Button = screen.getByRole('button', { name: /reset score to 0/i, }); @@ -66,9 +75,18 @@ describe('', () => { }); it('responds to score filter changes', async () => { - const { user } = renderWithReduxStore(, { - preloadState: preloadedState, + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + preloadedState: preloadedState, }); + const sliderValue = screen.getByRole('spinbutton', { name: /slider value input/i, }); diff --git a/src/test/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisTopicFilter.test.tsx b/src/test/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisTopicFilter.test.tsx index 43d0546b1..ce2f1067d 100644 --- a/src/test/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisTopicFilter.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisTopicFilter.test.tsx @@ -17,7 +17,7 @@ import { AutomatedAnalysisTopicFilter } from '@app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisTopicFilter'; import { AnalysisResult, CategorizedRuleEvaluations } from '@app/Shared/Services/api.types'; import { cleanup, screen, within } from '@testing-library/react'; -import { renderDefault } from '../../../Common'; +import { render } from '../../../utils'; const mockRuleEvaluation1: AnalysisResult = { name: 'rule1', @@ -118,13 +118,22 @@ describe('', () => { afterEach(cleanup); it('display topic selections when text input is clicked', async () => { - const { user } = renderDefault( - , - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + + ), + }, + ], + }, + }); const nameInput = screen.getByLabelText('Filter by topic...'); expect(nameInput).toBeInTheDocument(); expect(nameInput).toBeVisible(); @@ -143,13 +152,22 @@ describe('', () => { }); it('display topic selections when dropdown arrow is clicked', async () => { - const { user } = renderDefault( - , - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + + ), + }, + ], + }, + }); const dropDownArrow = screen.getByRole('button', { name: 'Options menu' }); expect(dropDownArrow).toBeInTheDocument(); expect(dropDownArrow).toBeVisible(); @@ -168,13 +186,22 @@ describe('', () => { }); it('should close selection menu when toggled with dropdown arrow', async () => { - const { user } = renderDefault( - , - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + + ), + }, + ], + }, + }); const dropDownArrow = screen.getByRole('button', { name: 'Options menu' }); expect(dropDownArrow).toBeInTheDocument(); @@ -198,13 +225,22 @@ describe('', () => { }); it('should close selection menu when toggled with text input', async () => { - const { user } = renderDefault( - , - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + + ), + }, + ], + }, + }); const nameInput = screen.getByLabelText('Filter by topic...'); expect(nameInput).toBeInTheDocument(); @@ -228,13 +264,22 @@ describe('', () => { }); it('should not display selected topics', async () => { - const { user } = renderDefault( - , - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + + ), + }, + ], + }, + }); const nameInput = screen.getByLabelText('Filter by topic...'); expect(nameInput).toBeInTheDocument(); @@ -253,13 +298,22 @@ describe('', () => { it('should select a topic when a topic option is clicked', async () => { const submitNameInput = jest.fn((nameInput) => emptyFilteredTopics.push(nameInput)); - const { user } = renderDefault( - , - ); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + + ), + }, + ], + }, + }); const nameInput = screen.getByLabelText('Filter by topic...'); expect(nameInput).toBeInTheDocument(); diff --git a/src/test/Dashboard/Charts/jfr/JFRMetricsChartCard.test.tsx b/src/test/Dashboard/Charts/jfr/JFRMetricsChartCard.test.tsx index 043451142..f54307af4 100644 --- a/src/test/Dashboard/Charts/jfr/JFRMetricsChartCard.test.tsx +++ b/src/test/Dashboard/Charts/jfr/JFRMetricsChartCard.test.tsx @@ -21,24 +21,11 @@ import { JFRMetricsChartCard, kindToId } from '@app/Dashboard/Charts/jfr/JFRMetr import { JFRMetricsChartController, ControllerState } from '@app/Dashboard/Charts/jfr/JFRMetricsChartController'; import { MBeanMetricsChartController } from '@app/Dashboard/Charts/mbean/MBeanMetricsChartController'; import { ThemeSetting } from '@app/Settings/types'; -import { setupStore, store } from '@app/Shared/Redux/ReduxStore'; -import { NotificationsContext, NotificationsInstance } from '@app/Shared/Services/Notifications.service'; -import { defaultServices, ServiceContext } from '@app/Shared/Services/Services'; +import { defaultServices } from '@app/Shared/Services/Services'; import { cleanup, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { createMemoryHistory, MemoryHistory } from 'history'; import * as React from 'react'; -import { Provider } from 'react-redux'; -import renderer, { act } from 'react-test-renderer'; import { of } from 'rxjs'; -import { mockMediaQueryList, renderWithProvidersAndRedux } from '../../../Common'; - -let history: MemoryHistory = createMemoryHistory({ initialEntries: ['/'] }); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useRouteMatch: () => ({ url: history.location.pathname }), - useHistory: () => history, -})); +import { mockMediaQueryList, render, renderSnapshot } from '../../../utils'; const mockDashboardUrl = 'http://localhost:3000'; jest.spyOn(defaultServices.api, 'grafanaDashboardUrl').mockReturnValue(of(mockDashboardUrl)); @@ -66,25 +53,21 @@ const mockChartContext = { }; describe('', () => { - beforeEach(() => (history = createMemoryHistory({ initialEntries: ['/'] }))); afterEach(cleanup); it('renders correctly', async () => { jest.spyOn(mockJfrController, 'attach').mockReturnValue(of(ControllerState.READY)); - let tree; - await act(async () => { - tree = renderer.create( - - - - - - - - - , - ); + const tree = renderSnapshot({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + providers: [{ kind: ChartContext.Provider, instance: mockChartContext }], }); expect(tree.toJSON()).toMatchSnapshot(); }); @@ -92,19 +75,16 @@ describe('', () => { it('renders loading state correctly', async () => { jest.spyOn(mockJfrController, 'attach').mockReturnValue(of(ControllerState.UNKNOWN)); - let tree; - await act(async () => { - tree = renderer.create( - - - - - - - - - , - ); + const tree = renderSnapshot({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + providers: [{ kind: ChartContext.Provider, instance: mockChartContext }], }); expect(tree.toJSON()).toMatchSnapshot(); }); @@ -112,19 +92,16 @@ describe('', () => { it('renders empty state correctly', async () => { jest.spyOn(mockJfrController, 'attach').mockReturnValue(of(ControllerState.NO_DATA)); - let tree; - await act(async () => { - tree = renderer.create( - - - - - - - - - , - ); + const tree = renderSnapshot({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + providers: [{ kind: ChartContext.Provider, instance: mockChartContext }], }); expect(tree.toJSON()).toMatchSnapshot(); }); @@ -132,7 +109,17 @@ describe('', () => { it('renders empty state with information and action button', async () => { jest.spyOn(mockJfrController, 'attach').mockReturnValue(of(ControllerState.NO_DATA)); - renderChartCard(); + render({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + providers: [{ kind: ChartContext.Provider, instance: mockChartContext }], + }); expect(screen.getByText('CPU Load (last 120s, every 10s)')).toBeInTheDocument(); expect(screen.getByText('No source recording')).toBeInTheDocument(); @@ -142,17 +129,26 @@ describe('', () => { expect(screen.getByRole('button', { name: /create/i })).toBeInTheDocument(); }); + // TODO: Use RouterProvider it('navigates to recording creation with prefilled state when empty state button clicked', async () => { jest.spyOn(mockJfrController, 'attach').mockReturnValue(of(ControllerState.NO_DATA)); - const { user } = renderChartCard( - , - ); + const { user, router } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: , + }, + ], + }, + providers: [{ kind: ChartContext.Provider, instance: mockChartContext }], + }); - expect(history.location.pathname).toBe('/'); + expect(router.state.location.pathname).toBe('/'); await user.click(screen.getByRole('button', { name: /create/i })); - expect(history.location.pathname).toBe('/recordings/create'); - expect(history.location.state).toEqual({ + expect(router.state.location.pathname).toBe('/recordings/create'); + expect(router.state.location.state).toEqual({ name: 'dashboard_metrics', template: { name: 'Continuous', @@ -177,9 +173,19 @@ describe('', () => { ])('renders iframe', async (chartKind: string, duration: number, period: number) => { jest.spyOn(mockJfrController, 'attach').mockReturnValue(of(ControllerState.READY)); - const { container } = renderChartCard( - , - ); + const { container } = render({ + routerConfigs: { + routes: [ + { + path: '/', + element: ( + + ), + }, + ], + }, + providers: [{ kind: ChartContext.Provider, instance: mockChartContext }], + }); const iframe = container.querySelector('iframe'); expect(iframe).toBeTruthy(); @@ -195,39 +201,3 @@ describe('', () => { expect(u.searchParams.toString()).toEqual(params.toString()); }); }); - -const renderChartCard = ( - ui: React.ReactElement, - { - services = defaultServices, - notifications = NotificationsInstance, - chartContext = mockChartContext, - preloadState = {}, - store = setupStore(preloadState), - user = userEvent.setup(), - ...renderOptions - } = {}, -) => { - return renderWithProvidersAndRedux( - ui, - [ - { - kind: ChartContext.Provider, - instance: chartContext, - }, - { - kind: NotificationsContext.Provider, - instance: notifications, - }, - { - kind: ServiceContext.Provider, - instance: services, - }, - ], - { - store, - user, - ...renderOptions, - }, - ); -}; diff --git a/src/test/Dashboard/Charts/jfr/__snapshots__/JFRMetricsChartCard.test.tsx.snap b/src/test/Dashboard/Charts/jfr/__snapshots__/JFRMetricsChartCard.test.tsx.snap index 7d3abde69..6e2dccc65 100644 --- a/src/test/Dashboard/Charts/jfr/__snapshots__/JFRMetricsChartCard.test.tsx.snap +++ b/src/test/Dashboard/Charts/jfr/__snapshots__/JFRMetricsChartCard.test.tsx.snap @@ -43,15 +43,16 @@ exports[` renders correctly 1`] = ` className="pf-c-card__actions" >