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}
displayKey="username"
onRowClick={this.handleResourceCheckboxClick}
onSearch={readUsers}
fetchItems={readUsers}
selectedLabel={i18n._(t`Selected`)}
selectedResourceRows={selectedResourceRows}
sortedColumnKey="username"
@ -269,7 +269,7 @@ class AddResourceRole extends React.Component {
searchColumns={teamSearchColumns}
sortColumns={teamSortColumns}
onRowClick={this.handleResourceCheckboxClick}
onSearch={readTeams}
fetchItems={readTeams}
selectedLabel={i18n._(t`Selected`)}
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 { withRouter } from 'react-router-dom';
import { withRouter, useLocation } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import useRequest from '../../util/useRequest';
import { SearchColumns, SortColumns } from '../../types';
import PaginatedDataList from '../PaginatedDataList';
import DataListToolbar from '../DataListToolbar';
@ -10,124 +12,94 @@ import CheckboxListItem from '../CheckboxListItem';
import SelectedList from '../SelectedList';
import { getQSConfig, parseQueryString } from '../../util/qs';
class SelectResourceStep extends React.Component {
constructor(props) {
super(props);
const QS_Config = sortColumns => {
return getQSConfig('resource', {
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 = {
isInitialized: false,
count: null,
error: false,
const {
isLoading,
error,
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: [],
};
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();
itemCount: 0,
}
}
);
async readResourceList() {
const { onSearch, location } = this.props;
const queryParams = parseQueryString(this.qsConfig, location.search);
useEffect(() => {
readResourceList();
}, [readResourceList]);
this.setState({
isLoading: true,
error: false,
});
try {
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>
return (
<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.`
)}
{error ? <div>error</div> : ''}
</Fragment>
);
}
</div>
{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 = {
@ -135,7 +107,7 @@ SelectResourceStep.propTypes = {
sortColumns: SortColumns,
displayKey: PropTypes.string,
onRowClick: PropTypes.func,
onSearch: PropTypes.func.isRequired,
fetchItems: PropTypes.func.isRequired,
selectedLabel: PropTypes.string,
selectedResourceRows: PropTypes.arrayOf(PropTypes.object),
};

View File

@ -1,7 +1,11 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { act } from 'react-dom/test-utils';
import { shallow } from 'enzyme';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import {
mountWithContexts,
waitForElement,
} from '../../../testUtils/enzymeHelpers';
import { sleep } from '../../../testUtils/testUtils';
import SelectResourceStep from './SelectResourceStep';
@ -30,12 +34,12 @@ describe('<SelectResourceStep />', () => {
sortColumns={sortColumns}
displayKey="username"
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({
data: {
count: 2,
@ -45,61 +49,24 @@ describe('<SelectResourceStep />', () => {
],
},
});
mountWithContexts(
<SelectResourceStep
searchColumns={searchColumns}
sortColumns={sortColumns}
displayKey="username"
onRowClick={() => {}}
onSearch={handleSearch}
/>
);
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<SelectResourceStep
searchColumns={searchColumns}
sortColumns={sortColumns}
displayKey="username"
onRowClick={() => {}}
fetchItems={handleSearch}
/>
);
});
expect(handleSearch).toHaveBeenCalledWith({
order_by: 'username',
page: 1,
page_size: 5,
});
});
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' },
]);
waitForElement(wrapper, 'CheckBoxListItem', el => el.length === 2);
});
test('clicking on row fires callback with correct params', async () => {
@ -111,20 +78,24 @@ describe('<SelectResourceStep />', () => {
{ id: 2, username: 'bar', url: 'item/2' },
],
};
const wrapper = mountWithContexts(
<SelectResourceStep
searchColumns={searchColumns}
sortColumns={sortColumns}
displayKey="username"
onRowClick={handleRowClick}
onSearch={() => ({ data })}
selectedResourceRows={[]}
/>
);
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<SelectResourceStep
searchColumns={searchColumns}
sortColumns={sortColumns}
displayKey="username"
onRowClick={handleRowClick}
fetchItems={() => ({ data })}
selectedResourceRows={[]}
/>
);
});
await sleep(0);
wrapper.update();
const checkboxListItemWrapper = wrapper.find('CheckboxListItem');
expect(checkboxListItemWrapper.length).toBe(2);
checkboxListItemWrapper
.first()
.find('input[type="checkbox"]')

View File

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

View File

@ -65,9 +65,10 @@ describe('<UserAndTeamAccessAdd/>', () => {
expect(wrapper.find('PFWizard').length).toBe(1);
});
test('should disable steps', async () => {
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
expect(
wrapper
.find('WizardNavItem[text="Select ttems from list"]')
.find('WizardNavItem[text="Select items from list"]')
.prop('isDisabled')
).toBe(true);
expect(
@ -93,7 +94,7 @@ describe('<UserAndTeamAccessAdd/>', () => {
).toBe(false);
expect(
wrapper
.find('WizardNavItem[text="Select ttems from list"]')
.find('WizardNavItem[text="Select items from list"]')
.prop('isDisabled')
).toBe(false);
expect(
@ -119,6 +120,12 @@ describe('<UserAndTeamAccessAdd/>', () => {
await act(async () =>
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);
expect(JobTemplatesAPI.read).toHaveBeenCalled();
await act(async () =>

View File

@ -8,7 +8,7 @@ import {
OrganizationsAPI,
} from '../../api';
export default function getResourceTypes(i18n) {
export default function getResourceAccessConfig(i18n) {
return [
{
selectedResource: 'jobTemplate',
@ -38,7 +38,7 @@ export default function getResourceTypes(i18n) {
key: 'name',
},
],
fetchItems: () => JobTemplatesAPI.read(),
fetchItems: queryParams => JobTemplatesAPI.read(queryParams),
},
{
selectedResource: 'workflowJobTemplate',
@ -68,7 +68,7 @@ export default function getResourceTypes(i18n) {
key: 'name',
},
],
fetchItems: () => WorkflowJobTemplatesAPI.read(),
fetchItems: queryParams => WorkflowJobTemplatesAPI.read(queryParams),
},
{
selectedResource: 'credential',
@ -109,7 +109,7 @@ export default function getResourceTypes(i18n) {
key: 'name',
},
],
fetchItems: () => CredentialsAPI.read(),
fetchItems: queryParams => CredentialsAPI.read(queryParams),
},
{
selectedResource: 'inventory',
@ -135,7 +135,7 @@ export default function getResourceTypes(i18n) {
key: 'name',
},
],
fetchItems: () => InventoriesAPI.read(),
fetchItems: queryParams => InventoriesAPI.read(queryParams),
},
{
selectedResource: 'project',
@ -176,7 +176,7 @@ export default function getResourceTypes(i18n) {
key: 'name',
},
],
fetchItems: () => ProjectsAPI.read(),
fetchItems: queryParams => ProjectsAPI.read(queryParams),
},
{
selectedResource: 'organization',
@ -202,7 +202,7 @@ export default function getResourceTypes(i18n) {
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 { getQSConfig, parseQueryString } from '../../../util/qs';
import TeamAccessListItem from './TeamAccessListItem';
import UserAndTeamAccessAdd from '../../../components/UserAccessAdd/UserAndTeamAccessAdd';
import UserAndTeamAccessAdd from '../../../components/UserAndTeamAccessAdd/UserAndTeamAccessAdd';
const QS_CONFIG = getQSConfig('team', {
page: 1,

View File

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