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('', () => {
actions: { PUT: true },
},
});
+ JobTemplatesAPI.readCredentials.mockResolvedValue({
+ data: {
+ results: [
+ {
+ id: 3,
+ type: 'credential',
+ url: '/api/v2/credentials/3/',
+ name: 'Vault1Id1',
+ inputs: {
+ vault_id: '1',
+ },
+ kind: 'vault',
+ },
+ ],
+ },
+ });
OrganizationsAPI.read.mockResolvedValue({
data: {
count: 1,