add searchable keys support for AssociateModal and SelectResourceStep lists

This commit is contained in:
John Mitchell 2020-08-20 12:53:26 -04:00
parent aee2a81b27
commit d03448aa9d
13 changed files with 135 additions and 11 deletions

View File

@ -11,8 +11,12 @@ import { TeamsAPI, UsersAPI } from '../../api';
const readUsers = async queryParams =>
UsersAPI.read(Object.assign(queryParams, { is_superuser: false }));
const readUsersOptions = async () => UsersAPI.readOptions();
const readTeams = async queryParams => TeamsAPI.read(queryParams);
const readTeamsOptions = async () => TeamsAPI.readOptions();
class AddResourceRole extends React.Component {
constructor(props) {
super(props);
@ -259,6 +263,7 @@ class AddResourceRole extends React.Component {
displayKey="username"
onRowClick={this.handleResourceCheckboxClick}
fetchItems={readUsers}
fetchOptions={readUsersOptions}
selectedLabel={i18n._(t`Selected`)}
selectedResourceRows={selectedResourceRows}
sortedColumnKey="username"
@ -270,6 +275,7 @@ class AddResourceRole extends React.Component {
sortColumns={teamSortColumns}
onRowClick={this.handleResourceCheckboxClick}
fetchItems={readTeams}
fetchOptions={readTeamsOptions}
selectedLabel={i18n._(t`Selected`)}
selectedResourceRows={selectedResourceRows}
/>

View File

@ -29,6 +29,7 @@ function SelectResourceStep({
selectedLabel,
selectedResourceRows,
fetchItems,
fetchOptions,
i18n,
}) {
const location = useLocation();
@ -37,7 +38,7 @@ function SelectResourceStep({
isLoading,
error,
request: readResourceList,
result: { resources, itemCount },
result: { resources, itemCount, relatedSearchableKeys, searchableKeys },
} = useRequest(
useCallback(async () => {
const queryParams = parseQueryString(
@ -45,14 +46,28 @@ function SelectResourceStep({
location.search
);
const {
data: { count, results },
} = await fetchItems(queryParams);
return { resources: results, itemCount: count };
}, [location, fetchItems, sortColumns]),
const [
{
data: { count, results },
},
actionsResponse,
] = await Promise.all([fetchItems(queryParams), fetchOptions()]);
return {
resources: results,
itemCount: count,
relatedSearchableKeys: (
actionsResponse?.data?.related_search_fields || []
).map(val => val.slice(0, -8)),
searchableKeys: Object.keys(
actionsResponse.data.actions?.GET || {}
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
};
}, [location, fetchItems, fetchOptions, sortColumns]),
{
resources: [],
itemCount: 0,
relatedSearchableKeys: [],
searchableKeys: [],
}
);
@ -84,6 +99,8 @@ function SelectResourceStep({
onRowClick={onRowClick}
toolbarSearchColumns={searchColumns}
toolbarSortColumns={sortColumns}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
renderItem={item => (
<CheckboxListItem
isSelected={selectedResourceRows.some(i => i.id === item.id)}

View File

@ -35,6 +35,7 @@ describe('<SelectResourceStep />', () => {
displayKey="username"
onRowClick={() => {}}
fetchItems={() => {}}
fetchOptions={() => {}}
/>
);
});
@ -49,6 +50,15 @@ describe('<SelectResourceStep />', () => {
],
},
});
const options = jest.fn().mockResolvedValue({
data: {
actions: {
GET: {},
POST: {},
},
related_search_fields: [],
},
});
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
@ -58,6 +68,7 @@ describe('<SelectResourceStep />', () => {
displayKey="username"
onRowClick={() => {}}
fetchItems={handleSearch}
fetchOptions={options}
/>
);
});
@ -78,6 +89,15 @@ describe('<SelectResourceStep />', () => {
{ id: 2, username: 'bar', url: 'item/2' },
],
};
const options = jest.fn().mockResolvedValue({
data: {
actions: {
GET: {},
POST: {},
},
related_search_fields: [],
},
});
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
@ -87,6 +107,7 @@ describe('<SelectResourceStep />', () => {
displayKey="username"
onRowClick={handleRowClick}
fetchItems={() => ({ data })}
fetchOptions={options}
selectedResourceRows={[]}
/>
);

View File

@ -21,6 +21,7 @@ function AssociateModal({
onClose,
onAssociate,
fetchRequest,
optionsRequest,
isModalOpen = false,
}) {
const history = useHistory();
@ -28,24 +29,35 @@ function AssociateModal({
const {
request: fetchItems,
result: { items, itemCount },
result: { items, itemCount, relatedSearchableKeys, searchableKeys },
error: contentError,
isLoading,
} = useRequest(
useCallback(async () => {
const params = parseQueryString(QS_CONFIG, history.location.search);
const {
data: { count, results },
} = await fetchRequest(params);
const [
{
data: { count, results },
},
actionsResponse,
] = await Promise.all([fetchRequest(params), optionsRequest()]);
return {
items: results,
itemCount: count,
relatedSearchableKeys: (
actionsResponse?.data?.related_search_fields || []
).map(val => val.slice(0, -8)),
searchableKeys: Object.keys(
actionsResponse.data.actions?.GET || {}
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
};
}, [fetchRequest, history.location.search]),
}, [fetchRequest, optionsRequest, history.location.search]),
{
items: [],
itemCount: 0,
relatedSearchableKeys: [],
searchableKeys: [],
}
);
@ -132,6 +144,8 @@ function AssociateModal({
key: 'name',
},
]}
searchableKeys={searchableKeys}
relatedSearchableKeys={relatedSearchableKeys}
/>
</Modal>
</Fragment>

View File

@ -15,6 +15,15 @@ describe('<AssociateModal />', () => {
const onClose = jest.fn();
const onAssociate = jest.fn().mockResolvedValue();
const fetchRequest = jest.fn().mockReturnValue({ data: { ...mockHosts } });
const optionsRequest = jest.fn().mockResolvedValue({
data: {
actions: {
GET: {},
POST: {},
},
related_search_fields: [],
},
});
beforeEach(async () => {
await act(async () => {
@ -23,6 +32,7 @@ describe('<AssociateModal />', () => {
onClose={onClose}
onAssociate={onAssociate}
fetchRequest={fetchRequest}
optionsRequest={optionsRequest}
isModalOpen
/>
);

View File

@ -96,6 +96,7 @@ function UserAndTeamAccessAdd({
displayKey="name"
onRowClick={handleResourceSelect}
fetchItems={selectedResourceType.fetchItems}
fetchOptions={selectedResourceType.fetchOptions}
selectedLabel={i18n._(t`Selected`)}
selectedResourceRows={resourcesSelected}
sortedColumnKey="username"

View File

@ -43,6 +43,15 @@ describe('<UserAndTeamAccessAdd/>', () => {
count: 1,
},
};
const options = {
data: {
actions: {
GET: {},
POST: {},
},
related_search_fields: [],
},
};
let wrapper;
beforeEach(async () => {
await act(async () => {
@ -111,11 +120,13 @@ describe('<UserAndTeamAccessAdd/>', () => {
test('should call api to associate role', async () => {
JobTemplatesAPI.read.mockResolvedValue(resources);
JobTemplatesAPI.readOptions.mockResolvedValue(options);
UsersAPI.associateRole.mockResolvedValue({});
await act(async () =>
wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({
fetchItems: JobTemplatesAPI.read,
fetchOptions: JobTemplatesAPI.readOptions,
label: 'Job template',
selectedResource: 'jobTemplate',
searchColumns: [
@ -169,6 +180,7 @@ describe('<UserAndTeamAccessAdd/>', () => {
test('should throw error', async () => {
JobTemplatesAPI.read.mockResolvedValue(resources);
JobTemplatesAPI.readOptions.mockResolvedValue(options);
UsersAPI.associateRole.mockRejectedValue(
new Error({
response: {
@ -192,6 +204,7 @@ describe('<UserAndTeamAccessAdd/>', () => {
await act(async () =>
wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({
fetchItems: JobTemplatesAPI.read,
fetchOptions: JobTemplatesAPI.readOptions,
label: 'Job template',
selectedResource: 'jobTemplate',
searchColumns: [

View File

@ -39,6 +39,7 @@ export default function getResourceAccessConfig(i18n) {
},
],
fetchItems: queryParams => JobTemplatesAPI.read(queryParams),
fetchOptions: () => JobTemplatesAPI.readOptions(),
},
{
selectedResource: 'workflowJobTemplate',
@ -69,6 +70,7 @@ export default function getResourceAccessConfig(i18n) {
},
],
fetchItems: queryParams => WorkflowJobTemplatesAPI.read(queryParams),
fetchOptions: () => WorkflowJobTemplatesAPI.readOptions(),
},
{
selectedResource: 'credential',
@ -110,6 +112,7 @@ export default function getResourceAccessConfig(i18n) {
},
],
fetchItems: queryParams => CredentialsAPI.read(queryParams),
fetchOptions: () => CredentialsAPI.readOptions(),
},
{
selectedResource: 'inventory',
@ -136,6 +139,7 @@ export default function getResourceAccessConfig(i18n) {
},
],
fetchItems: queryParams => InventoriesAPI.read(queryParams),
fetchOptions: () => InventoriesAPI.readOptions(),
},
{
selectedResource: 'project',
@ -177,6 +181,7 @@ export default function getResourceAccessConfig(i18n) {
},
],
fetchItems: queryParams => ProjectsAPI.read(queryParams),
fetchOptions: () => ProjectsAPI.readOptions(),
},
{
selectedResource: 'organization',
@ -203,6 +208,7 @@ export default function getResourceAccessConfig(i18n) {
},
],
fetchItems: queryParams => OrganizationsAPI.read(queryParams),
fetchOptions: () => OrganizationsAPI.readOptions(),
},
];
}

View File

@ -118,6 +118,11 @@ function HostGroupsList({ i18n, host }) {
[invId, hostId]
);
const fetchGroupsOptions = useCallback(
() => InventoriesAPI.readGroupsOptions(invId),
[invId]
);
const { request: handleAssociate, error: associateError } = useRequest(
useCallback(
async groupsToAssociate => {
@ -224,6 +229,7 @@ function HostGroupsList({ i18n, host }) {
<AssociateModal
header={i18n._(t`Groups`)}
fetchRequest={fetchGroupsToAssociate}
optionsRequest={fetchGroupsOptions}
isModalOpen={isModalOpen}
onAssociate={handleAssociate}
onClose={() => setIsModalOpen(false)}

View File

@ -207,6 +207,15 @@ describe('<HostGroupsList />', () => {
results: [{ id: 123, name: 'foo', url: '/api/v2/groups/123/' }],
},
});
InventoriesAPI.readGroupsOptions.mockResolvedValue({
data: {
actions: {
GET: {},
POST: {},
},
related_search_fields: [],
},
});
await act(async () => {
wrapper.find('ToolbarAddButton').simulate('click');
});

View File

@ -111,6 +111,11 @@ function InventoryGroupHostList({ i18n }) {
[groupId, inventoryId]
);
const fetchHostsOptions = useCallback(
() => InventoriesAPI.readHostsOptions(inventoryId),
[inventoryId]
);
const { request: handleAssociate, error: associateErr } = useRequest(
useCallback(
async hostsToAssociate => {
@ -227,6 +232,7 @@ function InventoryGroupHostList({ i18n }) {
<AssociateModal
header={i18n._(t`Hosts`)}
fetchRequest={fetchHostsToAssociate}
optionsRequest={fetchHostsOptions}
isModalOpen={isModalOpen}
onAssociate={handleAssociate}
onClose={() => setIsModalOpen(false)}

View File

@ -116,6 +116,11 @@ function InventoryHostGroupsList({ i18n }) {
[invId, hostId]
);
const fetchGroupsOptions = useCallback(
() => InventoriesAPI.readGroupsOptions(invId),
[invId]
);
const { request: handleAssociate, error: associateError } = useRequest(
useCallback(
async groupsToAssociate => {
@ -221,6 +226,7 @@ function InventoryHostGroupsList({ i18n }) {
<AssociateModal
header={i18n._(t`Groups`)}
fetchRequest={fetchGroupsToAssociate}
optionsRequest={fetchGroupsOptions}
isModalOpen={isModalOpen}
onAssociate={handleAssociate}
onClose={() => setIsModalOpen(false)}

View File

@ -199,6 +199,15 @@ describe('<InventoryHostGroupsList />', () => {
results: [{ id: 123, name: 'foo', url: '/api/v2/groups/123/' }],
},
});
InventoriesAPI.readGroupsOptions.mockResolvedValue({
data: {
actions: {
GET: {},
POST: {},
},
related_search_fields: [],
},
});
await act(async () => {
wrapper.find('ToolbarAddButton').simulate('click');
});