diff --git a/awx/ui_next/src/api/models/Applications.js b/awx/ui_next/src/api/models/Applications.js index 51aaeaa2a1..a8fe15f694 100644 --- a/awx/ui_next/src/api/models/Applications.js +++ b/awx/ui_next/src/api/models/Applications.js @@ -11,6 +11,10 @@ class Applications extends Base { params, }); } + + readTokenOptions(appId) { + return this.http.options(`${this.baseUrl}${appId}/tokens/`); + } } export default Applications; diff --git a/awx/ui_next/src/api/models/Users.js b/awx/ui_next/src/api/models/Users.js index 02099c2b67..c9d47826e2 100644 --- a/awx/ui_next/src/api/models/Users.js +++ b/awx/ui_next/src/api/models/Users.js @@ -60,6 +60,10 @@ class Users extends Base { params, }); } + + readTokenOptions(userId) { + return this.http.options(`${this.baseUrl}${userId}/tokens/`); + } } export default Users; diff --git a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx index 5727a78c74..2a8db9b40b 100644 --- a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx +++ b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx @@ -156,31 +156,31 @@ class AddResourceRole extends React.Component { const userSearchColumns = [ { name: i18n._(t`Username`), - key: 'username', + key: 'username__icontains', isDefault: true, }, { name: i18n._(t`First Name`), - key: 'first_name', + key: 'first_name__icontains', }, { name: i18n._(t`Last Name`), - key: 'last_name', + key: 'last_name__icontains', }, ]; const userSortColumns = [ { name: i18n._(t`Username`), - key: 'username', + key: 'username__icontains', }, { name: i18n._(t`First Name`), - key: 'first_name', + key: 'first_name__icontains', }, { name: i18n._(t`Last Name`), - key: 'last_name', + key: 'last_name__icontains', }, ]; diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx index d309ea706f..c4c83f9d3b 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx @@ -13,7 +13,7 @@ describe('', () => { const searchColumns = [ { name: 'Username', - key: 'username', + key: 'username__icontains', isDefault: true, }, ]; diff --git a/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx b/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx index e813e1f70b..339b5ed744 100644 --- a/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx +++ b/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx @@ -114,16 +114,16 @@ function AssociateModal({ searchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ]} sortColumns={[ diff --git a/awx/ui_next/src/components/JobList/JobList.jsx b/awx/ui_next/src/components/JobList/JobList.jsx index 5a18e10233..b1108b8cb9 100644 --- a/awx/ui_next/src/components/JobList/JobList.jsx +++ b/awx/ui_next/src/components/JobList/JobList.jsx @@ -38,7 +38,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { const [selected, setSelected] = useState([]); const location = useLocation(); const { - result: { results, count }, + result: { results, count, actions, relatedSearchFields }, error: contentError, isLoading, request: fetchJobs, @@ -46,12 +46,27 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { useCallback( async () => { const params = parseQueryString(QS_CONFIG, location.search); - const { data } = await UnifiedJobsAPI.read({ ...params }); - return data; + const [response, actionsResponse] = await Promise.all([ + UnifiedJobsAPI.read({ ...params }), + UnifiedJobsAPI.readOptions(), + ]); + return { + results: response.data.results, + count: response.data.count, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + }; }, [location] // eslint-disable-line react-hooks/exhaustive-deps ), - { results: [], count: 0 } + { + results: [], + count: 0, + actions: {}, + relatedSearchFields: [], + } ); useEffect(() => { fetchJobs(); @@ -123,6 +138,11 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { } }; + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); + return ( <> @@ -137,7 +157,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { toolbarSearchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { @@ -146,11 +166,11 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { }, { name: i18n._(t`Label Name`), - key: 'labels__name', + key: 'labels__name__icontains', }, { name: i18n._(t`Job Type`), - key: `type`, + key: `or__type`, options: [ [`project_update`, i18n._(t`Source Control Update`)], [`inventory_update`, i18n._(t`Inventory Sync`)], @@ -162,7 +182,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { }, { name: i18n._(t`Launched By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Status`), @@ -209,6 +229,8 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { key: 'started', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( val.slice(0, -8)), }; }, [selectedType, history.location.search]), - { credentials: [], count: 0 } + { credentials: [], count: 0, actions: {}, relatedSearchFields: [] } ); useEffect(() => { @@ -97,6 +104,11 @@ function CredentialsStep({ i18n }) { /> ); + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); + return ( <> {types && types.length > 0 && ( @@ -129,16 +141,16 @@ function CredentialsStep({ i18n }) { searchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ]} sortColumns={[ @@ -147,6 +159,8 @@ function CredentialsStep({ i18n }) { key: 'name', }, ]} + searchableKeys={searchableKeys} + relatedSearchableKeys={relatedSearchableKeys} multiple={isVault} header={i18n._(t`Credentials`)} name="credentials" diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.test.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.test.jsx index 543ac0baad..aec480547b 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.test.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.test.jsx @@ -31,6 +31,15 @@ describe('CredentialsStep', () => { count: 5, }, }); + CredentialsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); }); test('should load credentials', async () => { diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx index 7360a53f07..70e87ee6aa 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx @@ -27,20 +27,29 @@ function InventoryStep({ i18n }) { const { isLoading, error, - result: { inventories, count }, + result: { inventories, count, actions, relatedSearchFields }, request: fetchInventories, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, history.location.search); - const { data } = await InventoriesAPI.read(params); + const [{ data }, actionsResponse] = await Promise.all([ + InventoriesAPI.read(params), + InventoriesAPI.readOptions(), + ]); return { inventories: data.results, count: data.count, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), }; }, [history.location]), { count: 0, inventories: [], + actions: {}, + relatedSearchFields: [], } ); @@ -48,6 +57,11 @@ function InventoryStep({ i18n }) { fetchInventories(); }, [fetchInventories]); + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); + if (isLoading) { return ; } @@ -63,16 +77,16 @@ function InventoryStep({ i18n }) { searchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ]} sortColumns={[ @@ -81,6 +95,8 @@ function InventoryStep({ i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} header={i18n._(t`Inventory`)} name="inventory" qsConfig={QS_CONFIG} diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.test.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.test.jsx index f2bf29d90a..21380b07a9 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.test.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.test.jsx @@ -21,6 +21,16 @@ describe('InventoryStep', () => { count: 3, }, }); + + InventoriesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); }); test('should load inventories', async () => { diff --git a/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx b/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx index 2d43c0491b..bbcc1eabe6 100644 --- a/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx +++ b/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx @@ -22,22 +22,41 @@ function ApplicationLookup({ i18n, onChange, value, label }) { const location = useLocation(); const { error, - result: { applications, itemCount }, + result: { applications, itemCount, actions, relatedSearchFields }, request: fetchApplications, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const { - data: { results, count }, - } = await ApplicationsAPI.read(params); - return { applications: results, itemCount: count }; + const [ + { + data: { results, count }, + }, + actionsResponse, + ] = await Promise.all([ + ApplicationsAPI.read(params), + ApplicationsAPI.readOptions, + ]); + return { + applications: results, + itemCount: count, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + }; }, [location]), - { applications: [], itemCount: 0 } + { applications: [], itemCount: 0, actions: {}, relatedSearchFields: [] } ); useEffect(() => { fetchApplications(); }, [fetchApplications]); + + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); + return ( dispatch({ type: 'SELECT_ITEM', item })} diff --git a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx index 27b63f585c..aed9f15d73 100644 --- a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx +++ b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx @@ -35,7 +35,7 @@ function CredentialLookup({ tooltip, }) { const { - result: { count, credentials }, + result: { count, credentials, actions, relatedSearchFields }, error, request: fetchCredentials, } = useRequest( @@ -51,16 +51,23 @@ function CredentialLookup({ ? { credential_type__namespace: credentialTypeNamespace } : {}; - const { data } = await CredentialsAPI.read( - mergeParams(params, { - ...typeIdParams, - ...typeKindParams, - ...typeNamespaceParams, - }) - ); + const [{ data }, actionsResponse] = await Promise.all([ + CredentialsAPI.read( + mergeParams(params, { + ...typeIdParams, + ...typeKindParams, + ...typeNamespaceParams, + }) + ), + CredentialsAPI.readOptions, + ]); return { count: data.count, credentials: data.results, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), }; }, [ credentialTypeId, @@ -71,6 +78,8 @@ function CredentialLookup({ { count: 0, credentials: [], + actions: {}, + relatedSearchFields: [], } ); @@ -78,6 +87,11 @@ function CredentialLookup({ fetchCredentials(); }, [fetchCredentials]); + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); + // TODO: replace credential type search with REST-based grabbing of cred types return ( @@ -107,16 +121,16 @@ function CredentialLookup({ searchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ]} sortColumns={[ @@ -125,6 +139,8 @@ function CredentialLookup({ key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} readOnly={!canDelete} name="credential" selectItem={item => dispatch({ type: 'SELECT_ITEM', item })} diff --git a/awx/ui_next/src/components/Lookup/InventoryLookup.jsx b/awx/ui_next/src/components/Lookup/InventoryLookup.jsx index f3afb847f8..46e4d4f0c5 100644 --- a/awx/ui_next/src/components/Lookup/InventoryLookup.jsx +++ b/awx/ui_next/src/components/Lookup/InventoryLookup.jsx @@ -19,26 +19,38 @@ const QS_CONFIG = getQSConfig('inventory', { function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) { const { - result: { inventories, count }, + result: { inventories, count, actions, relatedSearchFields }, request: fetchInventories, error, isLoading, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, history.location.search); - const { data } = await InventoriesAPI.read(params); + const [{ data }, actionsResponse] = await Promise.all([ + InventoriesAPI.read(params), + InventoriesAPI.readOptions(), + ]); return { inventories: data.results, count: data.count, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), }; }, [history.location]), - { inventories: [], count: 0 } + { inventories: [], count: 0, actions: {}, relatedSearchFields: [] } ); useEffect(() => { fetchInventories(); }, [fetchInventories]); + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); + return ( <> { + const parsedParams = parseQueryString(QS_CONFIG, history.location.search); + const [ + { + data: { organization }, + }, + actionsResponse, + ] = await Promise.all([ + InventoriesAPI.readDetail(inventoryId), + InventoriesAPI.readOptions(), + ]); + const { data } = await InventoryScriptsAPI.read( + mergeParams(parsedParams, { organization }) + ); + return { + count: data.count, + inventoryScripts: data.results, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + }; + }, [history.location.search, inventoryId]), + { + count: 0, + inventoryScripts: [], + actions: {}, + relatedSearchFields: [], + } + ); + + useEffect(() => { + fetchInventoryScripts(); + }, [fetchInventoryScripts]); + + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); + + return ( + + ( + dispatch({ type: 'DESELECT_ITEM', item })} + selectItem={item => dispatch({ type: 'SELECT_ITEM', item })} + value={state.selectedItems} + searchColumns={[ + { + name: i18n._(t`Name`), + key: 'name__icontains', + isDefault: true, + }, + { + name: i18n._(t`Created By (Username)`), + key: 'created_by__username__icontains', + }, + { + name: i18n._(t`Modified By (Username)`), + key: 'modified_by__username__icontains', + }, + ]} + sortColumns={[ + { + name: i18n._(t`Name`), + key: 'name', + }, + ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} + /> + )} + /> + + + ); +} + +InventoryScriptLookup.propTypes = { + helperTextInvalid: node, + inventoryId: oneOfType([number, string]).isRequired, + isValid: bool, + onBlur: func, + onChange: func.isRequired, + required: bool, + value: InventoryScript, +}; + +InventoryScriptLookup.defaultProps = { + helperTextInvalid: '', + isValid: true, + onBlur: () => {}, + required: false, + value: null, +}; + +export default withI18n()(withRouter(InventoryScriptLookup)); diff --git a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx index d76bd905c8..1169961740 100644 --- a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx @@ -49,7 +49,7 @@ function MultiCredentialsLookup(props) { }, [fetchTypes]); const { - result: { credentials, credentialsCount }, + result: { credentials, credentialsCount, actions, relatedSearchFields }, request: fetchCredentials, error: credentialsError, isLoading: isCredentialsLoading, @@ -62,15 +62,24 @@ function MultiCredentialsLookup(props) { }; } const params = parseQueryString(QS_CONFIG, history.location.search); - const { results, count } = await loadCredentials(params, selectedType.id); + const [{ results, count }, actionsResponse] = await Promise.all([ + loadCredentials(params, selectedType.id), + CredentialsAPI.readOptions(), + ]); return { credentials: results, credentialsCount: count, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), }; }, [selectedType, history.location]), { credentials: [], credentialsCount: 0, + actions: {}, + relatedSearchFields: [], } ); @@ -95,6 +104,11 @@ function MultiCredentialsLookup(props) { const isVault = selectedType?.kind === 'vault'; + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); + return ( ', () => { count: 3, }, }); + CredentialsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); }); afterEach(() => { diff --git a/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx index 57d4323450..82274cd7fc 100644 --- a/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx +++ b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx @@ -80,16 +80,16 @@ function OrganizationLookup({ searchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { - name: i18n._(t`Created by (username)`), - key: 'created_by__username', + name: i18n._(t`Created By (Username)`), + key: 'created_by__username__icontains', }, { - name: i18n._(t`Modified by (username)`), - key: 'modified_by__username', + name: i18n._(t`Modified By (Username)`), + key: 'modified_by__username__icontains', }, ]} sortColumns={[ diff --git a/awx/ui_next/src/components/Lookup/ProjectLookup.jsx b/awx/ui_next/src/components/Lookup/ProjectLookup.jsx index e0246c574e..e8a0a50122 100644 --- a/awx/ui_next/src/components/Lookup/ProjectLookup.jsx +++ b/awx/ui_next/src/components/Lookup/ProjectLookup.jsx @@ -32,25 +32,34 @@ function ProjectLookup({ history, }) { const { - result: { projects, count }, + result: { projects, count, actions, relatedSearchFields }, request: fetchProjects, error, isLoading, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, history.location.search); - const { data } = await ProjectsAPI.read(params); + const [{ data }, actionsResponse] = await Promise.all([ + ProjectsAPI.read(params), + ProjectsAPI.readOptions(), + ]); if (data.count === 1 && autocomplete) { autocomplete(data.results[0]); } return { count: data.count, projects: data.results, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), }; }, [history.location.search, autocomplete]), { count: 0, projects: [], + actions: {}, + relatedSearchFields: [], } ); @@ -58,6 +67,11 @@ function ProjectLookup({ fetchProjects(); }, [fetchProjects]); + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); + return ( ( diff --git a/awx/ui_next/src/components/OptionsList/OptionsList.test.jsx b/awx/ui_next/src/components/OptionsList/OptionsList.test.jsx index 34bfed560b..5c4498a748 100644 --- a/awx/ui_next/src/components/OptionsList/OptionsList.test.jsx +++ b/awx/ui_next/src/components/OptionsList/OptionsList.test.jsx @@ -17,7 +17,9 @@ describe('', () => { value={[]} options={options} optionCount={3} - searchColumns={[{ name: 'Foo', key: 'foo', isDefault: true }]} + searchColumns={[ + { name: 'Foo', key: 'foo__icontains', isDefault: true }, + ]} sortColumns={[{ name: 'Foo', key: 'foo' }]} qsConfig={qsConfig} selectItem={() => {}} @@ -40,7 +42,9 @@ describe('', () => { value={[options[1]]} options={options} optionCount={3} - searchColumns={[{ name: 'Foo', key: 'foo', isDefault: true }]} + searchColumns={[ + { name: 'Foo', key: 'foo__icontains', isDefault: true }, + ]} sortColumns={[{ name: 'Foo', key: 'foo' }]} qsConfig={qsConfig} selectItem={() => {}} diff --git a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx index 6d8a1bf280..c74d6b5865 100644 --- a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx +++ b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx @@ -80,16 +80,16 @@ function ResourceAccessList({ i18n, apiModel, resource }) { toolbarSearchColumns={[ { name: i18n._(t`Username`), - key: 'username', + key: 'username__icontains', isDefault: true, }, { - name: i18n._(t`First name`), - key: 'first_name', + name: i18n._(t`First Name`), + key: 'first_name__icontains', }, { - name: i18n._(t`Last name`), - key: 'last_name', + name: i18n._(t`Last Name`), + key: 'last_name__icontains', }, ]} toolbarSortColumns={[ @@ -98,11 +98,11 @@ function ResourceAccessList({ i18n, apiModel, resource }) { key: 'username', }, { - name: i18n._(t`First name`), + name: i18n._(t`First Name`), key: 'first_name', }, { - name: i18n._(t`Last name`), + name: i18n._(t`Last Name`), key: 'last_name', }, ]} diff --git a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx index 16ce15c4b4..281e8ac8ff 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx @@ -32,7 +32,7 @@ function ScheduleList({ const location = useLocation(); const { - result: { schedules, itemCount, actions }, + result: { schedules, itemCount, actions, relatedSearchFields }, error: contentError, isLoading, request: fetchSchedules, @@ -49,12 +49,16 @@ function ScheduleList({ schedules: results, itemCount: count, actions: scheduleActions.data.actions, + relatedSearchFields: ( + scheduleActions?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), }; }, [location, loadSchedules, loadScheduleOptions]), { schedules: [], itemCount: 0, actions: {}, + relatedSearchFields: [], } ); @@ -102,6 +106,10 @@ function ScheduleList({ actions && Object.prototype.hasOwnProperty.call(actions, 'POST') && !hideAddButton; + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); return ( <> @@ -123,7 +131,7 @@ function ScheduleList({ toolbarSearchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, ]} @@ -141,6 +149,8 @@ function ScheduleList({ key: 'unified_job_template__polymorphic_ctype__model', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( ', () => { fetchItems: JobTemplatesAPI.read, label: 'Job template', selectedResource: 'jobTemplate', - searchColumns: [{ name: 'Name', key: 'name', isDefault: true }], + searchColumns: [ + { name: 'Name', key: 'name__icontains', isDefault: true }, + ], sortColumns: [{ name: 'Name', key: 'name' }], }) ); @@ -116,7 +118,9 @@ describe('', () => { fetchItems: JobTemplatesAPI.read, label: 'Job template', selectedResource: 'jobTemplate', - searchColumns: [{ name: 'Name', key: 'name', isDefault: true }], + searchColumns: [ + { name: 'Name', key: 'name__icontains', isDefault: true }, + ], sortColumns: [{ name: 'Name', key: 'name' }], }) ); @@ -190,7 +194,9 @@ describe('', () => { fetchItems: JobTemplatesAPI.read, label: 'Job template', selectedResource: 'jobTemplate', - searchColumns: [{ name: 'Name', key: 'name', isDefault: true }], + searchColumns: [ + { name: 'Name', key: 'name__icontains', isDefault: true }, + ], sortColumns: [{ name: 'Name', key: 'name' }], }) ); diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js index 718476e70e..cd922c23aa 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js @@ -16,20 +16,20 @@ export default function getResourceAccessConfig(i18n) { searchColumns: [ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Playbook name`), - key: 'playbook', + key: 'playbook__icontains', }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ], sortColumns: [ @@ -46,20 +46,20 @@ export default function getResourceAccessConfig(i18n) { searchColumns: [ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Playbook name`), - key: 'playbook', + key: 'playbook__icontains', }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ], sortColumns: [ @@ -76,12 +76,12 @@ export default function getResourceAccessConfig(i18n) { searchColumns: [ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Type`), - key: 'scm_type', + key: 'or__scm_type', options: [ [``, i18n._(t`Manual`)], [`git`, i18n._(t`Git`)], @@ -92,15 +92,15 @@ export default function getResourceAccessConfig(i18n) { }, { name: i18n._(t`Source Control URL`), - key: 'scm_url', + key: 'scm_url__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, ], sortColumns: [ @@ -117,16 +117,16 @@ export default function getResourceAccessConfig(i18n) { searchColumns: [ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ], sortColumns: [ @@ -143,12 +143,12 @@ export default function getResourceAccessConfig(i18n) { searchColumns: [ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Type`), - key: 'scm_type', + key: 'or__scm_type', options: [ [``, i18n._(t`Manual`)], [`git`, i18n._(t`Git`)], @@ -159,15 +159,15 @@ export default function getResourceAccessConfig(i18n) { }, { name: i18n._(t`Source Control URL`), - key: 'scm_url', + key: 'scm_url__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, ], sortColumns: [ @@ -184,16 +184,16 @@ export default function getResourceAccessConfig(i18n) { searchColumns: [ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ], sortColumns: [ diff --git a/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx b/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx index b0b29d6264..7d123385ef 100644 --- a/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx @@ -26,14 +26,20 @@ function ApplicationTokenList({ i18n }) { const { error, isLoading, - result: { tokens, itemCount }, + result: { tokens, itemCount, actions, relatedSearchFields }, request: fetchTokens, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const { - data: { results, count }, - } = await ApplicationsAPI.readTokens(id, params); + const [ + { + data: { results, count }, + }, + actionsResponse, + ] = await Promise.all([ + ApplicationsAPI.readTokens(id, params), + ApplicationsAPI.readTokenOptions(id), + ]); const modifiedResults = results.map(result => { result.summary_fields = { user: result.summary_fields.user, @@ -43,9 +49,16 @@ function ApplicationTokenList({ i18n }) { result.name = result.summary_fields.user?.username; return result; }); - return { tokens: modifiedResults, itemCount: count }; + return { + tokens: modifiedResults, + itemCount: count, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + }; }, [id, location.search]), - { tokens: [], itemCount: 0 } + { tokens: [], itemCount: 0, actions: {}, relatedSearchFields: [] } ); useEffect(() => { @@ -77,6 +90,12 @@ function ApplicationTokenList({ i18n }) { await handleDeleteApplications(); setSelected([]); }; + + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); + return ( <> ( ', () => { let wrapper; + + beforeEach(() => { + ApplicationsAPI.readTokenOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); + }); + test('should mount properly', async () => { ApplicationsAPI.readTokens.mockResolvedValue(tokens); await act(async () => { diff --git a/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationsList.jsx b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationsList.jsx index f4f2571863..598f4234c7 100644 --- a/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationsList.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationsList.jsx @@ -32,7 +32,7 @@ function ApplicationsList({ i18n }) { isLoading, error, request: fetchApplications, - result: { applications, itemCount, actions }, + result: { applications, itemCount, actions, relatedSearchFields }, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); @@ -46,12 +46,16 @@ function ApplicationsList({ i18n }) { applications: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), }; }, [location]), { applications: [], itemCount: 0, actions: {}, + relatedSearchFields: [], } ); @@ -85,6 +89,10 @@ function ApplicationsList({ i18n }) { }; const canAdd = actions && actions.POST; + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); return ( <> @@ -101,12 +109,12 @@ function ApplicationsList({ i18n }) { toolbarSearchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Description`), - key: 'description', + key: 'description__icontains', }, ]} toolbarSortColumns={[ @@ -127,6 +135,8 @@ function ApplicationsList({ i18n }) { key: 'description', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( { const params = parseQueryString(QS_CONFIG, history.location.search); - const { data } = await CredentialsAPI.read({ - ...params, - }); + const [{ data }, actionsResponse] = await Promise.all([ + CredentialsAPI.read({ ...params }), + CredentialsAPI.readOptions(), + ]); return { credentials: data.results, count: data.count, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), }; }, [history.location.search]), - { credentials: [], count: 0 } + { credentials: [], count: 0, actions: {}, relatedSearchFields: [] } ); useEffect(() => { fetchCredentials(); }, [fetchCredentials]); + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); + if (credentialsError) { return ; } @@ -76,16 +86,16 @@ function CredentialsStep({ i18n }) { toolbarSearchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ]} toolbarSortColumns={[ @@ -94,6 +104,8 @@ function CredentialsStep({ i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} /> ); } diff --git a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx index 9e8737912d..bfcde651a4 100644 --- a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx +++ b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx @@ -33,7 +33,7 @@ function HostGroupsList({ i18n, host }) { const invId = host.summary_fields.inventory.id; const { - result: { groups, itemCount, actions }, + result: { groups, itemCount, actions, relatedSearchFields }, error: contentError, isLoading, request: fetchGroups, @@ -55,11 +55,16 @@ function HostGroupsList({ i18n, host }) { groups: results, itemCount: count, actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), }; }, [hostId, search]), { groups: [], itemCount: 0, + actions: {}, + relatedSearchFields: [], } ); @@ -123,6 +128,10 @@ function HostGroupsList({ i18n, host }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); return ( <> @@ -136,16 +145,16 @@ function HostGroupsList({ i18n, host }) { toolbarSearchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ]} toolbarSortColumns={[ @@ -154,6 +163,8 @@ function HostGroupsList({ i18n, host }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderItem={item => ( val.slice(0, -8)), }; }, [location]), { hosts: [], count: 0, actions: {}, + relatedSearchFields: [], } ); @@ -93,6 +97,10 @@ function HostList({ i18n }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); return ( @@ -108,16 +116,16 @@ function HostList({ i18n }) { toolbarSearchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ]} toolbarSortColumns={[ @@ -126,6 +134,8 @@ function HostList({ i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( val.slice(0, -8)), }; }, [groupId, inventoryId, location.search]), { hosts: [], hostCount: 0, + actions: {}, + relatedSearchFields: [], } ); @@ -122,6 +127,10 @@ function InventoryGroupHostList({ i18n }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); const addFormUrl = `/inventories/inventory/${inventoryId}/groups/${groupId}/nested_hosts/add`; + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); return ( <> @@ -136,16 +145,16 @@ function InventoryGroupHostList({ i18n }) { toolbarSearchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ]} toolbarSortColumns={[ @@ -154,6 +163,8 @@ function InventoryGroupHostList({ i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( val.slice(0, -8)), }; }, [hostId, search]), // eslint-disable-line react-hooks/exhaustive-deps { groups: [], itemCount: 0, + actions: {}, + relatedSearchFields: [], } ); @@ -121,6 +126,10 @@ function InventoryHostGroupsList({ i18n }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); return ( <> @@ -134,16 +143,16 @@ function InventoryHostGroupsList({ i18n }) { toolbarSearchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ]} toolbarSortColumns={[ @@ -152,6 +161,8 @@ function InventoryHostGroupsList({ i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderItem={item => ( val.slice(0, -8)), }; }, [location]), { results: [], itemCount: 0, actions: {}, + relatedSearchFields: [], } ); @@ -93,6 +97,10 @@ function InventoryList({ i18n }) { const hasContentLoading = isDeleteLoading || isLoading; const canAdd = actions && actions.POST; + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); const handleSelectAll = isSelected => { setSelected(isSelected ? [...inventories] : []); @@ -135,16 +143,16 @@ function InventoryList({ i18n }) { toolbarSearchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ]} toolbarSortColumns={[ @@ -153,6 +161,8 @@ function InventoryList({ i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( val.slice(0, -8)), }; }, [location]), { organizations: [], organizationCount: 0, actions: {}, + relatedSearchFields: [], } ); @@ -86,6 +90,10 @@ function OrganizationsList({ i18n }) { const hasContentLoading = isDeleteLoading || isOrgsLoading; const canAdd = actions && actions.POST; + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); const handleSelectAll = isSelected => { setSelected(isSelected ? [...organizations] : []); @@ -114,16 +122,16 @@ function OrganizationsList({ i18n }) { toolbarSearchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ]} toolbarSortColumns={[ @@ -132,6 +140,8 @@ function OrganizationsList({ i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( val.slice(0, -8)), }; }, [location]), { results: [], itemCount: 0, actions: {}, + relatedSearchFields: [], } ); @@ -85,6 +89,10 @@ function ProjectList({ i18n }) { const hasContentLoading = isDeleteLoading || isLoading; const canAdd = actions && actions.POST; + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); const handleSelectAll = isSelected => { setSelected(isSelected ? [...projects] : []); @@ -113,12 +121,12 @@ function ProjectList({ i18n }) { toolbarSearchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Type`), - key: 'scm_type', + key: 'or__scm_type', options: [ [``, i18n._(t`Manual`)], [`git`, i18n._(t`Git`)], @@ -129,17 +137,19 @@ function ProjectList({ i18n }) { }, { name: i18n._(t`Source Control URL`), - key: 'scm_url', + key: 'scm_url__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarSortColumns={[ { name: i18n._(t`Name`), diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx index 945fbfc8e4..b50e569f18 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx @@ -78,6 +78,7 @@ describe('', () => { GET: {}, POST: {}, }, + related_search_fields: [], }, }); }); diff --git a/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx b/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx index 5dc6580274..f660064266 100644 --- a/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx +++ b/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx @@ -29,7 +29,7 @@ function TeamList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { teams, itemCount, actions }, + result: { teams, itemCount, actions, relatedSearchFields }, error: contentError, isLoading, request: fetchTeams, @@ -44,12 +44,16 @@ function TeamList({ i18n }) { teams: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), }; }, [location]), { teams: [], itemCount: 0, actions: {}, + relatedSearchFields: [], } ); @@ -81,6 +85,10 @@ function TeamList({ i18n }) { const hasContentLoading = isDeleteLoading || isLoading; const canAdd = actions && actions.POST; + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); const handleSelectAll = isSelected => { setSelected(isSelected ? [...teams] : []); @@ -109,20 +117,20 @@ function TeamList({ i18n }) { toolbarSearchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Organization Name`), - key: 'organization__name', + key: 'organization__name__icontains', }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ]} toolbarSortColumns={[ @@ -131,6 +139,8 @@ function TeamList({ i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( { const params = parseQueryString(QS_CONFIG, search); @@ -46,22 +46,30 @@ function TeamRolesList({ i18n, me, team }) { data: { results, count }, }, { count: orgAdminCount }, + actionsResponse, ] = await Promise.all([ TeamsAPI.readRoles(team.id, params), UsersAPI.readAdminOfOrganizations(me.id, { id: team.organization, }), + TeamsAPI.readRoleOptions(team.id), ]); return { roleCount: count, roles: results, isAdminOfOrg: orgAdminCount > 0, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), }; }, [me.id, team.id, team.organization, search]), { roles: [], roleCount: 0, isAdminOfOrg: false, + actions: {}, + relatedSearchFields: [], } ); @@ -90,6 +98,10 @@ function TeamRolesList({ i18n, me, team }) { ); const canAdd = team?.summary_fields?.user_capabilities?.edit || isAdminOfOrg; + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); const detailUrl = role => { const { resource_id, resource_type } = role.summary_fields; @@ -136,16 +148,18 @@ function TeamRolesList({ i18n, me, team }) { toolbarSearchColumns={[ { name: i18n._(t`Role`), - key: 'role_field', + key: 'role_field__icontains', isDefault: true, }, ]} toolbarSortColumns={[ { - name: i18n._(t`Name`), + name: i18n._(t`ID`), key: 'id', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( ', () => { }, ], }); + + TeamsAPI.readRoleOptions.mockResolvedValue({ + data: { + actions: { GET: {} }, + related_search_fields: [], + }, + }); }); afterEach(() => { diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx index 909db5d29c..31c04263ad 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx @@ -36,7 +36,7 @@ function TemplateList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { results, count, jtActions, wfjtActions }, + result: { results, count, jtActions, wfjtActions, relatedSearchFields }, error: contentError, isLoading, request: fetchTemplates, @@ -53,6 +53,14 @@ function TemplateList({ i18n }) { count: responses[0].data.count, jtActions: responses[1].data.actions, wfjtActions: responses[2].data.actions, + relatedSearchFields: [ + ...(responses[1]?.data?.related_search_fields || []).map(val => + val.slice(0, -8) + ), + ...(responses[2]?.data?.related_search_fields || []).map(val => + val.slice(0, -8) + ), + ], }; }, [location]), { @@ -60,6 +68,7 @@ function TemplateList({ i18n }) { count: 0, jtActions: {}, wfjtActions: {}, + relatedSearchFields: [], } ); @@ -118,6 +127,18 @@ function TemplateList({ i18n }) { jtActions && Object.prototype.hasOwnProperty.call(jtActions, 'POST'); const canAddWFJT = wfjtActions && Object.prototype.hasOwnProperty.call(wfjtActions, 'POST'); + // spreading Set() returns only unique keys + const relatedSearchableKeys = [...new Set(relatedSearchFields)] || []; + const searchableKeys = [ + ...new Set([ + ...Object.keys(jtActions?.GET || {}).filter( + key => jtActions.GET[key].filterable + ), + ...Object.keys(wfjtActions?.GET || {}).filter( + key => wfjtActions.GET[key].filterable + ), + ]), + ]; const addButtonOptions = []; if (canAddJT) { @@ -152,16 +173,16 @@ function TemplateList({ i18n }) { toolbarSearchColumns={[ { name: i18n._(t`Name`), - key: 'name', + key: 'name__icontains', isDefault: true, }, { name: i18n._(t`Description`), - key: 'description', + key: 'description__icontains', }, { name: i18n._(t`Type`), - key: 'type', + key: 'or__type', options: [ [`job_template`, i18n._(t`Job Template`)], [`workflow_job_template`, i18n._(t`Workflow Template`)], @@ -169,15 +190,15 @@ function TemplateList({ i18n }) { }, { name: i18n._(t`Playbook name`), - key: 'job_template__playbook', + key: 'job_template__playbook__icontains', }, { name: i18n._(t`Created By (Username)`), - key: 'created_by__username', + key: 'created_by__username__icontains', }, { name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + key: 'modified_by__username__icontains', }, ]} toolbarSortColumns={[ @@ -206,6 +227,8 @@ function TemplateList({ i18n }) { key: 'type', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( { const params = parseQueryString(QS_CONFIG, search); @@ -46,20 +46,28 @@ function UserAccessList({ i18n, user }) { { data: { results, count }, }, - { - data: { actions }, - }, + actionsResponse, ] = await Promise.all([ UsersAPI.readRoles(user.id, params), UsersAPI.readOptions(), ]); - return { roleCount: count, roles: results, options: actions }; + return { + roleCount: count, + roles: results, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + }; }, [user.id, search]), { roles: [], roleCount: 0, + actions: {}, + relatedSearchFields: [], } ); + useEffect(() => { fetchRoles(); }, [fetchRoles]); @@ -82,7 +90,12 @@ function UserAccessList({ i18n, user }) { const canAdd = user?.summary_fields?.user_capabilities?.edit || - (options && Object.prototype.hasOwnProperty.call(options, 'POST')); + (actions && Object.prototype.hasOwnProperty.call(actions, 'POST')); + + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); const saveRoles = () => { setIsWizardOpen(false); @@ -132,16 +145,18 @@ function UserAccessList({ i18n, user }) { toolbarSearchColumns={[ { name: i18n._(t`Role`), - key: 'role_field', + key: 'role_field__icontains', isDefault: true, }, ]} toolbarSortColumns={[ { - name: i18n._(t`Name`), + name: i18n._(t`ID`), key: 'id', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderItem={role => { return ( ', () => { ); }); test('should not render add button when user cannot create other users and user cannot edit this user', async () => { + UsersAPI.readRoleOptions.mockResolvedValueOnce({ + data: { + actions: { + GET: {}, + }, + related_search_fields: [], + }, + }); + UsersAPI.readRoles.mockResolvedValue({ data: { results: [ diff --git a/awx/ui_next/src/screens/User/UserList/UserList.jsx b/awx/ui_next/src/screens/User/UserList/UserList.jsx index 20f5570aa6..d34706a5d6 100644 --- a/awx/ui_next/src/screens/User/UserList/UserList.jsx +++ b/awx/ui_next/src/screens/User/UserList/UserList.jsx @@ -98,16 +98,16 @@ function UserList({ i18n }) { toolbarSearchColumns={[ { name: i18n._(t`Username`), - key: 'username', + key: 'username__icontains', isDefault: true, }, { - name: i18n._(t`First name`), - key: 'first_name', + name: i18n._(t`First Name`), + key: 'first_name__icontains', }, { - name: i18n._(t`Last name`), - key: 'last_name', + name: i18n._(t`Last Name`), + key: 'last_name__icontains', }, ]} toolbarSortColumns={[ @@ -116,11 +116,11 @@ function UserList({ i18n }) { key: 'username', }, { - name: i18n._(t`First name`), + name: i18n._(t`First Name`), key: 'first_name', }, { - name: i18n._(t`Last name`), + name: i18n._(t`Last Name`), key: 'last_name', }, ]} diff --git a/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx index d7a902b7e3..25d245cffd 100644 --- a/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx +++ b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx @@ -20,24 +20,36 @@ function UserTeamList({ i18n }) { const { id: userId } = useParams(); const { - result: { teams, count }, + result: { teams, count, actions, relatedSearchFields }, error: contentError, isLoading, request: fetchOrgs, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const { - data: { results, count: teamCount }, - } = await UsersAPI.readTeams(userId, params); + const [ + { + data: { results, count: teamCount }, + }, + actionsResponse, + ] = await Promise.all([ + UsersAPI.readTeams(userId, params), + UsersAPI.readTeamsOptions(userId), + ]); return { teams: results, count: teamCount, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), }; }, [userId, location.search]), { teams: [], count: 0, + actions: {}, + relatedSearchFields: [], } ); @@ -45,6 +57,11 @@ function UserTeamList({ i18n }) { fetchOrgs(); }, [fetchOrgs]); + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); + return ( ); } diff --git a/awx/ui_next/src/screens/User/UserTeams/UserTeamList.test.jsx b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.test.jsx index caac6b0c5f..b7fd0a9abf 100644 --- a/awx/ui_next/src/screens/User/UserTeams/UserTeamList.test.jsx +++ b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.test.jsx @@ -58,13 +58,14 @@ describe('', () => { data: mockAPIUserTeamList.data, }) ); - UsersAPI.readOptions = jest.fn(() => + UsersAPI.readTeamsOptions = jest.fn(() => Promise.resolve({ data: { actions: { GET: {}, POST: {}, }, + related_search_fields: [], }, }) ); diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx index 8ae62d85e1..1236cbe04c 100644 --- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx +++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx @@ -28,13 +28,19 @@ function UserTokenList({ i18n }) { error, isLoading, request: fetchTokens, - result: { tokens, itemCount }, + result: { tokens, itemCount, actions, relatedSearchFields }, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const { - data: { results, count }, - } = await UsersAPI.readTokens(id, params); + const [ + { + data: { results, count }, + }, + actionsResponse, + ] = await Promise.all([ + UsersAPI.readTokens(id, params), + UsersAPI.readTokenOptions(id), + ]); const modifiedResults = results.map(result => { result.summary_fields = { user: result.summary_fields.user, @@ -44,9 +50,16 @@ function UserTokenList({ i18n }) { result.name = result.summary_fields.application?.name; return result; }); - return { tokens: modifiedResults, itemCount: count }; + return { + tokens: modifiedResults, + itemCount: count, + actions: actionsResponse.data.actions, + relatedSearchFields: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + }; }, [id, location.search]), - { tokens: [], itemCount: 0 } + { tokens: [], itemCount: 0, actions: {}, relatedSearchFields: [] } ); useEffect(() => { @@ -80,6 +93,10 @@ function UserTokenList({ i18n }) { }; const canAdd = true; + const relatedSearchableKeys = relatedSearchFields || []; + const searchableKeys = Object.keys(actions?.GET || {}).filter( + key => actions.GET[key].filterable + ); return ( <> ', () => { let wrapper; - test('should mount properly, and fetch tokens', async () => { + + beforeEach(() => { UsersAPI.readTokens.mockResolvedValue(tokens); + UsersAPI.readTokenOptions.mockResolvedValue({ + data: { related_search_fields: [] }, + }); + }); + + test('should mount properly, and fetch tokens', async () => { await act(async () => { wrapper = mountWithContexts(); }); @@ -137,7 +144,6 @@ describe('', () => { }); test('edit button should be disabled', async () => { - UsersAPI.readTokens.mockResolvedValue(tokens); await act(async () => { wrapper = mountWithContexts(); });