diff --git a/awx/ui_next/src/api/models/Jobs.js b/awx/ui_next/src/api/models/Jobs.js index 9c43509f9e..fc9bbb2334 100644 --- a/awx/ui_next/src/api/models/Jobs.js +++ b/awx/ui_next/src/api/models/Jobs.js @@ -36,6 +36,10 @@ class Jobs extends RelaunchMixin(Base) { return this.http.post(`/api/v2${getBaseURL(type)}${id}/cancel/`); } + readCredentials(id, type) { + return this.http.get(`/api/v2${getBaseURL(type)}${id}/credentials/`); + } + readDetail(id, type) { return this.http.get(`/api/v2${getBaseURL(type)}${id}/`); } diff --git a/awx/ui_next/src/components/CredentialChip/CredentialChip.jsx b/awx/ui_next/src/components/CredentialChip/CredentialChip.jsx index 11e00ee7ed..7dd5b055b5 100644 --- a/awx/ui_next/src/components/CredentialChip/CredentialChip.jsx +++ b/awx/ui_next/src/components/CredentialChip/CredentialChip.jsx @@ -16,10 +16,17 @@ function CredentialChip({ credential, i18n, i18nHash, ...props }) { type = toTitleCase(credential.kind); } + const buildCredentialName = () => { + if (credential.kind === 'vault' && credential.inputs?.vault_id) { + return `${credential.name} | ${credential.inputs.vault_id}`; + } + return `${credential.name}`; + }; + return ( {type}: - {credential.name} + {buildCredentialName()} ); } diff --git a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx index b31efb35c5..ecc1a268c4 100644 --- a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx @@ -71,6 +71,16 @@ function MultiCredentialsLookup(props) { loadCredentials(params, selectedType.id), CredentialsAPI.readOptions(), ]); + + results.map(result => { + if (result.kind === 'vault' && result.inputs?.vault_id) { + result.label = `${result.name} | ${result.inputs.vault_id}`; + return result; + } + result.label = `${result.name}`; + return result; + }); + return { credentials: results, credentialsCount: count, @@ -108,7 +118,6 @@ function MultiCredentialsLookup(props) { credential={item} /> ); - const isVault = selectedType?.kind === 'vault'; return ( @@ -187,6 +196,7 @@ function MultiCredentialsLookup(props) { relatedSearchableKeys={relatedSearchableKeys} multiple={isVault} header={i18n._(t`Credentials`)} + displayKey={isVault ? 'label' : 'name'} name="credentials" qsConfig={QS_CONFIG} readOnly={!canDelete} diff --git a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx index d0a3738171..a020d56345 100644 --- a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx +++ b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx @@ -87,6 +87,23 @@ describe('', () => { name: 'Cred 5', url: 'www.google.com', }, + + { + id: 6, + credential_type: 5, + kind: 'vault', + name: 'Cred 6', + url: 'www.google.com', + inputs: { vault_id: 'vault ID' }, + }, + { + id: 7, + credential_type: 5, + kind: 'vault', + name: 'Cred 7', + url: 'www.google.com', + inputs: {}, + }, ], count: 3, }, @@ -196,7 +213,13 @@ describe('', () => { wrapper.update(); expect(CredentialsAPI.read).toHaveBeenCalledTimes(2); expect(wrapper.find('OptionsList').prop('options')).toEqual([ - { id: 1, kind: 'cloud', name: 'New Cred', url: 'www.google.com' }, + { + id: 1, + kind: 'cloud', + name: 'New Cred', + url: 'www.google.com', + label: 'New Cred', + }, ]); }); @@ -268,6 +291,36 @@ describe('', () => { ]); }); + test('should properly render vault credential labels', async () => { + await act(async () => { + wrapper = mountWithContexts( + {}} + onError={() => {}} + /> + ); + }); + const searchButton = await waitForElement( + wrapper, + 'Button[aria-label="Search"]' + ); + await act(async () => { + searchButton.invoke('onClick')(); + }); + wrapper.update(); + const typeSelect = wrapper.find('AnsibleSelect'); + act(() => { + typeSelect.invoke('onChange')({}, 500); + }); + wrapper.update(); + const optionsList = wrapper.find('OptionsList'); + expect(optionsList.prop('multiple')).toEqual(true); + expect(wrapper.find('CheckboxListItem[label="Cred 6 | vault ID"]')); + expect(wrapper.find('CheckboxListItem[label="Cred 7"]')); + }); + test('should allow multiple vault credentials with no vault id', async () => { const onChange = jest.fn(); await act(async () => { diff --git a/awx/ui_next/src/screens/Job/Job.jsx b/awx/ui_next/src/screens/Job/Job.jsx index ce62e7338c..479eeb6e49 100644 --- a/awx/ui_next/src/screens/Job/Job.jsx +++ b/awx/ui_next/src/screens/Job/Job.jsx @@ -29,10 +29,18 @@ function Job({ i18n, setBreadcrumb }) { const { isLoading, error, request: fetchJob, result } = useRequest( useCallback(async () => { const { data } = await JobsAPI.readDetail(id, type); + if ( + data?.summary_fields?.credentials?.find(cred => cred.kind === 'vault') + ) { + const { + data: { results }, + } = await JobsAPI.readCredentials(data.id, type); + + data.summary_fields.credentials = results; + } setBreadcrumb(data); return data; - }, [id, type, setBreadcrumb]), - null + }, [id, type, setBreadcrumb]) ); useEffect(() => { diff --git a/awx/ui_next/src/screens/Project/Project.jsx b/awx/ui_next/src/screens/Project/Project.jsx index f87aee15dc..72341a5de9 100644 --- a/awx/ui_next/src/screens/Project/Project.jsx +++ b/awx/ui_next/src/screens/Project/Project.jsx @@ -44,6 +44,19 @@ function Project({ i18n, setBreadcrumb }) { role_level: 'notification_admin_role', }), ]); + + if (data.summary_fields.credentials) { + const params = { + page: 1, + page_size: 200, + order_by: 'name', + }; + const { + data: { results }, + } = await ProjectsAPI.readCredentials(data.id, params); + + data.summary_fields.credentials = results; + } return { project: data, isNotifAdmin: notifAdminRes.data.results.length > 0, diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx index 182c4918f1..5fbab04fa3 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx @@ -1,13 +1,11 @@ /* eslint react/no-unused-state: 0 */ import React, { useState } from 'react'; -import { withRouter, Redirect, useHistory } from 'react-router-dom'; +import { Redirect, useHistory } from 'react-router-dom'; import { CardBody } from '../../../components/Card'; - import { JobTemplatesAPI } from '../../../api'; import { JobTemplate } from '../../../types'; import { getAddedAndRemoved } from '../../../util/lists'; import JobTemplateForm from '../shared/JobTemplateForm'; - import ContentLoading from '../../../components/ContentLoading'; function JobTemplateEdit({ template }) { @@ -95,9 +93,7 @@ function JobTemplateEdit({ template }) { return Promise.all([disassociatePromise, associatePromise]); }; - const handleCancel = () => { - history.push(detailsUrl); - }; + const handleCancel = () => history.push(detailsUrl); const canEdit = template?.summary_fields?.user_capabilities?.edit; @@ -122,5 +118,4 @@ function JobTemplateEdit({ template }) { JobTemplateEdit.propTypes = { template: JobTemplate.isRequired, }; - -export default withRouter(JobTemplateEdit); +export default JobTemplateEdit; diff --git a/awx/ui_next/src/screens/Template/Template.jsx b/awx/ui_next/src/screens/Template/Template.jsx index 0d9f84288d..b6ff53db16 100644 --- a/awx/ui_next/src/screens/Template/Template.jsx +++ b/awx/ui_next/src/screens/Template/Template.jsx @@ -46,8 +46,21 @@ function Template({ i18n, setBreadcrumb }) { role_level: 'notification_admin_role', }), ]); - if (actions?.data?.actions?.PUT) { - if (data?.webhook_service && data?.related?.webhook_key) { + if (data.summary_fields.credentials) { + const params = { + page: 1, + page_size: 200, + order_by: 'name', + }; + const { + data: { results }, + } = await JobTemplatesAPI.readCredentials(data.id, params); + + data.summary_fields.credentials = results; + } + + if (actions.data.actions.PUT) { + if (data.webhook_service && data?.related?.webhook_key) { const { data: { webhook_key }, } = await JobTemplatesAPI.readWebhookKey(templateId); @@ -142,7 +155,7 @@ function Template({ i18n, setBreadcrumb }) { - {contentError.response.status === 404 && ( + {contentError.response?.status === 404 && ( {i18n._(t`Template not found.`)}{' '} {i18n._(t`View all Templates.`)} diff --git a/awx/ui_next/src/screens/Template/Template.test.jsx b/awx/ui_next/src/screens/Template/Template.test.jsx index a38209db75..afb7221f06 100644 --- a/awx/ui_next/src/screens/Template/Template.test.jsx +++ b/awx/ui_next/src/screens/Template/Template.test.jsx @@ -28,6 +28,22 @@ describe('