diff --git a/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js b/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js
index f13208e4f0..69ed14eb93 100644
--- a/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js
+++ b/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js
@@ -34,8 +34,14 @@ const QS_CONFIG = getQSConfig('template', {
order_by: 'name',
});
-function RelatedTemplateList({ searchParams, projectName = null }) {
- const { id: projectId } = useParams();
+const resources = {
+ projects: 'project',
+ inventories: 'inventory',
+ credentials: 'credentials',
+};
+
+function RelatedTemplateList({ searchParams, resourceName = null }) {
+ const { id } = useParams();
const location = useLocation();
const { addToast, Toast, toastProps } = useToast();
@@ -129,12 +135,19 @@ function RelatedTemplateList({ searchParams, projectName = null }) {
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
let linkTo = '';
-
- if (projectName) {
- const qs = encodeQueryString({
- project_id: projectId,
- project_name: projectName,
- });
+ if (resourceName) {
+ const queryString = {
+ resource_id: id,
+ resource_name: resourceName,
+ resource_type: resources[location.pathname.split('/')[1]],
+ resource_kind: null,
+ };
+ if (Array.isArray(resourceName)) {
+ const [name, kind] = resourceName;
+ queryString.resource_name = name;
+ queryString.resource_kind = kind;
+ }
+ const qs = encodeQueryString(queryString);
linkTo = `/templates/job_template/add/?${qs}`;
} else {
linkTo = '/templates/job_template/add';
diff --git a/awx/ui/src/components/RelatedTemplateList/relatedTemplateHelpers.js b/awx/ui/src/components/RelatedTemplateList/relatedTemplateHelpers.js
new file mode 100644
index 0000000000..95ecb0ce85
--- /dev/null
+++ b/awx/ui/src/components/RelatedTemplateList/relatedTemplateHelpers.js
@@ -0,0 +1 @@
+/* eslint-disable import/prefer-default-export */
diff --git a/awx/ui/src/screens/Credential/Credential.js b/awx/ui/src/screens/Credential/Credential.js
index 45c507e23e..ce0509146d 100644
--- a/awx/ui/src/screens/Credential/Credential.js
+++ b/awx/ui/src/screens/Credential/Credential.js
@@ -22,6 +22,16 @@ import { CredentialsAPI } from 'api';
import CredentialDetail from './CredentialDetail';
import CredentialEdit from './CredentialEdit';
+const jobTemplateCredentialTypes = [
+ 'machine',
+ 'cloud',
+ 'net',
+ 'ssh',
+ 'vault',
+ 'kubernetes',
+ 'cryptography',
+];
+
function Credential({ setBreadcrumb }) {
const { pathname } = useLocation();
@@ -75,13 +85,14 @@ function Credential({ setBreadcrumb }) {
link: `/credentials/${id}/access`,
id: 1,
},
- {
+ ];
+ if (jobTemplateCredentialTypes.includes(credential?.kind)) {
+ tabsArray.push({
name: t`Job Templates`,
link: `/credentials/${id}/job_templates`,
id: 2,
- },
- ];
-
+ });
+ }
let showCardHeader = true;
if (pathname.endsWith('edit') || pathname.endsWith('add')) {
@@ -133,6 +144,7 @@ function Credential({ setBreadcrumb }) {
,
diff --git a/awx/ui/src/screens/Credential/Credential.test.js b/awx/ui/src/screens/Credential/Credential.test.js
index b66619c877..2df3162205 100644
--- a/awx/ui/src/screens/Credential/Credential.test.js
+++ b/awx/ui/src/screens/Credential/Credential.test.js
@@ -6,7 +6,8 @@ import {
mountWithContexts,
waitForElement,
} from '../../../testUtils/enzymeHelpers';
-import mockCredential from './shared/data.scmCredential.json';
+import mockMachineCredential from './shared/data.machineCredential.json';
+import mockSCMCredential from './shared/data.scmCredential.json';
import Credential from './Credential';
jest.mock('../../api');
@@ -21,13 +22,10 @@ jest.mock('react-router-dom', () => ({
describe('', () => {
let wrapper;
- beforeEach(() => {
+ test('initially renders user-based machine credential successfully', async () => {
CredentialsAPI.readDetail.mockResolvedValueOnce({
- data: mockCredential,
+ data: mockMachineCredential,
});
- });
-
- test('initially renders user-based credential successfully', async () => {
await act(async () => {
wrapper = mountWithContexts( {}} />);
});
@@ -36,6 +34,18 @@ describe('', () => {
expect(wrapper.find('RoutedTabs li').length).toBe(4);
});
+ test('initially renders user-based SCM credential successfully', async () => {
+ CredentialsAPI.readDetail.mockResolvedValueOnce({
+ data: mockSCMCredential,
+ });
+ await act(async () => {
+ wrapper = mountWithContexts( {}} />);
+ });
+ wrapper.update();
+ expect(wrapper.find('Credential').length).toBe(1);
+ expect(wrapper.find('RoutedTabs li').length).toBe(3);
+ });
+
test('should render expected tabs', async () => {
const expectedTabs = [
'Back to Credentials',
diff --git a/awx/ui/src/screens/Inventory/Inventory.js b/awx/ui/src/screens/Inventory/Inventory.js
index d26c0fc5ce..53da122cd6 100644
--- a/awx/ui/src/screens/Inventory/Inventory.js
+++ b/awx/ui/src/screens/Inventory/Inventory.js
@@ -181,6 +181,7 @@ function Inventory({ setBreadcrumb }) {
>
,
diff --git a/awx/ui/src/screens/Project/Project.js b/awx/ui/src/screens/Project/Project.js
index f3500d3eda..6ec4bd9558 100644
--- a/awx/ui/src/screens/Project/Project.js
+++ b/awx/ui/src/screens/Project/Project.js
@@ -179,7 +179,7 @@ function Project({ setBreadcrumb }) {
searchParams={{
project__id: project.id,
}}
- projectName={project.name}
+ resourceName={project.name}
/>
{project?.scm_type && project.scm_type !== '' && (
diff --git a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.js b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.js
index f4d0f4f49a..ebb171bb1e 100644
--- a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.js
+++ b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.js
@@ -9,29 +9,31 @@ function JobTemplateAdd() {
const [formSubmitError, setFormSubmitError] = useState(null);
const history = useHistory();
- const projectParams = {
- project_id: null,
- project_name: null,
+ const resourceParams = {
+ resource_id: null,
+ resource_name: null,
+ resource_type: null,
+ resource_kind: null,
};
history.location.search
.replace(/^\?/, '')
.split('&')
.map((s) => s.split('='))
.forEach(([key, val]) => {
- if (!(key in projectParams)) {
+ if (!(key in resourceParams)) {
return;
}
- projectParams[key] = decodeURIComponent(val);
+ resourceParams[key] = decodeURIComponent(val);
});
- let projectValues = null;
+ let resourceValues = null;
- if (
- Object.values(projectParams).filter((item) => item !== null).length === 2
- ) {
- projectValues = {
- id: projectParams.project_id,
- name: projectParams.project_name,
+ if (history.location.search.includes('resource_id' && 'resource_name')) {
+ resourceValues = {
+ id: resourceParams.resource_id,
+ name: resourceParams.resource_name,
+ type: resourceParams.resource_type,
+ kind: resourceParams.resource_kind, // refers to credential kind
};
}
@@ -122,7 +124,7 @@ function JobTemplateAdd() {
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={formSubmitError}
- projectValues={projectValues}
+ resourceValues={resourceValues}
isOverrideDisabledLookup
/>
diff --git a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js
index 3fd63394dc..e50b91d8c8 100644
--- a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js
+++ b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js
@@ -274,9 +274,14 @@ describe('', () => {
test('should parse and pre-fill project field from query params', async () => {
const history = createMemoryHistory({
initialEntries: [
- '/templates/job_template/add/add?project_id=6&project_name=Demo%20Project',
+ '/templates/job_template/add?resource_id=6&resource_name=Demo%20Project&resource_type=project',
],
});
+ ProjectsAPI.read.mockResolvedValueOnce({
+ count: 1,
+ results: [{ name: 'foo', id: 1, allow_override: true, organization: 1 }],
+ });
+ ProjectsAPI.readOptions.mockResolvedValueOnce({});
let wrapper;
await act(async () => {
wrapper = mountWithContexts(, {
@@ -284,8 +289,9 @@ describe('', () => {
});
});
await waitForElement(wrapper, 'EmptyStateBody', (el) => el.length === 0);
+
expect(wrapper.find('input#project').prop('value')).toEqual('Demo Project');
- expect(ProjectsAPI.readPlaybooks).toBeCalledWith('6');
+ expect(ProjectsAPI.readPlaybooks).toBeCalledWith(6);
});
test('should not call ProjectsAPI.readPlaybooks if there is no project', async () => {
diff --git a/awx/ui/src/screens/Template/shared/JobTemplateForm.js b/awx/ui/src/screens/Template/shared/JobTemplateForm.js
index 7621601e9e..dcdfd0d956 100644
--- a/awx/ui/src/screens/Template/shared/JobTemplateForm.js
+++ b/awx/ui/src/screens/Template/shared/JobTemplateForm.js
@@ -690,7 +690,7 @@ JobTemplateForm.defaultProps = {
};
const FormikApp = withFormik({
- mapPropsToValues({ projectValues = {}, template = {} }) {
+ mapPropsToValues({ resourceValues = null, template = {} }) {
const {
summary_fields = {
labels: { results: [] },
@@ -698,7 +698,7 @@ const FormikApp = withFormik({
},
} = template;
- return {
+ const initialValues = {
allow_callbacks: template.allow_callbacks || false,
allow_simultaneous: template.allow_simultaneous || false,
ask_credential_on_launch: template.ask_credential_on_launch || false,
@@ -739,7 +739,7 @@ const FormikApp = withFormik({
playbook: template.playbook || '',
prevent_instance_group_fallback:
template.prevent_instance_group_fallback || false,
- project: summary_fields?.project || projectValues || null,
+ project: summary_fields?.project || null,
scm_branch: template.scm_branch || '',
skip_tags: template.skip_tags || '',
timeout: template.timeout || 0,
@@ -756,6 +756,24 @@ const FormikApp = withFormik({
execution_environment:
template.summary_fields?.execution_environment || null,
};
+ if (resourceValues !== null) {
+ if (resourceValues.type === 'credentials') {
+ initialValues[resourceValues.type] = [
+ {
+ id: parseInt(resourceValues.id, 10),
+ name: resourceValues.name,
+ kind: resourceValues.kind,
+ },
+ ];
+ } else {
+ initialValues[resourceValues.type] = {
+ id: parseInt(resourceValues.id, 10),
+ name: resourceValues.name,
+ };
+ }
+ }
+
+ return initialValues;
},
handleSubmit: async (values, { props, setErrors }) => {
try {