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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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