From d03448aa9d1323d5ed1122c5842bc390566f529c Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 20 Aug 2020 12:53:26 -0400 Subject: [PATCH] add searchable keys support for AssociateModal and SelectResourceStep lists --- .../components/AddRole/AddResourceRole.jsx | 6 ++++ .../components/AddRole/SelectResourceStep.jsx | 29 +++++++++++++++---- .../AddRole/SelectResourceStep.test.jsx | 21 ++++++++++++++ .../AssociateModal/AssociateModal.jsx | 24 +++++++++++---- .../AssociateModal/AssociateModal.test.jsx | 10 +++++++ .../UserAndTeamAccessAdd.jsx | 1 + .../UserAndTeamAccessAdd.test.jsx | 13 +++++++++ .../getResourceAccessConfig.js | 6 ++++ .../Host/HostGroups/HostGroupsList.jsx | 6 ++++ .../Host/HostGroups/HostGroupsList.test.jsx | 9 ++++++ .../InventoryGroupHostList.jsx | 6 ++++ .../InventoryHostGroupsList.jsx | 6 ++++ .../InventoryHostGroupsList.test.jsx | 9 ++++++ 13 files changed, 135 insertions(+), 11 deletions(-) diff --git a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx index 2da3b02e9d..95cb910295 100644 --- a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx +++ b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx @@ -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} /> diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx index 461327587e..f9a73d24ce 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx @@ -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 => ( i.id === item.id)} diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx index c4c83f9d3b..4e307b7595 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx @@ -35,6 +35,7 @@ describe('', () => { displayKey="username" onRowClick={() => {}} fetchItems={() => {}} + fetchOptions={() => {}} /> ); }); @@ -49,6 +50,15 @@ describe('', () => { ], }, }); + const options = jest.fn().mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); let wrapper; await act(async () => { wrapper = mountWithContexts( @@ -58,6 +68,7 @@ describe('', () => { displayKey="username" onRowClick={() => {}} fetchItems={handleSearch} + fetchOptions={options} /> ); }); @@ -78,6 +89,15 @@ describe('', () => { { 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('', () => { displayKey="username" onRowClick={handleRowClick} fetchItems={() => ({ data })} + fetchOptions={options} selectedResourceRows={[]} /> ); diff --git a/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx b/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx index 339b5ed744..f7a1b2da67 100644 --- a/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx +++ b/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx @@ -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} /> diff --git a/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx b/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx index 2b2280b38b..4b58e900e3 100644 --- a/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx +++ b/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx @@ -15,6 +15,15 @@ describe('', () => { 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('', () => { onClose={onClose} onAssociate={onAssociate} fetchRequest={fetchRequest} + optionsRequest={optionsRequest} isModalOpen /> ); diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx index ceac1a32f5..ad83ce61e9 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx @@ -96,6 +96,7 @@ function UserAndTeamAccessAdd({ displayKey="name" onRowClick={handleResourceSelect} fetchItems={selectedResourceType.fetchItems} + fetchOptions={selectedResourceType.fetchOptions} selectedLabel={i18n._(t`Selected`)} selectedResourceRows={resourcesSelected} sortedColumnKey="username" diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx index a46d5d87a3..7ad19c9057 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx @@ -43,6 +43,15 @@ describe('', () => { count: 1, }, }; + const options = { + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }; let wrapper; beforeEach(async () => { await act(async () => { @@ -111,11 +120,13 @@ describe('', () => { 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('', () => { test('should throw error', async () => { JobTemplatesAPI.read.mockResolvedValue(resources); + JobTemplatesAPI.readOptions.mockResolvedValue(options); UsersAPI.associateRole.mockRejectedValue( new Error({ response: { @@ -192,6 +204,7 @@ describe('', () => { await act(async () => wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({ fetchItems: JobTemplatesAPI.read, + fetchOptions: JobTemplatesAPI.readOptions, label: 'Job template', selectedResource: 'jobTemplate', searchColumns: [ diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js index cd922c23aa..8df258973b 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js @@ -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(), }, ]; } diff --git a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx index 6233df73bf..f902b5e0a6 100644 --- a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx +++ b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx @@ -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 }) { setIsModalOpen(false)} diff --git a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx index 1b2984ed81..4720de35ed 100644 --- a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx +++ b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx @@ -207,6 +207,15 @@ describe('', () => { 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'); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx index 8584c01207..84203a700e 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx @@ -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 }) { setIsModalOpen(false)} diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx index 2f8d8fa1ea..609de04f5b 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx @@ -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 }) { setIsModalOpen(false)} diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx index 1b72f73bd1..493e9dc65a 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx @@ -199,6 +199,15 @@ describe('', () => { 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'); });