Improves naming and updates resource list and adds search functionality

This commit is contained in:
Alex Corey
2020-05-28 10:20:15 -04:00
parent 4f6d7e56eb
commit 585ca082e3
9 changed files with 147 additions and 196 deletions

View File

@@ -258,7 +258,7 @@ class AddResourceRole extends React.Component {
sortColumns={userSortColumns} sortColumns={userSortColumns}
displayKey="username" displayKey="username"
onRowClick={this.handleResourceCheckboxClick} onRowClick={this.handleResourceCheckboxClick}
onSearch={readUsers} fetchItems={readUsers}
selectedLabel={i18n._(t`Selected`)} selectedLabel={i18n._(t`Selected`)}
selectedResourceRows={selectedResourceRows} selectedResourceRows={selectedResourceRows}
sortedColumnKey="username" sortedColumnKey="username"
@@ -269,7 +269,7 @@ class AddResourceRole extends React.Component {
searchColumns={teamSearchColumns} searchColumns={teamSearchColumns}
sortColumns={teamSortColumns} sortColumns={teamSortColumns}
onRowClick={this.handleResourceCheckboxClick} onRowClick={this.handleResourceCheckboxClick}
onSearch={readTeams} fetchItems={readTeams}
selectedLabel={i18n._(t`Selected`)} selectedLabel={i18n._(t`Selected`)}
selectedResourceRows={selectedResourceRows} selectedResourceRows={selectedResourceRows}
/> />

View File

@@ -1,8 +1,10 @@
import React, { Fragment } from 'react'; import React, { Fragment, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom'; import { withRouter, useLocation } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import useRequest from '../../util/useRequest';
import { SearchColumns, SortColumns } from '../../types'; import { SearchColumns, SortColumns } from '../../types';
import PaginatedDataList from '../PaginatedDataList'; import PaginatedDataList from '../PaginatedDataList';
import DataListToolbar from '../DataListToolbar'; import DataListToolbar from '../DataListToolbar';
@@ -10,124 +12,94 @@ import CheckboxListItem from '../CheckboxListItem';
import SelectedList from '../SelectedList'; import SelectedList from '../SelectedList';
import { getQSConfig, parseQueryString } from '../../util/qs'; import { getQSConfig, parseQueryString } from '../../util/qs';
class SelectResourceStep extends React.Component { const QS_Config = sortColumns => {
constructor(props) { return getQSConfig('resource', {
super(props); page: 1,
page_size: 5,
order_by: `${
sortColumns.filter(col => col.key === 'name').length ? 'name' : 'username'
}`,
});
};
function SelectResourceStep({
searchColumns,
sortColumns,
displayKey,
onRowClick,
selectedLabel,
selectedResourceRows,
fetchItems,
i18n,
}) {
const location = useLocation();
this.state = { const {
isInitialized: false, isLoading,
count: null, error,
error: false, request: readResourceList,
result: { resources, itemCount },
} = useRequest(
useCallback(async () => {
const queryParams = parseQueryString(
QS_Config(sortColumns),
location.search
);
const {
data: { count, results },
} = await fetchItems(queryParams);
return { resources: results, itemCount: count };
}, [location, fetchItems, sortColumns]),
{
resources: [], resources: [],
}; itemCount: 0,
this.qsConfig = getQSConfig('resource', {
page: 1,
page_size: 5,
order_by: `${
props.sortColumns.filter(col => col.key === 'name').length
? 'name'
: 'username'
}`,
});
}
componentDidMount() {
this.readResourceList();
}
componentDidUpdate(prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
this.readResourceList();
} }
} );
async readResourceList() { useEffect(() => {
const { onSearch, location } = this.props; readResourceList();
const queryParams = parseQueryString(this.qsConfig, location.search); }, [readResourceList]);
this.setState({ return (
isLoading: true, <Fragment>
error: false, <div>
}); {i18n._(
try { t`Choose the resources that will be receiving new roles. You'll be able to select the roles to apply in the next step. Note that the resources chosen here will receive all roles chosen in the next step.`
const { data } = await onSearch(queryParams);
const { count, results } = data;
this.setState({
resources: results,
count,
isInitialized: true,
isLoading: false,
error: false,
});
} catch (err) {
this.setState({
isLoading: false,
error: true,
});
}
}
render() {
const { isInitialized, isLoading, count, error, resources } = this.state;
const {
searchColumns,
sortColumns,
displayKey,
onRowClick,
selectedLabel,
selectedResourceRows,
i18n,
} = this.props;
return (
<Fragment>
{isInitialized && (
<Fragment>
<div>
{i18n._(
t`Choose the resources that will be receiving new roles. You'll be able to select the roles to apply in the next step. Note that the resources chosen here will receive all roles chosen in the next step.`
)}
</div>
{selectedResourceRows.length > 0 && (
<SelectedList
displayKey={displayKey}
label={selectedLabel}
onRemove={onRowClick}
selected={selectedResourceRows}
/>
)}
<PaginatedDataList
hasContentLoading={isLoading}
items={resources}
itemCount={count}
qsConfig={this.qsConfig}
onRowClick={onRowClick}
toolbarSearchColumns={searchColumns}
toolbarSortColumns={sortColumns}
renderItem={item => (
<CheckboxListItem
isSelected={selectedResourceRows.some(i => i.id === item.id)}
itemId={item.id}
key={item.id}
name={item[displayKey]}
label={item[displayKey]}
onSelect={() => onRowClick(item)}
onDeselect={() => onRowClick(item)}
/>
)}
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
showPageSizeOptions={false}
/>
</Fragment>
)} )}
{error ? <div>error</div> : ''} </div>
</Fragment> {selectedResourceRows.length > 0 && (
); <SelectedList
} displayKey={displayKey}
label={selectedLabel}
onRemove={onRowClick}
selected={selectedResourceRows}
/>
)}
<PaginatedDataList
hasContentLoading={isLoading}
contentError={error}
items={resources}
itemCount={itemCount}
qsConfig={QS_Config(sortColumns)}
onRowClick={onRowClick}
toolbarSearchColumns={searchColumns}
toolbarSortColumns={sortColumns}
renderItem={item => (
<CheckboxListItem
isSelected={selectedResourceRows.some(i => i.id === item.id)}
itemId={item.id}
key={item.id}
name={item[displayKey]}
label={item[displayKey]}
onSelect={() => onRowClick(item)}
onDeselect={() => onRowClick(item)}
/>
)}
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
showPageSizeOptions={false}
/>
</Fragment>
);
} }
SelectResourceStep.propTypes = { SelectResourceStep.propTypes = {
@@ -135,7 +107,7 @@ SelectResourceStep.propTypes = {
sortColumns: SortColumns, sortColumns: SortColumns,
displayKey: PropTypes.string, displayKey: PropTypes.string,
onRowClick: PropTypes.func, onRowClick: PropTypes.func,
onSearch: PropTypes.func.isRequired, fetchItems: PropTypes.func.isRequired,
selectedLabel: PropTypes.string, selectedLabel: PropTypes.string,
selectedResourceRows: PropTypes.arrayOf(PropTypes.object), selectedResourceRows: PropTypes.arrayOf(PropTypes.object),
}; };

View File

@@ -1,7 +1,11 @@
import React from 'react'; import React from 'react';
import { createMemoryHistory } from 'history'; import { act } from 'react-dom/test-utils';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import {
mountWithContexts,
waitForElement,
} from '../../../testUtils/enzymeHelpers';
import { sleep } from '../../../testUtils/testUtils'; import { sleep } from '../../../testUtils/testUtils';
import SelectResourceStep from './SelectResourceStep'; import SelectResourceStep from './SelectResourceStep';
@@ -30,12 +34,12 @@ describe('<SelectResourceStep />', () => {
sortColumns={sortColumns} sortColumns={sortColumns}
displayKey="username" displayKey="username"
onRowClick={() => {}} onRowClick={() => {}}
onSearch={() => {}} fetchItems={() => {}}
/> />
); );
}); });
test('fetches resources on mount', async () => { test('fetches resources on mount and adds items to list', async () => {
const handleSearch = jest.fn().mockResolvedValue({ const handleSearch = jest.fn().mockResolvedValue({
data: { data: {
count: 2, count: 2,
@@ -45,61 +49,24 @@ describe('<SelectResourceStep />', () => {
], ],
}, },
}); });
mountWithContexts( let wrapper;
<SelectResourceStep await act(async () => {
searchColumns={searchColumns} wrapper = mountWithContexts(
sortColumns={sortColumns} <SelectResourceStep
displayKey="username" searchColumns={searchColumns}
onRowClick={() => {}} sortColumns={sortColumns}
onSearch={handleSearch} displayKey="username"
/> onRowClick={() => {}}
); fetchItems={handleSearch}
/>
);
});
expect(handleSearch).toHaveBeenCalledWith({ expect(handleSearch).toHaveBeenCalledWith({
order_by: 'username', order_by: 'username',
page: 1, page: 1,
page_size: 5, page_size: 5,
}); });
}); waitForElement(wrapper, 'CheckBoxListItem', el => el.length === 2);
test('readResourceList properly adds rows to state', async () => {
const selectedResourceRows = [{ id: 1, username: 'foo', url: 'item/1' }];
const handleSearch = jest.fn().mockResolvedValue({
data: {
count: 2,
results: [
{ id: 1, username: 'foo', url: 'item/1' },
{ id: 2, username: 'bar', url: 'item/2' },
],
},
});
const history = createMemoryHistory({
initialEntries: [
'/organizations/1/access?resource.page=1&resource.order_by=-username',
],
});
const wrapper = mountWithContexts(
<SelectResourceStep
searchColumns={searchColumns}
sortColumns={sortColumns}
displayKey="username"
onRowClick={() => {}}
onSearch={handleSearch}
selectedResourceRows={selectedResourceRows}
/>,
{
context: { router: { history, route: { location: history.location } } },
}
).find('SelectResourceStep');
await wrapper.instance().readResourceList();
expect(handleSearch).toHaveBeenCalledWith({
order_by: '-username',
page: 1,
page_size: 5,
});
expect(wrapper.state('resources')).toEqual([
{ id: 1, username: 'foo', url: 'item/1' },
{ id: 2, username: 'bar', url: 'item/2' },
]);
}); });
test('clicking on row fires callback with correct params', async () => { test('clicking on row fires callback with correct params', async () => {
@@ -111,20 +78,24 @@ describe('<SelectResourceStep />', () => {
{ id: 2, username: 'bar', url: 'item/2' }, { id: 2, username: 'bar', url: 'item/2' },
], ],
}; };
const wrapper = mountWithContexts( let wrapper;
<SelectResourceStep await act(async () => {
searchColumns={searchColumns} wrapper = mountWithContexts(
sortColumns={sortColumns} <SelectResourceStep
displayKey="username" searchColumns={searchColumns}
onRowClick={handleRowClick} sortColumns={sortColumns}
onSearch={() => ({ data })} displayKey="username"
selectedResourceRows={[]} onRowClick={handleRowClick}
/> fetchItems={() => ({ data })}
); selectedResourceRows={[]}
/>
);
});
await sleep(0); await sleep(0);
wrapper.update(); wrapper.update();
const checkboxListItemWrapper = wrapper.find('CheckboxListItem'); const checkboxListItemWrapper = wrapper.find('CheckboxListItem');
expect(checkboxListItemWrapper.length).toBe(2); expect(checkboxListItemWrapper.length).toBe(2);
checkboxListItemWrapper checkboxListItemWrapper
.first() .first()
.find('input[type="checkbox"]') .find('input[type="checkbox"]')

View File

@@ -11,7 +11,7 @@ import Wizard from '../Wizard/Wizard';
import useSelected from '../../util/useSelected'; import useSelected from '../../util/useSelected';
import SelectResourceStep from '../AddRole/SelectResourceStep'; import SelectResourceStep from '../AddRole/SelectResourceStep';
import SelectRoleStep from '../AddRole/SelectRoleStep'; import SelectRoleStep from '../AddRole/SelectRoleStep';
import getResourceTypes from './resources.data'; import getResourceAccessConfig from './getResourceAccessConfig';
const Grid = styled.div` const Grid = styled.div`
display: grid; display: grid;
@@ -28,7 +28,7 @@ function UserAndTeamAccessAdd({
apiModel, apiModel,
onClose, onClose,
}) { }) {
const [selectedResourceType, setSelectedResourceType] = useState(); const [selectedResourceType, setSelectedResourceType] = useState(null);
const [stepIdReached, setStepIdReached] = useState(1); const [stepIdReached, setStepIdReached] = useState(1);
const { id: userId } = useParams(); const { id: userId } = useParams();
const { const {
@@ -70,7 +70,7 @@ function UserAndTeamAccessAdd({
name: i18n._(t`Add resource type`), name: i18n._(t`Add resource type`),
component: ( component: (
<Grid> <Grid>
{getResourceTypes(i18n).map(resource => ( {getResourceAccessConfig(i18n).map(resource => (
<SelectableCard <SelectableCard
key={resource.selectedResource} key={resource.selectedResource}
isSelected={ isSelected={
@@ -84,6 +84,7 @@ function UserAndTeamAccessAdd({
))} ))}
</Grid> </Grid>
), ),
enableNext: selectedResourceType !== null,
}, },
{ {
id: 2, id: 2,
@@ -94,7 +95,7 @@ function UserAndTeamAccessAdd({
sortColumns={selectedResourceType.sortColumns} sortColumns={selectedResourceType.sortColumns}
displayKey="name" displayKey="name"
onRowClick={handleResourceSelect} onRowClick={handleResourceSelect}
onSearch={selectedResourceType.fetchItems} fetchItems={selectedResourceType.fetchItems}
selectedLabel={i18n._(t`Selected`)} selectedLabel={i18n._(t`Selected`)}
selectedResourceRows={resourcesSelected} selectedResourceRows={resourcesSelected}
sortedColumnKey="username" sortedColumnKey="username"

View File

@@ -65,9 +65,10 @@ describe('<UserAndTeamAccessAdd/>', () => {
expect(wrapper.find('PFWizard').length).toBe(1); expect(wrapper.find('PFWizard').length).toBe(1);
}); });
test('should disable steps', async () => { test('should disable steps', async () => {
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
expect( expect(
wrapper wrapper
.find('WizardNavItem[text="Select ttems from list"]') .find('WizardNavItem[text="Select items from list"]')
.prop('isDisabled') .prop('isDisabled')
).toBe(true); ).toBe(true);
expect( expect(
@@ -93,7 +94,7 @@ describe('<UserAndTeamAccessAdd/>', () => {
).toBe(false); ).toBe(false);
expect( expect(
wrapper wrapper
.find('WizardNavItem[text="Select ttems from list"]') .find('WizardNavItem[text="Select items from list"]')
.prop('isDisabled') .prop('isDisabled')
).toBe(false); ).toBe(false);
expect( expect(
@@ -119,6 +120,12 @@ describe('<UserAndTeamAccessAdd/>', () => {
await act(async () => await act(async () =>
wrapper.find('Button[type="submit"]').prop('onClick')() wrapper.find('Button[type="submit"]').prop('onClick')()
); );
expect(JobTemplatesAPI.read).toHaveBeenCalledWith({
order_by: 'name',
page: 1,
page_size: 5,
});
await waitForElement(wrapper, 'SelectResourceStep', el => el.length > 0); await waitForElement(wrapper, 'SelectResourceStep', el => el.length > 0);
expect(JobTemplatesAPI.read).toHaveBeenCalled(); expect(JobTemplatesAPI.read).toHaveBeenCalled();
await act(async () => await act(async () =>

View File

@@ -8,7 +8,7 @@ import {
OrganizationsAPI, OrganizationsAPI,
} from '../../api'; } from '../../api';
export default function getResourceTypes(i18n) { export default function getResourceAccessConfig(i18n) {
return [ return [
{ {
selectedResource: 'jobTemplate', selectedResource: 'jobTemplate',
@@ -38,7 +38,7 @@ export default function getResourceTypes(i18n) {
key: 'name', key: 'name',
}, },
], ],
fetchItems: () => JobTemplatesAPI.read(), fetchItems: queryParams => JobTemplatesAPI.read(queryParams),
}, },
{ {
selectedResource: 'workflowJobTemplate', selectedResource: 'workflowJobTemplate',
@@ -68,7 +68,7 @@ export default function getResourceTypes(i18n) {
key: 'name', key: 'name',
}, },
], ],
fetchItems: () => WorkflowJobTemplatesAPI.read(), fetchItems: queryParams => WorkflowJobTemplatesAPI.read(queryParams),
}, },
{ {
selectedResource: 'credential', selectedResource: 'credential',
@@ -109,7 +109,7 @@ export default function getResourceTypes(i18n) {
key: 'name', key: 'name',
}, },
], ],
fetchItems: () => CredentialsAPI.read(), fetchItems: queryParams => CredentialsAPI.read(queryParams),
}, },
{ {
selectedResource: 'inventory', selectedResource: 'inventory',
@@ -135,7 +135,7 @@ export default function getResourceTypes(i18n) {
key: 'name', key: 'name',
}, },
], ],
fetchItems: () => InventoriesAPI.read(), fetchItems: queryParams => InventoriesAPI.read(queryParams),
}, },
{ {
selectedResource: 'project', selectedResource: 'project',
@@ -176,7 +176,7 @@ export default function getResourceTypes(i18n) {
key: 'name', key: 'name',
}, },
], ],
fetchItems: () => ProjectsAPI.read(), fetchItems: queryParams => ProjectsAPI.read(queryParams),
}, },
{ {
selectedResource: 'organization', selectedResource: 'organization',
@@ -202,7 +202,7 @@ export default function getResourceTypes(i18n) {
key: 'name', key: 'name',
}, },
], ],
fetchItems: () => OrganizationsAPI.read(), fetchItems: queryParams => OrganizationsAPI.read(queryParams),
}, },
]; ];
} }

View File

@@ -12,7 +12,7 @@ import DataListToolbar from '../../../components/DataListToolbar';
import PaginatedDataList from '../../../components/PaginatedDataList'; import PaginatedDataList from '../../../components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '../../../util/qs'; import { getQSConfig, parseQueryString } from '../../../util/qs';
import TeamAccessListItem from './TeamAccessListItem'; import TeamAccessListItem from './TeamAccessListItem';
import UserAndTeamAccessAdd from '../../../components/UserAccessAdd/UserAndTeamAccessAdd'; import UserAndTeamAccessAdd from '../../../components/UserAndTeamAccessAdd/UserAndTeamAccessAdd';
const QS_CONFIG = getQSConfig('team', { const QS_CONFIG = getQSConfig('team', {
page: 1, page: 1,

View File

@@ -10,7 +10,7 @@ import useRequest from '../../../util/useRequest';
import PaginatedDataList from '../../../components/PaginatedDataList'; import PaginatedDataList from '../../../components/PaginatedDataList';
import DatalistToolbar from '../../../components/DataListToolbar'; import DatalistToolbar from '../../../components/DataListToolbar';
import UserAccessListItem from './UserAccessListItem'; import UserAccessListItem from './UserAccessListItem';
import UserAndTeamAccessAdd from '../../../components/UserAccessAdd/UserAndTeamAccessAdd'; import UserAndTeamAccessAdd from '../../../components/UserAndTeamAccessAdd/UserAndTeamAccessAdd';
const QS_CONFIG = getQSConfig('roles', { const QS_CONFIG = getQSConfig('roles', {
page: 1, page: 1,