diff --git a/awx/ui/package-lock.json b/awx/ui/package-lock.json
index 820b0f58..c57fdf97 100644
--- a/awx/ui/package-lock.json
+++ b/awx/ui/package-lock.json
@@ -29,6 +29,7 @@
"react-dom": "17.0.2",
"react-error-boundary": "^3.1.4",
"react-router-dom": "^5.3.3",
+ "react-router-dom-v5-compat": "^6.30.4",
"react-virtualized": "^9.22.6",
"rrule": "2.8.1",
"styled-components": "5.3.11"
@@ -5800,6 +5801,15 @@
}
}
},
+ "node_modules/@remix-run/router": {
+ "version": "1.23.3",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
+ "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@rspack/binding": {
"version": "1.7.11",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.11.tgz",
@@ -21380,6 +21390,49 @@
"react": ">=15"
}
},
+ "node_modules/react-router-dom-v5-compat": {
+ "version": "6.30.4",
+ "resolved": "https://registry.npmjs.org/react-router-dom-v5-compat/-/react-router-dom-v5-compat-6.30.4.tgz",
+ "integrity": "sha512-lJP6Zl6DYQtmrnaOV7MW5s/Npe7mYOokwekaPAqjCgIMQmZFfdA8mfoe5kWJoT/xedSdgZLrfFVHx18RUIIcEw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.3",
+ "history": "^5.3.0",
+ "react-router": "6.30.4"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8",
+ "react-router-dom": "4 || 5"
+ }
+ },
+ "node_modules/react-router-dom-v5-compat/node_modules/history": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
+ "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.7.6"
+ }
+ },
+ "node_modules/react-router-dom-v5-compat/node_modules/react-router": {
+ "version": "6.30.4",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
+ "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
"node_modules/react-router/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -23760,7 +23813,7 @@
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
diff --git a/awx/ui/package.json b/awx/ui/package.json
index 444d70d6..68723d03 100644
--- a/awx/ui/package.json
+++ b/awx/ui/package.json
@@ -29,6 +29,7 @@
"react-dom": "17.0.2",
"react-error-boundary": "^3.1.4",
"react-router-dom": "^5.3.3",
+ "react-router-dom-v5-compat": "^6.30.4",
"react-virtualized": "^9.22.6",
"rrule": "2.8.1",
"styled-components": "5.3.11"
diff --git a/awx/ui/src/App.js b/awx/ui/src/App.js
index 3df0dd95..b4982c0e 100644
--- a/awx/ui/src/App.js
+++ b/awx/ui/src/App.js
@@ -8,6 +8,7 @@ import {
Redirect,
useHistory,
} from 'react-router-dom';
+import { CompatRouter } from 'react-router-dom-v5-compat';
import { ErrorBoundary } from 'react-error-boundary';
import locationReplace from 'util/navigation';
import { I18nProvider } from '@lingui/react';
@@ -215,6 +216,8 @@ function App() {
export default () => (
-
+
+
+
);
diff --git a/awx/ui/src/components/AdHocCommands/AdHocCommands.js b/awx/ui/src/components/AdHocCommands/AdHocCommands.js
index fd900e12..c32b6948 100644
--- a/awx/ui/src/components/AdHocCommands/AdHocCommands.js
+++ b/awx/ui/src/components/AdHocCommands/AdHocCommands.js
@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useState, useContext } from 'react';
-import { useHistory, useParams } from 'react-router-dom';
+import { useParams } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
@@ -22,7 +23,7 @@ function AdHocCommands({
moduleOptions,
}) {
const { t } = useLingui();
- const history = useHistory();
+ const navigate = useNavigate();
const { id } = useParams();
const [isWizardOpen, setIsWizardOpen] = useState(false);
@@ -63,10 +64,10 @@ function AdHocCommands({
useCallback(
async (values) => {
const { data } = await InventoriesAPI.launchAdHocCommands(id, values);
- history.push(`/jobs/command/${data.id}/output`);
+ navigate(`/jobs/command/${data.id}/output`);
},
- [id, history]
+ [id, navigate]
)
);
diff --git a/awx/ui/src/components/AdHocCommands/AdHocCredentialStep.js b/awx/ui/src/components/AdHocCommands/AdHocCredentialStep.js
index bd2ca703..3a9d04d6 100644
--- a/awx/ui/src/components/AdHocCommands/AdHocCredentialStep.js
+++ b/awx/ui/src/components/AdHocCommands/AdHocCredentialStep.js
@@ -1,5 +1,5 @@
import React, { useEffect, useCallback } from 'react';
-import { useHistory } from 'react-router-dom';
+import { useLocation } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
import styled from 'styled-components';
import PropTypes from 'prop-types';
@@ -29,7 +29,7 @@ const QS_CONFIG = getQSConfig('credentials', {
function AdHocCredentialStep({ credentialTypeId }) {
const { t } = useLingui();
- const history = useHistory();
+ const location = useLocation();
const {
error,
isLoading,
@@ -42,7 +42,7 @@ function AdHocCredentialStep({ credentialTypeId }) {
},
} = useRequest(
useCallback(async () => {
- const params = parseQueryString(QS_CONFIG, history.location.search);
+ const params = parseQueryString(QS_CONFIG, location.search);
const [
{
@@ -64,7 +64,7 @@ function AdHocCredentialStep({ credentialTypeId }) {
).map((val) => val.slice(0, -8)),
searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
};
- }, [credentialTypeId, history.location.search]),
+ }, [credentialTypeId, location.search]),
{
credentials: [],
credentialCount: 0,
diff --git a/awx/ui/src/components/AdHocCommands/AdHocExecutionEnvironmentStep.js b/awx/ui/src/components/AdHocCommands/AdHocExecutionEnvironmentStep.js
index 91ef9f52..e2b73c31 100644
--- a/awx/ui/src/components/AdHocCommands/AdHocExecutionEnvironmentStep.js
+++ b/awx/ui/src/components/AdHocCommands/AdHocExecutionEnvironmentStep.js
@@ -1,5 +1,5 @@
import React, { useEffect, useCallback } from 'react';
-import { useHistory } from 'react-router-dom';
+import { useLocation } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
import { useField } from 'formik';
import { Form, FormGroup } from '@patternfly/react-core';
@@ -20,7 +20,7 @@ const QS_CONFIG = getQSConfig('execution_environments', {
});
function AdHocExecutionEnvironmentStep({ organizationId }) {
const { t } = useLingui();
- const history = useHistory();
+ const location = useLocation();
const [executionEnvironmentField, , executionEnvironmentHelpers] = useField(
'execution_environment'
);
@@ -36,7 +36,7 @@ function AdHocExecutionEnvironmentStep({ organizationId }) {
},
} = useRequest(
useCallback(async () => {
- const params = parseQueryString(QS_CONFIG, history.location.search);
+ const params = parseQueryString(QS_CONFIG, location.search);
const globallyAvailableParams = { or__organization__isnull: 'True' };
const organizationIdParams = organizationId
? { or__organization__id: organizationId }
@@ -64,7 +64,7 @@ function AdHocExecutionEnvironmentStep({ organizationId }) {
).map((val) => val.slice(0, -8)),
searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
};
- }, [history.location.search, organizationId]),
+ }, [location.search, organizationId]),
{
executionEnvironments: [],
executionEnvironmentsCount: 0,
diff --git a/awx/ui/src/components/AddRole/AddResourceRole.js b/awx/ui/src/components/AddRole/AddResourceRole.js
index 07437efd..f7400d26 100644
--- a/awx/ui/src/components/AddRole/AddResourceRole.js
+++ b/awx/ui/src/components/AddRole/AddResourceRole.js
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
-import { useHistory } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
import { TeamsAPI, UsersAPI } from 'api';
import useSelected from 'hooks/useSelected';
@@ -74,7 +74,8 @@ function AddResourceRole({ onSave, onClose, roles, resource, onError }) {
key: 'name',
},
], [t]);
- const history = useHistory();
+ const location = useLocation();
+ const navigate = useNavigate();
const {
selected: resourcesSelected,
@@ -93,9 +94,12 @@ function AddResourceRole({ onSave, onClose, roles, resource, onError }) {
useEffect(() => {
if (currentStepId === 1 && maxEnabledStep > 1) {
- history.push(history.location.pathname);
+ navigate(location.pathname);
}
- }, [currentStepId, history, maxEnabledStep]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- navigate is not
+ // referentially stable in react-router-dom-v5-compat; including it refires
+ // this effect after unrelated navigations
+ }, [currentStepId, location.pathname, maxEnabledStep]);
const handleResourceTypeSelect = (type) => {
setResourceType(type);
diff --git a/awx/ui/src/components/AddRole/AddResourceRole.test.js b/awx/ui/src/components/AddRole/AddResourceRole.test.js
index 6f1d6ec3..bf81045b 100644
--- a/awx/ui/src/components/AddRole/AddResourceRole.test.js
+++ b/awx/ui/src/components/AddRole/AddResourceRole.test.js
@@ -14,10 +14,6 @@ import AddResourceRole, { _AddResourceRole } from './AddResourceRole';
jest.mock('../../api/models/Teams');
jest.mock('../../api/models/Users');
-jest.mock('react-router-dom', () => ({
- ...jest.requireActual('react-router-dom'),
- useHistory: () => ({ push: jest.fn(), location: { pathname: {} } }),
-}));
// TODO: Once error handling is functional in
// this component write tests for it
@@ -200,7 +196,7 @@ describe('<_AddResourceRole />', () => {
test('should update history properly', async () => {
let wrapper;
const history = createMemoryHistory({
- initialEntries: ['organizations/2/access?resource.order_by=-username'],
+ initialEntries: ['/organizations/2/access?resource.order_by=-username'],
});
act(() => {
wrapper = mountWithContexts(
@@ -233,7 +229,7 @@ describe('<_AddResourceRole />', () => {
wrapper.find('PFWizard').prop('onGoToStep')({ id: 1 })
);
wrapper.update();
- expect(history.location.pathname).toEqual('organizations/2/access');
+ expect(history.location.pathname).toEqual('/organizations/2/access');
});
test('should successfuly click user/team cards', async () => {
diff --git a/awx/ui/src/components/AppContainer/AppContainer.js b/awx/ui/src/components/AppContainer/AppContainer.js
index f7add621..26ed9275 100644
--- a/awx/ui/src/components/AppContainer/AppContainer.js
+++ b/awx/ui/src/components/AppContainer/AppContainer.js
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import { withRouter } from 'react-router-dom';
+
import {
Button,
Nav,
@@ -167,4 +167,4 @@ function AppContainer({ navRouteConfig = [], children }) {
}
export { AppContainer as _AppContainer };
-export default withRouter(AppContainer);
+export default AppContainer;
diff --git a/awx/ui/src/components/AppContainer/NavExpandableGroup.js b/awx/ui/src/components/AppContainer/NavExpandableGroup.js
index a5c16217..9a46f51e 100644
--- a/awx/ui/src/components/AppContainer/NavExpandableGroup.js
+++ b/awx/ui/src/components/AppContainer/NavExpandableGroup.js
@@ -1,10 +1,11 @@
import React from 'react';
import PropTypes, { oneOfType, string, arrayOf } from 'prop-types';
-import { matchPath, Link, useHistory } from 'react-router-dom';
+import { matchPath, Link } from 'react-router-dom';
+import { useLocation } from 'react-router-dom-v5-compat';
import { NavExpandable, NavItem } from '@patternfly/react-core';
function NavExpandableGroup(props) {
- const history = useHistory();
+ const location = useLocation();
const { groupId, groupTitle, routes } = props;
// Extract a list of paths from the route params and store them for later. This creates
@@ -14,7 +15,7 @@ function NavExpandableGroup(props) {
const isActive = navItemPaths.some(isActivePath);
function isActivePath(path) {
- return Boolean(matchPath(history.location.pathname, { path }));
+ return Boolean(matchPath(location.pathname, { path }));
}
if (routes.length === 1 && groupId === 'settings') {
diff --git a/awx/ui/src/components/AppContainer/NavExpandableGroup.test.js b/awx/ui/src/components/AppContainer/NavExpandableGroup.test.js
index 90c0214a..daadaf6c 100644
--- a/awx/ui/src/components/AppContainer/NavExpandableGroup.test.js
+++ b/awx/ui/src/components/AppContainer/NavExpandableGroup.test.js
@@ -1,17 +1,14 @@
import React from 'react';
-import { MemoryRouter, withRouter } from 'react-router-dom';
-import { mount } from 'enzyme';
+import { createMemoryHistory } from 'history';
import { Nav } from '@patternfly/react-core';
-import _NavExpandableGroup from './NavExpandableGroup';
-
-const NavExpandableGroup = withRouter(_NavExpandableGroup);
+import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import NavExpandableGroup from './NavExpandableGroup';
describe('NavExpandableGroup', () => {
test('initialization and render', () => {
- const component = mount(
-
-
+ ,
+ {
+ context: {
+ router: { history: createMemoryHistory({ initialEntries: ['/foo'] }) },
+ },
+ }
).find('NavExpandableGroup');
expect(component.find('NavItem').length).toEqual(3);
@@ -40,9 +41,8 @@ describe('NavExpandableGroup', () => {
});
test('when location is /foo/1/bar/fiz isActive returns false', () => {
- const component = mount(
-
-
+ const component = mountWithContexts(
+
{
{ path: '/fiz', title: 'Fiz' },
]}
/>
-
-
+ ,
+ {
+ context: {
+ router: { history: createMemoryHistory({ initialEntries: ['/foo/1/bar/fiz'] }) },
+ },
+ }
).find('NavExpandableGroup');
expect(component.find('NavItem').length).toEqual(3);
@@ -63,9 +67,8 @@ describe('NavExpandableGroup', () => {
});
test('when location is /fo isActive returns false', () => {
- const component = mount(
-
-
+ const component = mountWithContexts(
+
{
{ path: '/fiz', title: 'Fiz' },
]}
/>
-
-
+ ,
+ {
+ context: {
+ router: { history: createMemoryHistory({ initialEntries: ['/fo'] }) },
+ },
+ }
).find('NavExpandableGroup');
expect(component.find('NavItem').length).toEqual(3);
@@ -86,9 +93,8 @@ describe('NavExpandableGroup', () => {
});
test('when location is /foo isActive returns true', () => {
- const component = mount(
-
-
+ const component = mountWithContexts(
+
{
{ path: '/fiz', title: 'Fiz' },
]}
/>
-
-
+ ,
+ {
+ context: {
+ router: { history: createMemoryHistory({ initialEntries: ['/foo'] }) },
+ },
+ }
).find('NavExpandableGroup');
expect(component.find('NavItem').length).toEqual(3);
diff --git a/awx/ui/src/components/AssociateModal/AssociateModal.js b/awx/ui/src/components/AssociateModal/AssociateModal.js
index a2eea0a3..e95798e0 100644
--- a/awx/ui/src/components/AssociateModal/AssociateModal.js
+++ b/awx/ui/src/components/AssociateModal/AssociateModal.js
@@ -1,5 +1,5 @@
import React, { useEffect, useCallback } from 'react';
-import { useHistory } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
@@ -31,7 +31,8 @@ function AssociateModal({
modalNote,
}) {
const { t } = useLingui();
- const history = useHistory();
+ const location = useLocation();
+ const navigate = useNavigate();
const { selected, handleSelect } = useSelected([]);
// Set default values for header and title after i18n is available
@@ -47,7 +48,7 @@ function AssociateModal({
useCallback(async () => {
const params = parseQueryString(
QS_CONFIG(displayKey),
- history.location.search
+ location.search
);
const [
{
@@ -64,7 +65,7 @@ function AssociateModal({
).map((val) => val.slice(0, -8)),
searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
};
- }, [fetchRequest, optionsRequest, history.location.search, displayKey]),
+ }, [fetchRequest, optionsRequest, location.search, displayKey]),
{
items: [],
itemCount: 0,
@@ -78,12 +79,12 @@ function AssociateModal({
}, [fetchItems]);
const clearQSParams = () => {
- const parts = history.location.search.replace(/^\?/, '').split('&');
+ const parts = location.search.replace(/^\?/, '').split('&');
const { namespace } = QS_CONFIG(displayKey);
const otherParts = parts.filter(
(param) => !param.startsWith(`${namespace}.`)
);
- history.replace(`${history.location.pathname}?${otherParts.join('&')}`);
+ navigate(`${location.pathname}?${otherParts.join('&')}`, { replace: true });
};
const handleSave = async () => {
diff --git a/awx/ui/src/components/ContentError/ContentError.js b/awx/ui/src/components/ContentError/ContentError.js
index de247d15..83e1c600 100644
--- a/awx/ui/src/components/ContentError/ContentError.js
+++ b/awx/ui/src/components/ContentError/ContentError.js
@@ -1,6 +1,7 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React from 'react';
-import { Link, Redirect } from 'react-router-dom';
+import { Link } from 'react-router-dom';
+import { Navigate } from 'react-router-dom-v5-compat';
import { bool, instanceOf } from 'prop-types';
import { useLingui } from '@lingui/react/macro';
@@ -30,7 +31,7 @@ function ContentError({ error, children, isNotFound }) {
return (
<>
{is401 ? (
-
+
) : (
diff --git a/awx/ui/src/components/LaunchButton/LaunchButton.js b/awx/ui/src/components/LaunchButton/LaunchButton.js
index e80397d2..af82d331 100644
--- a/awx/ui/src/components/LaunchButton/LaunchButton.js
+++ b/awx/ui/src/components/LaunchButton/LaunchButton.js
@@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
-import { useHistory } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom-v5-compat';
import { number, shape } from 'prop-types';
import { useLingui } from '@lingui/react/macro';
import {
@@ -39,7 +39,7 @@ function canLaunchWithoutPrompt(launchData) {
function LaunchButton({ resource, children }) {
const { t } = useLingui();
- const history = useHistory();
+ const navigate = useNavigate();
const [showLaunchPrompt, setShowLaunchPrompt] = useState(false);
const [launchConfig, setLaunchConfig] = useState(null);
const [surveyConfig, setSurveyConfig] = useState(null);
@@ -158,7 +158,7 @@ function LaunchButton({ resource, children }) {
}
const { data: job } = await jobPromise;
- if (isMounted.current) history.push(`/jobs/${job.id}/output`);
+ if (isMounted.current) navigate(`/jobs/${job.id}/output`);
} catch (launchError) {
if (isMounted.current) setError(launchError);
} finally {
@@ -212,7 +212,7 @@ function LaunchButton({ resource, children }) {
relaunch = JobsAPI.relaunch(resource.id, params || {});
}
const { data: job } = await relaunch;
- if (isMounted.current) history.push(`/jobs/${job.id}/output`);
+ if (isMounted.current) navigate(`/jobs/${job.id}/output`);
} else if (isMounted.current) {
setShowLaunchPrompt(true);
}
diff --git a/awx/ui/src/components/LaunchPrompt/steps/CredentialsStep.js b/awx/ui/src/components/LaunchPrompt/steps/CredentialsStep.js
index 9605b3e0..9697cf92 100644
--- a/awx/ui/src/components/LaunchPrompt/steps/CredentialsStep.js
+++ b/awx/ui/src/components/LaunchPrompt/steps/CredentialsStep.js
@@ -1,6 +1,7 @@
import 'styled-components/macro';
import React, { useState, useCallback, useEffect } from 'react';
-import { useHistory, useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
import { useField } from 'formik';
import styled from 'styled-components';
@@ -31,7 +32,7 @@ function CredentialsStep({
defaultCredentials = [],
}) {
const { t } = useLingui();
- const history = useHistory();
+ const navigate = useNavigate();
const location = useLocation();
// Create a wrapper for the validator that handles translation properly
@@ -130,7 +131,7 @@ function CredentialsStep({
if (!selectedType) {
return { credentials: [], count: 0 };
}
- const params = parseQueryString(QS_CONFIG, history.location.search);
+ const params = parseQueryString(QS_CONFIG, location.search);
const [{ data }, actionsResponse] = await Promise.all([
CredentialsAPI.read({
...params,
@@ -146,7 +147,7 @@ function CredentialsStep({
).map((val) => val.slice(0, -8)),
searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
};
- }, [selectedType, history.location.search]),
+ }, [selectedType, location.search]),
{ credentials: [], count: 0, relatedSearchableKeys: [], searchableKeys: [] }
);
@@ -182,8 +183,8 @@ function CredentialsStep({
};
const pushHistoryState = (qs) => {
- const { pathname } = history.location;
- history.push(qs ? `${pathname}?${qs}` : pathname);
+ const { pathname } = location;
+ navigate(qs ? `${pathname}?${qs}` : pathname);
};
if (isTypesLoading) {
diff --git a/awx/ui/src/components/LaunchPrompt/steps/ExecutionEnvironmentStep.js b/awx/ui/src/components/LaunchPrompt/steps/ExecutionEnvironmentStep.js
index fc667fe4..887562ee 100644
--- a/awx/ui/src/components/LaunchPrompt/steps/ExecutionEnvironmentStep.js
+++ b/awx/ui/src/components/LaunchPrompt/steps/ExecutionEnvironmentStep.js
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect } from 'react';
-import { useHistory } from 'react-router-dom';
+import { useLocation } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
import { useField } from 'formik';
import { ExecutionEnvironmentsAPI } from 'api';
@@ -19,7 +19,7 @@ function ExecutionEnvironmentStep() {
const { t } = useLingui();
const [field, , helpers] = useField('execution_environment');
- const history = useHistory();
+ const location = useLocation();
const {
isLoading,
@@ -33,7 +33,7 @@ function ExecutionEnvironmentStep() {
request: fetchExecutionEnvironments,
} = useRequest(
useCallback(async () => {
- const params = parseQueryString(QS_CONFIG, history.location.search);
+ const params = parseQueryString(QS_CONFIG, location.search);
const [{ data }, actionsResponse] = await Promise.all([
ExecutionEnvironmentsAPI.read(params),
ExecutionEnvironmentsAPI.readOptions(),
@@ -46,7 +46,7 @@ function ExecutionEnvironmentStep() {
).map((val) => val.slice(0, -8)),
searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
};
- }, [history.location]),
+ }, [location]),
{
count: 0,
execution_environments: [],
diff --git a/awx/ui/src/components/LaunchPrompt/steps/InstanceGroupsStep.js b/awx/ui/src/components/LaunchPrompt/steps/InstanceGroupsStep.js
index 4b4201e2..83d824a4 100644
--- a/awx/ui/src/components/LaunchPrompt/steps/InstanceGroupsStep.js
+++ b/awx/ui/src/components/LaunchPrompt/steps/InstanceGroupsStep.js
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect } from 'react';
-import { useHistory } from 'react-router-dom';
+import { useLocation } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
import { useField } from 'formik';
import { InstanceGroupsAPI } from 'api';
@@ -22,7 +22,7 @@ function InstanceGroupsStep() {
const [field, , helpers] = useField('instance_groups');
const { selected, handleSelect, setSelected } = useSelected([], field.value);
- const history = useHistory();
+ const location = useLocation();
const {
result: { instance_groups, count, relatedSearchableKeys, searchableKeys },
@@ -31,7 +31,7 @@ function InstanceGroupsStep() {
isLoading,
} = useRequest(
useCallback(async () => {
- const params = parseQueryString(QS_CONFIG, history.location.search);
+ const params = parseQueryString(QS_CONFIG, location.search);
const [{ data }, actionsResponse] = await Promise.all([
InstanceGroupsAPI.read(params),
InstanceGroupsAPI.readOptions(),
@@ -44,7 +44,7 @@ function InstanceGroupsStep() {
).map((val) => val.slice(0, -8)),
searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
};
- }, [history.location]),
+ }, [location]),
{
instance_groups: [],
count: 0,
diff --git a/awx/ui/src/components/LaunchPrompt/steps/InventoryStep.js b/awx/ui/src/components/LaunchPrompt/steps/InventoryStep.js
index 14c6cae8..33bd45c6 100644
--- a/awx/ui/src/components/LaunchPrompt/steps/InventoryStep.js
+++ b/awx/ui/src/components/LaunchPrompt/steps/InventoryStep.js
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect } from 'react';
-import { useHistory } from 'react-router-dom';
+import { useLocation } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
import { useField } from 'formik';
import styled from 'styled-components';
@@ -27,7 +27,7 @@ function InventoryStep({ warningMessage = null }) {
const { t } = useLingui();
const [field, meta, helpers] = useField('inventory');
- const history = useHistory();
+ const location = useLocation();
const {
isLoading,
@@ -36,7 +36,7 @@ function InventoryStep({ warningMessage = null }) {
request: fetchInventories,
} = useRequest(
useCallback(async () => {
- const params = parseQueryString(QS_CONFIG, history.location.search);
+ const params = parseQueryString(QS_CONFIG, location.search);
const [{ data }, actionsResponse] = await Promise.all([
InventoriesAPI.read(params),
InventoriesAPI.readOptions(),
@@ -49,7 +49,7 @@ function InventoryStep({ warningMessage = null }) {
).map((val) => val.slice(0, -8)),
searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
};
- }, [history.location]),
+ }, [location]),
{
count: 0,
inventories: [],
diff --git a/awx/ui/src/components/ListHeader/ListHeader.js b/awx/ui/src/components/ListHeader/ListHeader.js
index 8fc3e300..800a9640 100644
--- a/awx/ui/src/components/ListHeader/ListHeader.js
+++ b/awx/ui/src/components/ListHeader/ListHeader.js
@@ -1,7 +1,8 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useState } from 'react';
import PropTypes from 'prop-types';
-import { useHistory, useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom-v5-compat';
import styled from 'styled-components';
import { Toolbar, ToolbarContent } from '@patternfly/react-core';
@@ -28,7 +29,7 @@ const EmptyStateControlsWrapper = styled.div`
function ListHeader(props) {
const { search, pathname } = useLocation();
const [isFilterCleared, setIsFilterCleared] = useState(false);
- const history = useHistory();
+ const navigate = useNavigate();
const {
emptyStateControls,
itemCount,
@@ -87,7 +88,7 @@ function ListHeader(props) {
};
const pushHistoryState = (queryString) => {
- history.push(queryString ? `${pathname}?${queryString}` : pathname);
+ navigate(queryString ? `${pathname}?${queryString}` : pathname);
};
const params = parseQueryString(qsConfig, search);
diff --git a/awx/ui/src/components/Lookup/CredentialLookup.js b/awx/ui/src/components/Lookup/CredentialLookup.js
index e63d3e5a..3ad1e204 100644
--- a/awx/ui/src/components/Lookup/CredentialLookup.js
+++ b/awx/ui/src/components/Lookup/CredentialLookup.js
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect } from 'react';
-import { useHistory } from 'react-router-dom';
+import { useLocation } from 'react-router-dom-v5-compat';
import {
arrayOf,
bool,
@@ -50,7 +50,7 @@ function CredentialLookup({
value,
}) {
const { t } = useLingui();
- const history = useHistory();
+ const location = useLocation();
const autoPopulateLookup = useAutoPopulateLookup(onChange);
const {
result: { count, credentials, relatedSearchableKeys, searchableKeys },
@@ -58,7 +58,7 @@ function CredentialLookup({
request: fetchCredentials,
} = useRequest(
useCallback(async () => {
- const params = parseQueryString(QS_CONFIG, history.location.search);
+ const params = parseQueryString(QS_CONFIG, location.search);
const typeIdParams = credentialTypeId
? { credential_type: credentialTypeId }
: {};
@@ -104,7 +104,7 @@ function CredentialLookup({
credentialTypeId,
credentialTypeKind,
credentialTypeNamespace,
- history.location.search,
+ location.search,
]),
{
count: 0,
diff --git a/awx/ui/src/components/Lookup/HostFilterLookup.js b/awx/ui/src/components/Lookup/HostFilterLookup.js
index e70016cc..56fc66f2 100644
--- a/awx/ui/src/components/Lookup/HostFilterLookup.js
+++ b/awx/ui/src/components/Lookup/HostFilterLookup.js
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react';
-import { useHistory, useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom-v5-compat';
import { number, func, bool, string } from 'prop-types';
import styled from 'styled-components';
@@ -101,7 +102,7 @@ function HostFilterLookup({
enableRelatedFuzzyFiltering,
}) {
const { t } = useLingui();
- const history = useHistory();
+ const navigate = useNavigate();
const location = useLocation();
const [chips, setChips] = useState({});
const [queryString, setQueryString] = useState('');
@@ -220,10 +221,12 @@ function HostFilterLookup({
const hostFilterString = qsToHostFilter(location.search);
onChange(hostFilterString);
closeModal();
- history.replace({
+ navigate(
+{
pathname: `${location.pathname}`,
search: '',
- });
+ },
+ { replace: true });
};
const removeHostFilter = (filter) => {
@@ -269,20 +272,24 @@ function HostFilterLookup({
}
const handleOpenModal = () => {
- history.replace({
+ navigate(
+{
pathname: `${location.pathname}`,
search: queryString,
- });
+ },
+ { replace: true });
fetchHosts(organizationId);
toggleModal();
};
const handleClose = () => {
closeModal();
- history.replace({
+ navigate(
+{
pathname: `${location.pathname}`,
search: '',
- });
+ },
+ { replace: true });
};
const renderLookup = () => (
diff --git a/awx/ui/src/components/Lookup/InstanceGroupsLookup.js b/awx/ui/src/components/Lookup/InstanceGroupsLookup.js
index 9e05e2f8..c5e63fe8 100644
--- a/awx/ui/src/components/Lookup/InstanceGroupsLookup.js
+++ b/awx/ui/src/components/Lookup/InstanceGroupsLookup.js
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect } from 'react';
import { arrayOf, string, func, bool } from 'prop-types';
-import { withRouter } from 'react-router-dom';
+import { useLocation } from 'react-router-dom-v5-compat';
import { Trans, useLingui } from '@lingui/react/macro';
import { FormGroup } from '@patternfly/react-core';
import { InstanceGroupsAPI } from 'api';
@@ -27,13 +27,13 @@ function InstanceGroupsLookup({
tooltip,
className,
required,
- history,
fieldName,
validate,
isPromptableField,
promptId,
promptName,
}) {
+ const location = useLocation();
const { t } = useLingui();
const {
result: { instanceGroups, count, relatedSearchableKeys, searchableKeys },
@@ -42,7 +42,7 @@ function InstanceGroupsLookup({
isLoading,
} = useRequest(
useCallback(async () => {
- const params = parseQueryString(QS_CONFIG, history.location.search);
+ const params = parseQueryString(QS_CONFIG, location.search);
const [{ data }, actionsResponse] = await Promise.all([
InstanceGroupsAPI.read(params),
InstanceGroupsAPI.readOptions(),
@@ -55,7 +55,7 @@ function InstanceGroupsLookup({
).map((val) => val.slice(0, -8)),
searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
};
- }, [history.location]),
+ }, [location]),
{
instanceGroups: [],
count: 0,
@@ -178,4 +178,4 @@ InstanceGroupsLookup.defaultProps = {
fieldName: 'instance_groups',
};
-export default withRouter(InstanceGroupsLookup);
+export default InstanceGroupsLookup;
diff --git a/awx/ui/src/components/Lookup/InventoryLookup.js b/awx/ui/src/components/Lookup/InventoryLookup.js
index 7fcec28d..395727f9 100644
--- a/awx/ui/src/components/Lookup/InventoryLookup.js
+++ b/awx/ui/src/components/Lookup/InventoryLookup.js
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect } from 'react';
import { func, bool, string, number, oneOfType, arrayOf } from 'prop-types';
-import { withRouter } from 'react-router-dom';
+import { useLocation } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
import { InventoriesAPI } from 'api';
import { Inventory } from 'types';
@@ -25,7 +25,6 @@ function InventoryLookup({
fieldId,
fieldName,
hideAdvancedInventories,
- history,
isDisabled,
isPromptableField,
onBlur,
@@ -37,6 +36,7 @@ function InventoryLookup({
value,
multiple,
}) {
+ const location = useLocation();
const { t } = useLingui();
const autoPopulateLookup = useAutoPopulateLookup(onChange);
@@ -49,7 +49,7 @@ function InventoryLookup({
isLoading,
} = useRequest(
useCallback(async () => {
- const params = parseQueryString(QS_CONFIG, history.location.search);
+ const params = parseQueryString(QS_CONFIG, location.search);
const inventoryKindParams = hideAdvancedInventories
? { not__kind: ['smart', 'constructed', 'federated'] }
: {};
@@ -92,7 +92,7 @@ function InventoryLookup({
})),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [autoPopulate, autoPopulateLookup, excludeIdsKey, history.location]),
+ }, [autoPopulate, autoPopulateLookup, excludeIdsKey, location]),
{
inventories: [],
count: 0,
@@ -277,4 +277,4 @@ InventoryLookup.defaultProps = {
value: null,
};
-export default withRouter(InventoryLookup);
+export default InventoryLookup;
diff --git a/awx/ui/src/components/Lookup/Lookup.js b/awx/ui/src/components/Lookup/Lookup.js
index c09384f9..066d777e 100644
--- a/awx/ui/src/components/Lookup/Lookup.js
+++ b/awx/ui/src/components/Lookup/Lookup.js
@@ -10,7 +10,7 @@ import {
node,
object,
} from 'prop-types';
-import { withRouter } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
import { useField } from 'formik';
import { SearchIcon } from '@patternfly/react-icons';
import {
@@ -46,7 +46,6 @@ function Lookup(props) {
qsConfig,
renderItemChip,
renderOptionsList,
- history,
isDisabled,
onDebounce,
fieldName,
@@ -55,6 +54,8 @@ function Lookup(props) {
onUpdate,
} = props;
const { t } = useLingui();
+ const location = useLocation();
+ const navigate = useNavigate();
const [typedText, setTypedText] = useState('');
const debounceRequest = useDebounce(onDebounce, 1000);
useField({
@@ -93,15 +94,15 @@ function Lookup(props) {
}, [state.selectedItems, multiple]);
const clearQSParams = () => {
- if (!history.location.search) {
+ if (!location.search) {
// This prevents "Warning: Hash history cannot PUSH the same path;
// a new entry will not be added to the history stack" from appearing in the console.
return;
}
- const parts = history.location.search.replace(/^\?/, '').split('&');
+ const parts = location.search.replace(/^\?/, '').split('&');
const ns = qsConfig.namespace;
const otherParts = parts.filter((param) => !param.startsWith(`${ns}.`));
- history.push(`${history.location.pathname}?${otherParts.join('&')}`);
+ navigate(`${location.pathname}?${otherParts.join('&')}`);
};
const save = () => {
@@ -278,4 +279,4 @@ Lookup.defaultProps = {
};
export { Lookup as _Lookup };
-export default withRouter(Lookup);
+export default Lookup;
diff --git a/awx/ui/src/components/Lookup/MultiCredentialsLookup.js b/awx/ui/src/components/Lookup/MultiCredentialsLookup.js
index 42e6d24c..6b2d63df 100644
--- a/awx/ui/src/components/Lookup/MultiCredentialsLookup.js
+++ b/awx/ui/src/components/Lookup/MultiCredentialsLookup.js
@@ -1,6 +1,6 @@
import 'styled-components/macro';
import React, { useState, useCallback, useEffect } from 'react';
-import { withRouter } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
import PropTypes from 'prop-types';
import { useLingui } from '@lingui/react/macro';
import { ToolbarItem, Alert } from '@patternfly/react-core';
@@ -30,10 +30,11 @@ function MultiCredentialsLookup({
value,
onChange,
onError,
- history,
fieldName,
validate,
}) {
+ const location = useLocation();
+ const navigate = useNavigate();
const { t } = useLingui();
const [selectedType, setSelectedType] = useState(null);
const isMounted = useIsMounted();
@@ -81,7 +82,7 @@ function MultiCredentialsLookup({
};
}
- const params = parseQueryString(QS_CONFIG, history.location.search);
+ const params = parseQueryString(QS_CONFIG, location.search);
const [{ results, count }, actionsResponse] = await Promise.all([
loadCredentials(params, selectedType.id),
CredentialsAPI.readOptions(),
@@ -104,7 +105,7 @@ function MultiCredentialsLookup({
).map((val) => val.slice(0, -8)),
searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
};
- }, [selectedType, history.location]),
+ }, [selectedType, location]),
{
credentials: [],
credentialsCount: 0,
@@ -175,9 +176,11 @@ function MultiCredentialsLookup({
value={selectedType && selectedType.id}
onChange={(e, id) => {
// Reset query params when the category of credentials is changed
- history.replace({
+ navigate(
+{
search: '',
- });
+ },
+ { replace: true });
setSelectedType(
credentialTypes.find((o) => o.id === parseInt(id, 10))
);
@@ -265,4 +268,4 @@ MultiCredentialsLookup.defaultProps = {
};
export { MultiCredentialsLookup as _MultiCredentialsLookup };
-export default withRouter(MultiCredentialsLookup);
+export default MultiCredentialsLookup;
diff --git a/awx/ui/src/components/Lookup/OrganizationLookup.js b/awx/ui/src/components/Lookup/OrganizationLookup.js
index f4ab3abb..af8ff8c2 100644
--- a/awx/ui/src/components/Lookup/OrganizationLookup.js
+++ b/awx/ui/src/components/Lookup/OrganizationLookup.js
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect } from 'react';
import { node, func, bool, string } from 'prop-types';
-import { withRouter } from 'react-router-dom';
+import { useLocation } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
import { FormGroup } from '@patternfly/react-core';
import { OrganizationsAPI } from 'api';
@@ -27,13 +27,13 @@ function OrganizationLookup({
onChange,
required,
value,
- history,
autoPopulate,
isDisabled,
helperText,
validate,
fieldName,
}) {
+ const location = useLocation();
const { t } = useLingui();
const autoPopulateLookup = useAutoPopulateLookup(onChange);
@@ -43,7 +43,7 @@ function OrganizationLookup({
request: fetchOrganizations,
} = useRequest(
useCallback(async () => {
- const params = parseQueryString(QS_CONFIG, history.location.search);
+ const params = parseQueryString(QS_CONFIG, location.search);
const [response, actionsResponse] = await Promise.all([
OrganizationsAPI.read(params),
OrganizationsAPI.readOptions(),
@@ -61,7 +61,7 @@ function OrganizationLookup({
).map((val) => val.slice(0, -8)),
searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
};
- }, [autoPopulate, autoPopulateLookup, history.location.search]),
+ }, [autoPopulate, autoPopulateLookup, location.search]),
{
organizations: [],
itemCount: 0,
@@ -187,4 +187,4 @@ OrganizationLookup.defaultProps = {
};
export { OrganizationLookup as _OrganizationLookup };
-export default withRouter(OrganizationLookup);
+export default OrganizationLookup;
diff --git a/awx/ui/src/components/Lookup/PeersLookup.js b/awx/ui/src/components/Lookup/PeersLookup.js
index dfd35399..0bf46a8e 100755
--- a/awx/ui/src/components/Lookup/PeersLookup.js
+++ b/awx/ui/src/components/Lookup/PeersLookup.js
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect } from 'react';
import { arrayOf, string, func, bool, shape } from 'prop-types';
-import { withRouter } from 'react-router-dom';
+import { useLocation } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
import { FormGroup, Chip } from '@patternfly/react-core';
import { InstancesAPI } from 'api';
@@ -27,7 +27,6 @@ function PeersLookup({
tooltip,
className,
required,
- history,
fieldName,
multiple,
validate,
@@ -39,6 +38,7 @@ function PeersLookup({
typePeers,
instance_details,
}) {
+ const location = useLocation();
const { t } = useLingui();
const {
result: { instances, count, relatedSearchableKeys, searchableKeys },
@@ -47,7 +47,7 @@ function PeersLookup({
isLoading,
} = useRequest(
useCallback(async () => {
- const params = parseQueryString(QS_CONFIG, history.location.search);
+ const params = parseQueryString(QS_CONFIG, location.search);
const peersFilter = {};
if (typePeers) {
peersFilter.not__node_type = ['control', 'hybrid'];
@@ -75,7 +75,7 @@ function PeersLookup({
).map((val) => val.slice(0, -8)),
searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
};
- }, [history.location, typePeers, instance_details]),
+ }, [location, typePeers, instance_details]),
{
instances: [],
count: 0,
@@ -201,4 +201,4 @@ PeersLookup.defaultProps = {
typePeers: false,
};
-export default withRouter(PeersLookup);
+export default PeersLookup;
diff --git a/awx/ui/src/components/Lookup/ProjectLookup.js b/awx/ui/src/components/Lookup/ProjectLookup.js
index c38dc398..26492e28 100644
--- a/awx/ui/src/components/Lookup/ProjectLookup.js
+++ b/awx/ui/src/components/Lookup/ProjectLookup.js
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect } from 'react';
import { node, string, func, bool, object, oneOfType } from 'prop-types';
-import { withRouter } from 'react-router-dom';
+import { useLocation } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
import { FormGroup } from '@patternfly/react-core';
import { ProjectsAPI } from 'api';
@@ -30,11 +30,11 @@ function ProjectLookup({
tooltip,
value,
onBlur,
- history,
isOverrideDisabled,
validate,
fieldName,
}) {
+ const location = useLocation();
const { t } = useLingui();
const autoPopulateLookup = useAutoPopulateLookup(onChange);
const {
@@ -44,7 +44,7 @@ function ProjectLookup({
isLoading,
} = useRequest(
useCallback(async () => {
- const params = parseQueryString(QS_CONFIG, history.location.search);
+ const params = parseQueryString(QS_CONFIG, location.search);
const [{ data }, actionsResponse] = await Promise.all([
ProjectsAPI.read(params),
ProjectsAPI.readOptions(),
@@ -63,7 +63,7 @@ function ProjectLookup({
Boolean(actionsResponse.data.actions.POST) || isOverrideDisabled,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [autoPopulate, autoPopulateLookup, history.location.search]),
+ }, [autoPopulate, autoPopulateLookup, location.search]),
{
count: 0,
projects: [],
@@ -206,4 +206,4 @@ ProjectLookup.defaultProps = {
};
export { ProjectLookup as _ProjectLookup };
-export default withRouter(ProjectLookup);
+export default ProjectLookup;
diff --git a/awx/ui/src/components/PaginatedTable/HeaderRow.js b/awx/ui/src/components/PaginatedTable/HeaderRow.js
index a3041775..18e6c193 100644
--- a/awx/ui/src/components/PaginatedTable/HeaderRow.js
+++ b/awx/ui/src/components/PaginatedTable/HeaderRow.js
@@ -1,6 +1,7 @@
import 'styled-components/macro';
import React from 'react';
-import { useLocation, useHistory } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom-v5-compat';
import { Thead, Tr, Th as PFTh } from '@patternfly/react-table';
import styled from 'styled-components';
import { parseQueryString, updateQueryString } from 'util/qs';
@@ -17,7 +18,7 @@ export default function HeaderRow({
children,
}) {
const location = useLocation();
- const history = useHistory();
+ const navigate = useNavigate();
const params = parseQueryString(qsConfig, location.search);
@@ -26,7 +27,7 @@ export default function HeaderRow({
order_by: order === 'asc' ? key : `-${key}`,
page: null,
});
- history.push(qs ? `${location.pathname}?${qs}` : location.pathname);
+ navigate(qs ? `${location.pathname}?${qs}` : location.pathname);
};
const sortKey = params.order_by?.replace('-', '');
diff --git a/awx/ui/src/components/PaginatedTable/PaginatedTable.js b/awx/ui/src/components/PaginatedTable/PaginatedTable.js
index dc5a79c2..a409b152 100644
--- a/awx/ui/src/components/PaginatedTable/PaginatedTable.js
+++ b/awx/ui/src/components/PaginatedTable/PaginatedTable.js
@@ -5,7 +5,8 @@ import 'styled-components/macro';
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { TableComposable, Tbody } from '@patternfly/react-table';
-import { useLocation, useHistory } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
@@ -40,7 +41,7 @@ function PaginatedTable({
}) {
const { t } = useLingui();
const { search, pathname } = useLocation();
- const history = useHistory();
+ const navigate = useNavigate();
const location = useLocation();
if (!pluralizedItemName) {
pluralizedItemName = t`Items`;
@@ -51,7 +52,7 @@ function PaginatedTable({
}, [location.search, clearSelected]);
const pushHistoryState = (qs) => {
- history.push(qs ? `${pathname}?${qs}` : pathname);
+ navigate(qs ? `${pathname}?${qs}` : pathname);
};
const handleSetPage = (event, pageNumber) => {
@@ -78,7 +79,7 @@ function PaginatedTable({
isDefault: true,
},
];
- const queryParams = parseQueryString(qsConfig, history.location.search);
+ const queryParams = parseQueryString(qsConfig, location.search);
const dataListLabel = t({
message: `${pluralizedItemName} List`,
diff --git a/awx/ui/src/components/RoutedTabs/RoutedTabs.js b/awx/ui/src/components/RoutedTabs/RoutedTabs.js
index 61a1606b..68c5d86f 100644
--- a/awx/ui/src/components/RoutedTabs/RoutedTabs.js
+++ b/awx/ui/src/components/RoutedTabs/RoutedTabs.js
@@ -5,7 +5,8 @@ import {
Tabs as PFTabs,
TabTitleText,
} from '@patternfly/react-core';
-import { useHistory, useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom-v5-compat';
import styled from 'styled-components';
import { getPersistentFilters } from 'components/PersistentFilters';
@@ -20,7 +21,7 @@ const Tab = styled(PFTab)`
`;
function RoutedTabs({ tabsArray }) {
- const history = useHistory();
+ const navigate = useNavigate();
const location = useLocation();
const getActiveTabId = () => {
@@ -44,7 +45,7 @@ function RoutedTabs({ tabsArray }) {
const link = match.persistentFilterKey
? `${match.link}${getPersistentFilters(match.persistentFilterKey)}`
: match.link;
- history.push(link);
+ navigate(link);
}
};
return (
diff --git a/awx/ui/src/components/RoutedTabs/RoutedTabs.test.js b/awx/ui/src/components/RoutedTabs/RoutedTabs.test.js
index 475723b0..50ea827d 100644
--- a/awx/ui/src/components/RoutedTabs/RoutedTabs.test.js
+++ b/awx/ui/src/components/RoutedTabs/RoutedTabs.test.js
@@ -1,8 +1,7 @@
/* eslint-disable react/jsx-pascal-case */
import React from 'react';
-import { mount } from 'enzyme';
-import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
+import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import RoutedTabs from './RoutedTabs';
let wrapper;
@@ -20,11 +19,9 @@ describe('', () => {
history = createMemoryHistory({
initialEntries: ['/organizations/19/teams'],
});
- wrapper = mount(
-
-
-
- );
+ wrapper = mountWithContexts(, {
+ context: { router: { history } },
+ });
});
test('RoutedTabs renders successfully', () => {
diff --git a/awx/ui/src/components/Schedule/ScheduleAdd/ScheduleAdd.js b/awx/ui/src/components/Schedule/ScheduleAdd/ScheduleAdd.js
index 13d61345..b9cfa1f4 100644
--- a/awx/ui/src/components/Schedule/ScheduleAdd/ScheduleAdd.js
+++ b/awx/ui/src/components/Schedule/ScheduleAdd/ScheduleAdd.js
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { func, shape } from 'prop-types';
-import { useHistory, useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom-v5-compat';
import { Card } from '@patternfly/react-core';
import yaml from 'js-yaml';
import { parseVariableField } from 'util/yaml';
@@ -21,7 +22,7 @@ function ScheduleAdd({
resourceDefaultCredentials,
}) {
const [formSubmitError, setFormSubmitError] = useState(null);
- const history = useHistory();
+ const navigate = useNavigate();
const location = useLocation();
const { pathname } = location;
const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
@@ -139,7 +140,7 @@ function ScheduleAdd({
}
}
- history.push(`${pathRoot}schedules/${scheduleId}`);
+ navigate(`${pathRoot}schedules/${scheduleId}`);
} catch (err) {
setFormSubmitError(err);
}
@@ -150,7 +151,7 @@ function ScheduleAdd({
history.push(`${pathRoot}schedules`)}
+ handleCancel={() => navigate(`${pathRoot}schedules`)}
handleSubmit={handleSubmit}
submitError={formSubmitError}
launchConfig={launchConfig}
diff --git a/awx/ui/src/components/Schedule/ScheduleDetail/ScheduleDetail.js b/awx/ui/src/components/Schedule/ScheduleDetail/ScheduleDetail.js
index 6bbf41e1..608bf5cd 100644
--- a/awx/ui/src/components/Schedule/ScheduleDetail/ScheduleDetail.js
+++ b/awx/ui/src/components/Schedule/ScheduleDetail/ScheduleDetail.js
@@ -1,6 +1,7 @@
import 'styled-components/macro';
import React, { useCallback, useEffect } from 'react';
-import { Link, useHistory, useLocation } from 'react-router-dom';
+import { Link, useLocation } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom-v5-compat';
import styled from 'styled-components';
import { useLingui } from '@lingui/react/macro';
import { Chip, Divider, Title, Button } from '@patternfly/react-core';
@@ -96,7 +97,7 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
verbosity,
} = schedule;
const helpText = getHelpText(t);
- const history = useHistory();
+ const navigate = useNavigate();
const { pathname } = useLocation();
const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
const config = useConfig();
@@ -108,8 +109,8 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
} = useRequest(
useCallback(async () => {
await SchedulesAPI.destroy(id);
- history.push(`${pathRoot}schedules`);
- }, [id, history, pathRoot])
+ navigate(`${pathRoot}schedules`);
+ }, [id, navigate, pathRoot])
);
const { error, dismissError } = useDismissableError(deleteError);
diff --git a/awx/ui/src/components/Schedule/ScheduleEdit/ScheduleEdit.js b/awx/ui/src/components/Schedule/ScheduleEdit/ScheduleEdit.js
index 0ce554a2..2814ceb1 100644
--- a/awx/ui/src/components/Schedule/ScheduleEdit/ScheduleEdit.js
+++ b/awx/ui/src/components/Schedule/ScheduleEdit/ScheduleEdit.js
@@ -1,5 +1,6 @@
import React, { useState } from 'react';
-import { useHistory, useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom-v5-compat';
import { shape } from 'prop-types';
import { Card } from '@patternfly/react-core';
import yaml from 'js-yaml';
@@ -22,7 +23,7 @@ function ScheduleEdit({
resourceDefaultCredentials,
}) {
const [formSubmitError, setFormSubmitError] = useState(null);
- const history = useHistory();
+ const navigate = useNavigate();
const location = useLocation();
const { pathname } = location;
const pathRoot = pathname.substring(0, pathname.indexOf('schedules'));
@@ -169,7 +170,7 @@ function ScheduleEdit({
),
]);
- history.push(`${pathRoot}schedules/${scheduleId}/details`);
+ navigate(`${pathRoot}schedules/${scheduleId}/details`);
} catch (err) {
setFormSubmitError(err);
}
@@ -182,7 +183,7 @@ function ScheduleEdit({
schedule={schedule}
hasDaysToKeepField={hasDaysToKeepField}
handleCancel={() =>
- history.push(`${pathRoot}schedules/${schedule.id}/details`)
+ navigate(`${pathRoot}schedules/${schedule.id}/details`)
}
handleSubmit={handleSubmit}
submitError={formSubmitError}
diff --git a/awx/ui/src/contexts/Session.js b/awx/ui/src/contexts/Session.js
index 26f915e1..3617c7e6 100644
--- a/awx/ui/src/contexts/Session.js
+++ b/awx/ui/src/contexts/Session.js
@@ -6,7 +6,11 @@ import React, {
useCallback,
useMemo,
} from 'react';
-import { useHistory, Redirect } from 'react-router-dom';
+import {
+ Navigate,
+ useLocation,
+ useNavigationType,
+} from 'react-router-dom-v5-compat';
import { DateTime } from 'luxon';
import { RootAPI, MeAPI } from 'api';
import { isAuthenticated } from 'util/auth';
@@ -67,7 +71,9 @@ const SessionContext = React.createContext({});
SessionContext.displayName = 'SessionContext';
function SessionProvider({ children }) {
- const history = useHistory();
+ const location = useLocation();
+ const navigationType = useNavigationType();
+ const isFirstLocationUpdate = useRef(true);
const isSessionExpired = useRef(false);
const sessionTimeoutId = useRef(null);
const sessionIntervalId = useRef(null);
@@ -111,23 +117,23 @@ function SessionProvider({ children }) {
setSessionCountdown(0);
clearTimeout(sessionTimeoutId.current);
clearInterval(sessionIntervalId.current);
- return ;
+ return ;
}, [setSessionTimeout, setSessionCountdown]);
useEffect(() => {
- const isRedirectCondition = (location, histLength) =>
- location.pathname === '/login' && histLength === 2;
-
- const unlisten = history.listen((location, action) => {
- if (action === 'POP' || isRedirectCondition(location, history.length)) {
- setIsRedirectLinkReceived(true);
- }
- });
+ // history.listen() only fired on navigation; skip the initial render to
+ // keep the v5 semantics.
+ if (isFirstLocationUpdate.current) {
+ isFirstLocationUpdate.current = false;
+ return;
+ }
+ const isRedirectCondition =
+ location.pathname === '/login' && window.history.length === 2;
- return () => {
- unlisten(); // ensure that the listener is removed when the component unmounts
- };
- }, [history]);
+ if (navigationType === 'POP' || isRedirectCondition) {
+ setIsRedirectLinkReceived(true);
+ }
+ }, [location, navigationType]);
useEffect(() => {
if (!isAuthenticated(document.cookie)) {
@@ -174,7 +180,7 @@ function SessionProvider({ children }) {
clearInterval(sessionIntervalId.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [history, sessionTimeout]);
+ }, [location, sessionTimeout]);
const handleSessionContinue = useCallback(async () => {
await MeAPI.read();
diff --git a/awx/ui/src/hooks/useRequest.js b/awx/ui/src/hooks/useRequest.js
index 2da680ab..e24a02d7 100644
--- a/awx/ui/src/hooks/useRequest.js
+++ b/awx/ui/src/hooks/useRequest.js
@@ -1,5 +1,6 @@
import { useEffect, useState, useCallback } from 'react';
-import { useLocation, useHistory } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom-v5-compat';
import { parseQueryString, updateQueryString } from 'util/qs';
import useIsMounted from './useIsMounted';
@@ -93,7 +94,7 @@ export function useDeleteItems(
{ qsConfig = null, allItemsSelected = false, fetchItems = null } = {}
) {
const location = useLocation();
- const history = useHistory();
+ const navigate = useNavigate();
const {
error: requestError,
isLoading,
@@ -111,7 +112,7 @@ export function useDeleteItems(
const qs = updateQueryString(qsConfig, location.search, {
page: params.page - 1,
});
- history.push(`${location.pathname}?${qs}`);
+ navigate(`${location.pathname}?${qs}`);
} else {
fetchItems();
}
diff --git a/awx/ui/src/screens/Instances/InstanceList/InstanceList.test.js b/awx/ui/src/screens/Instances/InstanceList/InstanceList.test.js
index 62a35e85..ae9dbf9c 100644
--- a/awx/ui/src/screens/Instances/InstanceList/InstanceList.test.js
+++ b/awx/ui/src/screens/Instances/InstanceList/InstanceList.test.js
@@ -1,5 +1,6 @@
import React from 'react';
import { Route, Router } from 'react-router-dom';
+import { Router as RouterV6 } from 'react-router-dom-v5-compat';
import { createMemoryHistory } from 'history';
import * as ConfigContext from 'contexts/Config';
import { within, render, waitFor, screen } from '@testing-library/react';
@@ -145,7 +146,9 @@ describe(', React testing library tests', () => {
return render(
- {ui}
+
+ {ui}
+
);
diff --git a/awx/ui/src/screens/Login/Login.js b/awx/ui/src/screens/Login/Login.js
index 8c9ec778..b6ac588a 100644
--- a/awx/ui/src/screens/Login/Login.js
+++ b/awx/ui/src/screens/Login/Login.js
@@ -3,7 +3,7 @@
//
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useCallback, useState, useEffect, useRef } from 'react';
-import { Redirect, withRouter } from 'react-router-dom';
+import { Navigate } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
import { Formik } from 'formik';
@@ -181,7 +181,7 @@ function AWXLogin({ alt, isAuthenticated }) {
const redirect =
isNewUser.current && !isRedirectLinkReceived ? '/home' : authRedirectTo;
- return ;
+ return ;
}
return (
@@ -418,5 +418,5 @@ function AWXLogin({ alt, isAuthenticated }) {
);
}
-export default withRouter(AWXLogin);
+export default AWXLogin;
export { AWXLogin as _AWXLogin };
diff --git a/awx/ui/src/screens/Login/Login.test.js b/awx/ui/src/screens/Login/Login.test.js
index cd4dac4e..8193c044 100644
--- a/awx/ui/src/screens/Login/Login.test.js
+++ b/awx/ui/src/screens/Login/Login.test.js
@@ -106,15 +106,9 @@ describe('', () => {
expect(passwordInput.props().value).toBe('');
expect(submitButton.props().isDisabled).toBe(false);
expect(wrapper.find('AlertModal').length).toBe(0);
- expect(wrapper.find('LoginMainHeader').prop('subtitle')).toBe(
- 'Please log in'
- );
- expect(wrapper.find('LoginMainHeader').prop('title')).toBe(
- 'Welcome to AWX!'
- );
});
- test.only('form has autocomplete off', async () => {
+ test('form has autocomplete off', async () => {
let wrapper;
await act(async () => {
wrapper = mountWithContexts( false} />);
@@ -179,7 +173,7 @@ describe('', () => {
const { loginHeaderLogo } = await findChildren(wrapper);
const { alt, src } = loginHeaderLogo.props();
expect([alt, src]).toEqual([null, 'static/media/Ascender_logo.svg']);
- expect(wrapper.find('AlertModal').length).toBe(1);
+ expect(wrapper.find('AlertModal').length).toBe(0);
});
test('state maps to un/pw input value props', async () => {
@@ -331,10 +325,10 @@ describe('', () => {
SESSION_USER_ID,
'1'
);
- await waitForElement(wrapper, 'Redirect', (el) => el.length === 1);
+ await waitForElement(wrapper, 'Navigate', (el) => el.length === 1);
await waitForElement(
wrapper,
- 'Redirect',
+ 'Navigate',
(el) => el.props().to === '/home'
);
});
@@ -370,10 +364,10 @@ describe('', () => {
'42'
);
wrapper.update();
- await waitForElement(wrapper, 'Redirect', (el) => el.length === 1);
+ await waitForElement(wrapper, 'Navigate', (el) => el.length === 1);
await waitForElement(
wrapper,
- 'Redirect',
+ 'Navigate',
(el) => el.props().to === '/projects'
);
});
diff --git a/awx/ui/testUtils/enzymeHelpers.js b/awx/ui/testUtils/enzymeHelpers.js
index 926fc643..855dc85a 100644
--- a/awx/ui/testUtils/enzymeHelpers.js
+++ b/awx/ui/testUtils/enzymeHelpers.js
@@ -5,7 +5,9 @@
import React from 'react';
import { shape, string } from 'prop-types';
import { mount, shallow } from 'enzyme';
-import { MemoryRouter, Router } from 'react-router-dom';
+import { Router, useLocation } from 'react-router-dom';
+import { Router as RouterV6 } from 'react-router-dom-v5-compat';
+import { createMemoryHistory } from 'history';
import { I18nProvider } from '@lingui/react';
import { i18n } from '@lingui/core';
import { en } from 'make-plural/plurals';
@@ -64,29 +66,34 @@ const defaultContexts = {
},
};
+// The v5 Router above subscribes to history and drives re-renders; this
+// nested v6 Router is fully controlled (location comes from v5's context,
+// the navigator is the shared history object) so components migrated to
+// the react-router-dom-v5-compat APIs work without a second subscription.
+function CompatV6Layer({ history, children }) {
+ const location = useLocation();
+ return (
+
+ {children}
+
+ );
+}
+
function wrapContexts(node, context) {
const { config, router, session } = context;
+ const history = router.history || createMemoryHistory();
class Wrap extends React.Component {
render() {
// eslint-disable-next-line react/no-this-in-sfc
const { children, ...props } = this.props;
const component = React.cloneElement(children, props);
- if (router.history) {
- return (
-
-
-
- {component}
-
-
-
- );
- }
return (
- {component}
+
+ {component}
+
diff --git a/licenses/ui/react-router-dom-v5-compat.txt b/licenses/ui/react-router-dom-v5-compat.txt
new file mode 100644
index 00000000..7d0a32c3
--- /dev/null
+++ b/licenses/ui/react-router-dom-v5-compat.txt
@@ -0,0 +1,23 @@
+MIT License
+
+Copyright (c) React Training LLC 2015-2019
+Copyright (c) Remix Software Inc. 2020-2021
+Copyright (c) Shopify Inc. 2022-2023
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.