diff --git a/awx/ui/src/components/AddRole/CheckboxCard.js b/awx/ui/src/components/AddRole/CheckboxCard.js
index 0b3f96fb7e..828df6a213 100644
--- a/awx/ui/src/components/AddRole/CheckboxCard.js
+++ b/awx/ui/src/components/AddRole/CheckboxCard.js
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
import { Checkbox as PFCheckbox } from '@patternfly/react-core';
import styled from 'styled-components';
@@ -17,27 +17,25 @@ const Checkbox = styled(PFCheckbox)`
}
`;
-class CheckboxCard extends Component {
- render() {
- const { name, description, isSelected, onSelect, itemId } = this.props;
- return (
-
-
- {name}
- {description}
- >
- }
- value={itemId}
- />
-
- );
- }
+function CheckboxCard(props) {
+ const { name, description, isSelected, onSelect, itemId } = props;
+ return (
+
+
+ {name}
+ {description}
+ >
+ }
+ value={itemId}
+ />
+
+ );
}
CheckboxCard.propTypes = {
diff --git a/awx/ui/src/components/AppContainer/NavExpandableGroup.js b/awx/ui/src/components/AppContainer/NavExpandableGroup.js
index 10a59fcd60..694545ec02 100644
--- a/awx/ui/src/components/AppContainer/NavExpandableGroup.js
+++ b/awx/ui/src/components/AppContainer/NavExpandableGroup.js
@@ -1,60 +1,45 @@
-import React, { Component } from 'react';
+import React from 'react';
import PropTypes, { oneOfType, string, arrayOf } from 'prop-types';
-import { matchPath, Link, withRouter } from 'react-router-dom';
+import { matchPath, Link, useHistory } from 'react-router-dom';
import { NavExpandable, NavItem } from '@patternfly/react-core';
-class NavExpandableGroup extends Component {
- constructor(props) {
- super(props);
- const { routes } = this.props;
+function NavExpandableGroup(props) {
+ const history = useHistory();
+ const { groupId, groupTitle, routes } = props;
- // Extract a list of paths from the route params and store them for later. This creates
- // an array of url paths associated with any NavItem component rendered by this component.
- this.navItemPaths = routes.map(({ path }) => path);
- this.isActiveGroup = this.isActiveGroup.bind(this);
- this.isActivePath = this.isActivePath.bind(this);
- }
+ // Extract a list of paths from the route params and store them for later. This creates
+ // an array of url paths associated with any NavItem component rendered by this component.
+ const navItemPaths = routes.map(({ path }) => path);
- isActiveGroup() {
- return this.navItemPaths.some(this.isActivePath);
- }
+ const isActive = navItemPaths.some(isActivePath);
- isActivePath(path) {
- const { history } = this.props;
+ function isActivePath(path) {
return Boolean(matchPath(history.location.pathname, { path }));
}
- render() {
- const { groupId, groupTitle, routes } = this.props;
-
- if (routes.length === 1 && groupId === 'settings') {
- const [{ path }] = routes;
- return (
-
- {groupTitle}
-
- );
- }
-
+ if (routes.length === 1 && groupId === 'settings') {
+ const [{ path }] = routes;
return (
-
- {routes.map(({ path, title }) => (
-
- {title}
-
- ))}
-
+
+ {groupTitle}
+
);
}
+
+ return (
+
+ {routes.map(({ path, title }) => (
+
+ {title}
+
+ ))}
+
+ );
}
NavExpandableGroup.propTypes = {
@@ -63,4 +48,4 @@ NavExpandableGroup.propTypes = {
routes: arrayOf(PropTypes.object).isRequired,
};
-export default withRouter(NavExpandableGroup);
+export default NavExpandableGroup;
diff --git a/awx/ui/src/components/AppContainer/NavExpandableGroup.test.js b/awx/ui/src/components/AppContainer/NavExpandableGroup.test.js
index 486cda9e6d..90c0214ac6 100644
--- a/awx/ui/src/components/AppContainer/NavExpandableGroup.test.js
+++ b/awx/ui/src/components/AppContainer/NavExpandableGroup.test.js
@@ -1,9 +1,11 @@
import React from 'react';
-import { MemoryRouter } from 'react-router-dom';
+import { MemoryRouter, withRouter } from 'react-router-dom';
import { mount } from 'enzyme';
import { Nav } from '@patternfly/react-core';
-import NavExpandableGroup from './NavExpandableGroup';
+import _NavExpandableGroup from './NavExpandableGroup';
+
+const NavExpandableGroup = withRouter(_NavExpandableGroup);
describe('NavExpandableGroup', () => {
test('initialization and render', () => {
@@ -21,47 +23,88 @@ describe('NavExpandableGroup', () => {
/>
- )
- .find('NavExpandableGroup')
- .instance();
+ ).find('NavExpandableGroup');
- expect(component.navItemPaths).toEqual(['/foo', '/bar', '/fiz']);
- expect(component.isActiveGroup()).toEqual(true);
+ expect(component.find('NavItem').length).toEqual(3);
+ let link = component.find('NavItem').at(0);
+ expect(component.find('NavItem').at(0).prop('isActive')).toBeTruthy();
+ expect(link.find('Link').prop('to')).toBe('/foo');
+
+ link = component.find('NavItem').at(1);
+ expect(link.prop('isActive')).toBeFalsy();
+ expect(link.find('Link').prop('to')).toBe('/bar');
+
+ link = component.find('NavItem').at(2);
+ expect(link.prop('isActive')).toBeFalsy();
+ expect(link.find('Link').prop('to')).toBe('/fiz');
});
- describe('isActivePath', () => {
- const params = [
- ['/fo', '/foo', false],
- ['/foo', '/foo', true],
- ['/foo/1/bar/fiz', '/foo', true],
- ['/foo/1/bar/fiz', 'foo', false],
- ['/foo/1/bar/fiz', 'foo/', false],
- ['/foo/1/bar/fiz', '/bar', false],
- ['/foo/1/bar/fiz', '/fiz', false],
- ];
+ test('when location is /foo/1/bar/fiz isActive returns false', () => {
+ const component = mount(
+
+
+
+ ).find('NavExpandableGroup');
- params.forEach(([location, path, expected]) => {
- test(`when location is ${location}, isActivePath('${path}') returns ${expected} `, () => {
- const component = mount(
-
-
-
- )
- .find('NavExpandableGroup')
- .instance();
+ expect(component.find('NavItem').length).toEqual(3);
+ const link = component.find('NavItem').at(0);
+ expect(component.find('NavItem').at(0).prop('isActive')).toBeTruthy();
+ expect(link.find('Link').prop('to')).toBe('/foo');
+ });
- expect(component.isActivePath(path)).toEqual(expected);
- });
- });
+ test('when location is /fo isActive returns false', () => {
+ const component = mount(
+
+
+
+ ).find('NavExpandableGroup');
+
+ expect(component.find('NavItem').length).toEqual(3);
+ const link = component.find('NavItem').at(0);
+ expect(component.find('NavItem').at(0).prop('isActive')).toBeFalsy();
+ expect(link.find('Link').prop('to')).toBe('/foo');
+ });
+
+ test('when location is /foo isActive returns true', () => {
+ const component = mount(
+
+
+
+ ).find('NavExpandableGroup');
+
+ expect(component.find('NavItem').length).toEqual(3);
+ const link = component.find('NavItem').at(0);
+ expect(component.find('NavItem').at(0).prop('isActive')).toBeTruthy();
+ expect(link.find('Link').prop('to')).toBe('/foo');
});
});
diff --git a/awx/ui/src/components/AppendBody/AppendBody.js b/awx/ui/src/components/AppendBody/AppendBody.js
index 62e2551372..f7317187b1 100644
--- a/awx/ui/src/components/AppendBody/AppendBody.js
+++ b/awx/ui/src/components/AppendBody/AppendBody.js
@@ -1,24 +1,17 @@
-import { Component } from 'react';
+import { useEffect } from 'react';
import ReactDOM from 'react-dom';
-class AppendBody extends Component {
- constructor(props) {
- super(props);
- this.el = document.createElement('div');
- }
+function AppendBody({ children }) {
+ const el = document.createElement('div');
- componentDidMount() {
- document.body.appendChild(this.el);
- }
+ useEffect(() => {
+ document.body.appendChild(el);
+ return () => {
+ document.body.removeChild(el);
+ };
+ }, [el]);
- componentWillUnmount() {
- document.body.removeChild(this.el);
- }
-
- render() {
- const { children } = this.props;
- return ReactDOM.createPortal(children, this.el);
- }
+ return ReactDOM.createPortal(children, el);
}
export default AppendBody;
diff --git a/awx/ui/src/components/ListHeader/ListHeader.js b/awx/ui/src/components/ListHeader/ListHeader.js
index 27055f1c45..d44103803e 100644
--- a/awx/ui/src/components/ListHeader/ListHeader.js
+++ b/awx/ui/src/components/ListHeader/ListHeader.js
@@ -1,6 +1,6 @@
-import React, { Fragment } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
-import { withRouter } from 'react-router-dom';
+import { useHistory, useLocation } from 'react-router-dom';
import styled from 'styled-components';
import { Toolbar, ToolbarContent } from '@patternfly/react-core';
@@ -24,122 +24,105 @@ const EmptyStateControlsWrapper = styled.div`
margin-left: 20px;
}
`;
-class ListHeader extends React.Component {
- constructor(props) {
- super(props);
+function ListHeader(props) {
+ const { search, pathname } = useLocation();
+ const history = useHistory();
+ const {
+ emptyStateControls,
+ itemCount,
+ pagination,
+ qsConfig,
+ relatedSearchableKeys,
+ renderToolbar,
+ searchColumns,
+ searchableKeys,
+ sortColumns,
+ } = props;
- this.handleSearch = this.handleSearch.bind(this);
- this.handleReplaceSearch = this.handleReplaceSearch.bind(this);
- this.handleSort = this.handleSort.bind(this);
- this.handleRemove = this.handleRemove.bind(this);
- this.handleRemoveAll = this.handleRemoveAll.bind(this);
- }
-
- handleSearch(key, value) {
- const { location, qsConfig } = this.props;
- const params = parseQueryString(qsConfig, location.search);
- const qs = updateQueryString(qsConfig, location.search, {
+ const handleSearch = (key, value) => {
+ const params = parseQueryString(qsConfig, search);
+ const qs = updateQueryString(qsConfig, search, {
...mergeParams(params, { [key]: value }),
page: 1,
});
- this.pushHistoryState(qs);
- }
+ pushHistoryState(qs);
+ };
- handleReplaceSearch(key, value) {
- const { location, qsConfig } = this.props;
- const qs = updateQueryString(qsConfig, location.search, {
+ const handleReplaceSearch = (key, value) => {
+ const qs = updateQueryString(qsConfig, search, {
[key]: value,
});
- this.pushHistoryState(qs);
- }
+ pushHistoryState(qs);
+ };
- handleRemove(key, value) {
- const { location, qsConfig } = this.props;
- const oldParams = parseQueryString(qsConfig, location.search);
+ const handleRemove = (key, value) => {
+ const oldParams = parseQueryString(qsConfig, search);
const updatedParams = removeParams(qsConfig, oldParams, {
[key]: value,
});
- const qs = updateQueryString(qsConfig, location.search, updatedParams);
- this.pushHistoryState(qs);
- }
+ const qs = updateQueryString(qsConfig, search, updatedParams);
+ pushHistoryState(qs);
+ };
- handleRemoveAll() {
- const { location, qsConfig } = this.props;
- const oldParams = parseQueryString(qsConfig, location.search);
+ const handleRemoveAll = () => {
+ const oldParams = parseQueryString(qsConfig, search);
Object.keys(oldParams).forEach((key) => {
oldParams[key] = null;
});
delete oldParams.page_size;
delete oldParams.order_by;
- const qs = updateQueryString(qsConfig, location.search, oldParams);
- this.pushHistoryState(qs);
- }
+ const qs = updateQueryString(qsConfig, search, oldParams);
+ pushHistoryState(qs);
+ };
- handleSort(key, order) {
- const { location, qsConfig } = this.props;
- const qs = updateQueryString(qsConfig, location.search, {
+ const handleSort = (key, order) => {
+ const qs = updateQueryString(qsConfig, search, {
order_by: order === 'ascending' ? key : `-${key}`,
page: null,
});
- this.pushHistoryState(qs);
- }
+ pushHistoryState(qs);
+ };
- pushHistoryState(queryString) {
- const { history } = this.props;
- const { pathname } = history.location;
+ const pushHistoryState = (queryString) => {
history.push(queryString ? `${pathname}?${queryString}` : pathname);
- }
+ };
- render() {
- const {
- emptyStateControls,
- itemCount,
- searchColumns,
- searchableKeys,
- relatedSearchableKeys,
- sortColumns,
- renderToolbar,
- qsConfig,
- location,
- pagination,
- } = this.props;
- const params = parseQueryString(qsConfig, location.search);
- const isEmpty = itemCount === 0 && Object.keys(params).length === 0;
- return (
- <>
- {isEmpty ? (
-
-
-
- {emptyStateControls}
-
-
-
- ) : (
- <>
- {renderToolbar({
- itemCount,
- searchColumns,
- sortColumns,
- searchableKeys,
- relatedSearchableKeys,
- onSearch: this.handleSearch,
- onReplaceSearch: this.handleReplaceSearch,
- onSort: this.handleSort,
- onRemove: this.handleRemove,
- clearAllFilters: this.handleRemoveAll,
- qsConfig,
- pagination,
- })}
- >
- )}
- >
- );
- }
+ const params = parseQueryString(qsConfig, search);
+ const isEmpty = itemCount === 0 && Object.keys(params).length === 0;
+ return (
+ <>
+ {isEmpty ? (
+
+
+
+ {emptyStateControls}
+
+
+
+ ) : (
+ <>
+ {renderToolbar({
+ itemCount,
+ searchColumns,
+ sortColumns,
+ searchableKeys,
+ relatedSearchableKeys,
+ onSearch: handleSearch,
+ onReplaceSearch: handleReplaceSearch,
+ onSort: handleSort,
+ onRemove: handleRemove,
+ clearAllFilters: handleRemoveAll,
+ qsConfig,
+ pagination,
+ })}
+ >
+ )}
+ >
+ );
}
ListHeader.propTypes = {
@@ -159,4 +142,4 @@ ListHeader.defaultProps = {
relatedSearchableKeys: [],
};
-export default withRouter(ListHeader);
+export default ListHeader;
diff --git a/awx/ui/src/components/ListHeader/ListHeader.test.js b/awx/ui/src/components/ListHeader/ListHeader.test.js
index 6cca8a7969..60e304110a 100644
--- a/awx/ui/src/components/ListHeader/ListHeader.test.js
+++ b/awx/ui/src/components/ListHeader/ListHeader.test.js
@@ -7,11 +7,15 @@ describe('ListHeader', () => {
const qsConfig = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: 'foo' },
- integerFields: [],
+ integerFields: ['id', 'page', 'page_size'],
+ dateFields: ['modified', 'created'],
};
const renderToolbarFn = jest.fn();
test('initially renders without crashing', () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/organizations/1/teams'],
+ });
const wrapper = mountWithContexts(
{
]}
sortColumns={[{ name: 'foo', key: 'foo' }]}
renderToolbar={renderToolbarFn}
- />
+ />,
+ { context: { router: { history } } }
);
expect(wrapper.length).toBe(1);
});
diff --git a/awx/ui/src/components/PaginatedTable/PaginatedTable.js b/awx/ui/src/components/PaginatedTable/PaginatedTable.js
index 37ac22c4a9..ee1d379014 100644
--- a/awx/ui/src/components/PaginatedTable/PaginatedTable.js
+++ b/awx/ui/src/components/PaginatedTable/PaginatedTable.js
@@ -137,14 +137,14 @@ function PaginatedTable({
return (
<>
{Content}
{items.length ? (
diff --git a/awx/ui/src/components/SelectedList/SelectedList.js b/awx/ui/src/components/SelectedList/SelectedList.js
index c386ffe54c..07846cd552 100644
--- a/awx/ui/src/components/SelectedList/SelectedList.js
+++ b/awx/ui/src/components/SelectedList/SelectedList.js
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
import { Chip, Split as PFSplit, SplitItem } from '@patternfly/react-core';
@@ -16,42 +16,34 @@ const SplitLabelItem = styled(SplitItem)`
word-break: initial;
`;
-class SelectedList extends Component {
- render() {
- const {
- label,
- selected,
- onRemove,
- displayKey,
- isReadOnly,
- renderItemChip,
- } = this.props;
+function SelectedList(props) {
+ const { label, selected, onRemove, displayKey, isReadOnly, renderItemChip } =
+ props;
- const renderChip =
- renderItemChip ||
- (({ item, removeItem }) => (
-
- {item[displayKey]}
-
- ));
+ const renderChip =
+ renderItemChip ||
+ (({ item, removeItem }) => (
+
+ {item[displayKey]}
+
+ ));
- return (
-
- {label}
-
-
- {selected.map((item) =>
- renderChip({
- item,
- removeItem: () => onRemove(item),
- canDelete: !isReadOnly,
- })
- )}
-
-
-
- );
- }
+ return (
+
+ {label}
+
+
+ {selected.map((item) =>
+ renderChip({
+ item,
+ removeItem: () => onRemove(item),
+ canDelete: !isReadOnly,
+ })
+ )}
+
+
+
+ );
}
SelectedList.propTypes = {
diff --git a/awx/ui/src/screens/Team/TeamAdd/TeamAdd.js b/awx/ui/src/screens/Team/TeamAdd/TeamAdd.js
index bc23d337e3..dd5d979ff1 100644
--- a/awx/ui/src/screens/Team/TeamAdd/TeamAdd.js
+++ b/awx/ui/src/screens/Team/TeamAdd/TeamAdd.js
@@ -1,5 +1,5 @@
-import React from 'react';
-import { withRouter } from 'react-router-dom';
+import React, { useState } from 'react';
+import { useHistory } from 'react-router-dom';
import { PageSection, Card } from '@patternfly/react-core';
import { TeamsAPI } from 'api';
@@ -7,54 +7,48 @@ import { Config } from 'contexts/Config';
import { CardBody } from 'components/Card';
import TeamForm from '../shared/TeamForm';
-class TeamAdd extends React.Component {
- constructor(props) {
- super(props);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleCancel = this.handleCancel.bind(this);
- this.state = { error: null };
- }
+function TeamAdd() {
+ const [submitError, setSubmitError] = useState(null);
+ const history = useHistory();
- async handleSubmit(values) {
- const { history } = this.props;
+ const handleSubmit = async (values) => {
try {
- const valuesToSend = { ...values };
- valuesToSend.organization = valuesToSend.organization.id;
+ const {
+ name,
+ description,
+ organization: { id },
+ } = values;
+ const valuesToSend = { name, description, organization: id };
const { data: response } = await TeamsAPI.create(valuesToSend);
history.push(`/teams/${response.id}`);
} catch (error) {
- this.setState({ error });
+ setSubmitError(error);
}
- }
+ };
- handleCancel() {
- const { history } = this.props;
+ const handleCancel = () => {
history.push('/teams');
- }
+ };
- render() {
- const { error } = this.state;
-
- return (
-
-
-
-
- {({ me }) => (
-
- )}
-
-
-
-
- );
- }
+ return (
+
+
+
+
+ {({ me }) => (
+
+ )}
+
+
+
+
+ );
}
export { TeamAdd as _TeamAdd };
-export default withRouter(TeamAdd);
+export default TeamAdd;
diff --git a/awx/ui/src/screens/Team/TeamAdd/TeamAdd.test.js b/awx/ui/src/screens/Team/TeamAdd/TeamAdd.test.js
index c18c55e927..8a7a193d5c 100644
--- a/awx/ui/src/screens/Team/TeamAdd/TeamAdd.test.js
+++ b/awx/ui/src/screens/Team/TeamAdd/TeamAdd.test.js
@@ -12,8 +12,7 @@ jest.mock('../../../api');
describe('', () => {
test('handleSubmit should post to api', async () => {
- TeamsAPI.create.mockResolvedValueOnce({ data: {} });
- const wrapper = mountWithContexts();
+ const history = createMemoryHistory({});
const updatedTeamData = {
name: 'new name',
description: 'new description',
@@ -22,6 +21,10 @@ describe('', () => {
name: 'Default',
},
};
+ TeamsAPI.create.mockResolvedValueOnce({ data: {} });
+ const wrapper = mountWithContexts(, {
+ context: { router: { history } },
+ });
await act(async () => {
wrapper.find('TeamForm').invoke('handleSubmit')(updatedTeamData);
});
diff --git a/awx/ui/src/util/qs.js b/awx/ui/src/util/qs.js
index 1ef09d4be3..6f63a6372d 100644
--- a/awx/ui/src/util/qs.js
+++ b/awx/ui/src/util/qs.js
@@ -228,7 +228,7 @@ export function updateQueryString(config, queryString, newParams) {
return encodeQueryString(allParams);
}
-function parseFullQueryString(queryString) {
+function parseFullQueryString(queryString = '') {
const allParams = {};
queryString
.replace(/^\?/, '')