diff --git a/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx
index ab68ca4b28..7c0d555485 100644
--- a/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx
+++ b/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx
@@ -1,8 +1,187 @@
import React from 'react';
-import { CardBody } from '@components/Card';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { Link } from 'react-router-dom';
-function PromptJobTemplateDetail() {
- return Coming soon :);
+import { Chip, ChipGroup, List, ListItem } from '@patternfly/react-core';
+import { Detail } from '@components/DetailList';
+import { VariablesDetail } from '@components/CodeMirrorInput';
+import CredentialChip from '@components/CredentialChip';
+
+function PromptJobTemplateDetail({ i18n, resource }) {
+ const {
+ allow_simultaneous,
+ become_enabled,
+ diff_mode,
+ extra_vars,
+ forks,
+ host_config_key,
+ instance_groups,
+ job_slice_count,
+ job_tags,
+ job_type,
+ limit,
+ playbook,
+ scm_branch,
+ skip_tags,
+ summary_fields,
+ url,
+ use_fact_cache,
+ verbosity,
+ } = resource;
+
+ const VERBOSITY = {
+ 0: i18n._(t`0 (Normal)`),
+ 1: i18n._(t`1 (Verbose)`),
+ 2: i18n._(t`2 (More Verbose)`),
+ 3: i18n._(t`3 (Debug)`),
+ 4: i18n._(t`4 (Connection Debug)`),
+ };
+
+ let optionsList = '';
+ if (
+ become_enabled ||
+ host_config_key ||
+ allow_simultaneous ||
+ use_fact_cache
+ ) {
+ optionsList = (
+
+ {become_enabled && (
+ {i18n._(t`Enable Privilege Escalation`)}
+ )}
+ {host_config_key && (
+ {i18n._(t`Allow Provisioning Callbacks`)}
+ )}
+ {allow_simultaneous && (
+ {i18n._(t`Enable Concurrent Jobs`)}
+ )}
+ {use_fact_cache && {i18n._(t`Use Fact Storage`)}}
+
+ );
+ }
+
+ return (
+ <>
+
+ {summary_fields?.inventory && (
+
+ {summary_fields.inventory?.name}
+
+ }
+ />
+ )}
+ {summary_fields?.project && (
+
+ {summary_fields.project?.name}
+
+ }
+ />
+ )}
+
+
+
+
+
+
+
+ {host_config_key && (
+
+
+
+
+ )}
+ {summary_fields?.credentials?.length > 0 && (
+ (
+
+ ))}
+ />
+ )}
+ {summary_fields?.labels?.results?.length > 0 && (
+
+ {summary_fields.labels.results.map(label => (
+
+ {label.name}
+
+ ))}
+
+ }
+ />
+ )}
+ {instance_groups?.length > 0 && (
+
+ {instance_groups.map(ig => (
+
+ {ig.name}
+
+ ))}
+
+ }
+ />
+ )}
+ {job_tags?.length > 0 && (
+
+ {job_tags.split(',').map(jobTag => (
+
+ {jobTag}
+
+ ))}
+
+ }
+ />
+ )}
+ {skip_tags?.length > 0 && (
+
+ {skip_tags.split(',').map(skipTag => (
+
+ {skipTag}
+
+ ))}
+
+ }
+ />
+ )}
+ {optionsList && }
+ {extra_vars && (
+
+ )}
+ >
+ );
}
-export default PromptJobTemplateDetail;
+export default withI18n()(PromptJobTemplateDetail);
diff --git a/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.test.jsx b/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.test.jsx
new file mode 100644
index 0000000000..f4129bae38
--- /dev/null
+++ b/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.test.jsx
@@ -0,0 +1,99 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import PromptJobTemplateDetail from './PromptJobTemplateDetail';
+import mockData from './data.job_template.json';
+
+const mockJT = {
+ ...mockData,
+ instance_groups: [
+ {
+ id: 1,
+ name: 'ig1',
+ },
+ {
+ id: 2,
+ name: 'ig2',
+ },
+ ],
+};
+
+describe('PromptJobTemplateDetail', () => {
+ let wrapper;
+
+ beforeAll(() => {
+ wrapper = mountWithContexts();
+ });
+
+ afterAll(() => {
+ wrapper.unmount();
+ });
+
+ test('should render successfully', () => {
+ expect(wrapper.find('PromptJobTemplateDetail')).toHaveLength(1);
+ });
+
+ test('should render expected details', () => {
+ function assertDetail(label, value) {
+ expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
+ expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
+ }
+
+ assertDetail('Job Type', 'run');
+ assertDetail('Inventory', 'Demo Inventory');
+ assertDetail('Project', 'Mock Project');
+ assertDetail('SCM Branch', 'Foo branch');
+ assertDetail('Playbook', 'ping.yml');
+ assertDetail('Forks', '2');
+ assertDetail('Limit', 'alpha:beta');
+ assertDetail('Verbosity', '3 (Debug)');
+ assertDetail('Show Changes', 'Off');
+ assertDetail('Job Slicing', '1');
+ assertDetail('Host Config Key', 'a1b2c3');
+ expect(
+ wrapper.find('Detail[label="Provisioning Callback URL"] dd').text()
+ ).toEqual(expect.stringContaining('/api/v2/job_templates/7/callback/'));
+ expect(
+ wrapper.find('Detail[label="Credentials"]').containsAllMatchingElements([
+
+ SSH:Credential 1
+ ,
+
+ Awx:Credential 2
+ ,
+ ])
+ ).toEqual(true);
+ expect(
+ wrapper
+ .find('Detail[label="Labels"]')
+ .containsAllMatchingElements([L_91o2, L_91o3])
+ ).toEqual(true);
+ expect(
+ wrapper
+ .find('Detail[label="Instance Groups"]')
+ .containsAllMatchingElements([ig1, ig2])
+ ).toEqual(true);
+ expect(
+ wrapper
+ .find('Detail[label="Job Tags"]')
+ .containsAllMatchingElements([T_100, T_200])
+ ).toEqual(true);
+ expect(
+ wrapper
+ .find('Detail[label="Skip Tags"]')
+ .containsAllMatchingElements([S_100, S_200])
+ ).toEqual(true);
+ expect(
+ wrapper
+ .find('Detail[label="Options"]')
+ .containsAllMatchingElements([
+ Enable Privilege Escalation,
+ Allow Provisioning Callbacks,
+ Enable Concurrent Jobs,
+ Use Fact Storage,
+ ])
+ ).toEqual(true);
+ expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
+ '---foo: bar'
+ );
+ });
+});
diff --git a/awx/ui_next/src/components/PromptDetail/data.job_template.json b/awx/ui_next/src/components/PromptDetail/data.job_template.json
new file mode 100644
index 0000000000..34dfc47154
--- /dev/null
+++ b/awx/ui_next/src/components/PromptDetail/data.job_template.json
@@ -0,0 +1,178 @@
+{
+ "id": 7,
+ "type": "job_template",
+ "url": "/api/v2/job_templates/7/",
+ "related": {
+ "named_url": "/api/v2/job_templates/MockJT/",
+ "created_by": "/api/v2/users/1/",
+ "modified_by": "/api/v2/users/1/",
+ "labels": "/api/v2/job_templates/7/labels/",
+ "inventory": "/api/v2/inventories/1/",
+ "project": "/api/v2/projects/6/",
+ "extra_credentials": "/api/v2/job_templates/7/extra_credentials/",
+ "credentials": "/api/v2/job_templates/7/credentials/",
+ "last_job": "/api/v2/jobs/12/",
+ "jobs": "/api/v2/job_templates/7/jobs/",
+ "schedules": "/api/v2/job_templates/7/schedules/",
+ "activity_stream": "/api/v2/job_templates/7/activity_stream/",
+ "launch": "/api/v2/job_templates/7/launch/",
+ "notification_templates_started": "/api/v2/job_templates/7/notification_templates_started/",
+ "notification_templates_success": "/api/v2/job_templates/7/notification_templates_success/",
+ "notification_templates_error": "/api/v2/job_templates/7/notification_templates_error/",
+ "access_list": "/api/v2/job_templates/7/access_list/",
+ "survey_spec": "/api/v2/job_templates/7/survey_spec/",
+ "object_roles": "/api/v2/job_templates/7/object_roles/",
+ "instance_groups": "/api/v2/job_templates/7/instance_groups/",
+ "slice_workflow_jobs": "/api/v2/job_templates/7/slice_workflow_jobs/",
+ "copy": "/api/v2/job_templates/7/copy/"
+ },
+ "summary_fields": {
+ "inventory": {
+ "id": 1,
+ "name": "Demo Inventory",
+ "description": "",
+ "has_active_failures": false,
+ "total_hosts": 1,
+ "hosts_with_active_failures": 0,
+ "total_groups": 0,
+ "groups_with_active_failures": 0,
+ "has_inventory_sources": false,
+ "total_inventory_sources": 0,
+ "inventory_sources_with_failures": 0,
+ "organization_id": 1,
+ "kind": ""
+ },
+ "project": {
+ "id": 6,
+ "name": "Mock Project",
+ "description": "",
+ "status": "successful",
+ "scm_type": "git"
+ },
+ "last_job": {
+ "id": 12,
+ "name": "Mock JT",
+ "description": "",
+ "finished": "2019-10-01T14:34:35.142483Z",
+ "status": "successful",
+ "failed": false
+ },
+ "last_update": {
+ "id": 12,
+ "name": "Mock JT",
+ "description": "",
+ "status": "successful",
+ "failed": false
+ },
+ "created_by": {
+ "id": 1,
+ "username": "admin",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 1,
+ "username": "admin",
+ "first_name": "",
+ "last_name": ""
+ },
+ "object_roles": {
+ "admin_role": {
+ "description": "Can manage all aspects of the job template",
+ "name": "Admin",
+ "id": 24
+ },
+ "execute_role": {
+ "description": "May run the job template",
+ "name": "Execute",
+ "id": 25
+ },
+ "read_role": {
+ "description": "May view settings for the job template",
+ "name": "Read",
+ "id": 26
+ }
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true,
+ "start": true,
+ "schedule": true,
+ "copy": true
+ },
+ "labels": {
+ "count": 1,
+ "results": [
+ {
+ "id": 91,
+ "name": "L_91o2"
+ },
+ {
+ "id": 92,
+ "name": "L_91o3"
+ }
+ ]
+ },
+ "survey": {
+ "title": "",
+ "description": ""
+ },
+ "recent_jobs": [
+ {
+ "id": 12,
+ "status": "successful",
+ "finished": "2019-10-01T14:34:35.142483Z",
+ "type": "job"
+ }
+ ],
+ "extra_credentials": [],
+ "credentials": [
+ {
+ "id": 1, "kind": "ssh" , "name": "Credential 1"
+ },
+ {
+ "id": 2, "kind": "awx" , "name": "Credential 2"
+ }
+ ]
+ },
+ "created": "2019-09-30T16:18:34.564820Z",
+ "modified": "2019-10-01T14:47:31.818431Z",
+ "name": "Mock JT",
+ "description": "Mock JT Description",
+ "job_type": "run",
+ "inventory": 1,
+ "project": 6,
+ "playbook": "ping.yml",
+ "scm_branch": "Foo branch",
+ "forks": 2,
+ "limit": "alpha:beta",
+ "verbosity": 3,
+ "extra_vars": "---foo: bar",
+ "job_tags": "T_100,T_200",
+ "force_handlers": false,
+ "skip_tags": "S_100,S_200",
+ "start_at_task": "",
+ "timeout": 0,
+ "use_fact_cache": true,
+ "last_job_run": "2019-10-01T14:34:35.142483Z",
+ "last_job_failed": false,
+ "next_job_run": null,
+ "status": "successful",
+ "host_config_key": "a1b2c3",
+ "ask_scm_branch_on_launch": false,
+ "ask_diff_mode_on_launch": false,
+ "ask_variables_on_launch": false,
+ "ask_limit_on_launch": false,
+ "ask_tags_on_launch": false,
+ "ask_skip_tags_on_launch": false,
+ "ask_job_type_on_launch": false,
+ "ask_verbosity_on_launch": false,
+ "ask_inventory_on_launch": false,
+ "ask_credential_on_launch": false,
+ "survey_enabled": true,
+ "become_enabled": true,
+ "diff_mode": false,
+ "allow_simultaneous": true,
+ "custom_virtualenv": null,
+ "job_slice_count": 1
+}
\ No newline at end of file
diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
index baa6407bc6..cc8ff2e861 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
@@ -294,7 +294,7 @@ function JobTemplateDetail({ i18n, template }) {
{job_tags && job_tags.length > 0 && (
{job_tags.split(',').map(jobTag => (
@@ -309,7 +309,7 @@ function JobTemplateDetail({ i18n, template }) {
{skip_tags && skip_tags.length > 0 && (
{skip_tags.split(',').map(skipTag => (
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.jsx
index 7ca5f4cf63..e550de8ae4 100644
--- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.jsx
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.jsx
@@ -71,7 +71,14 @@ function NodeViewModal({ i18n }) {
request: fetchNodeDetail,
} = useRequest(
useCallback(async () => {
- const { data } = await nodeAPI?.readDetail(unifiedJobTemplate.id);
+ let { data } = await nodeAPI?.readDetail(unifiedJobTemplate.id);
+ if (data?.type === 'job_template') {
+ const {
+ data: { results = [] },
+ } = await JobTemplatesAPI.readInstanceGroups(data.id);
+ data = Object.assign(data, { instance_groups: results });
+ }
+
return data;
}, [nodeAPI, unifiedJobTemplate.id]),
null
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.test.jsx
index d4f22b14de..6edcfbbd66 100644
--- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.test.jsx
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.test.jsx
@@ -13,6 +13,13 @@ jest.mock('@api/models/WorkflowJobTemplates');
WorkflowJobTemplatesAPI.readLaunch.mockResolvedValue({});
WorkflowJobTemplatesAPI.readDetail.mockResolvedValue({});
JobTemplatesAPI.readLaunch.mockResolvedValue({});
+JobTemplatesAPI.readInstanceGroups.mockResolvedValue({});
+JobTemplatesAPI.readDetail.mockResolvedValue({
+ data: {
+ id: 1,
+ type: 'job_template',
+ },
+});
const dispatch = jest.fn();
@@ -64,6 +71,8 @@ describe('NodeViewModal', () => {
test('should fetch workflow template launch data', () => {
expect(JobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
+ expect(JobTemplatesAPI.readDetail).not.toHaveBeenCalled();
+ expect(JobTemplatesAPI.readInstanceGroups).not.toHaveBeenCalled();
expect(WorkflowJobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
});
@@ -95,7 +104,7 @@ describe('NodeViewModal', () => {
id: 1,
name: 'Mock Node',
description: '',
- type: 'job_template',
+ unified_job_type: 'job',
created: '2019-08-08T19:24:05.344276Z',
modified: '2019-08-08T19:24:18.162949Z',
},
@@ -104,6 +113,7 @@ describe('NodeViewModal', () => {
test('should fetch job template launch data', async () => {
let wrapper;
+
await act(async () => {
wrapper = mountWithContexts(
@@ -116,6 +126,8 @@ describe('NodeViewModal', () => {
waitForLoaded(wrapper);
expect(WorkflowJobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
expect(JobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
+ expect(JobTemplatesAPI.readDetail).toHaveBeenCalledWith(1);
+ expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
wrapper.unmount();
jest.clearAllMocks();
});
@@ -167,6 +179,7 @@ describe('NodeViewModal', () => {
waitForLoaded(wrapper);
expect(WorkflowJobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
expect(JobTemplatesAPI.readLaunch).not.toHaveBeenCalled();
+ expect(JobTemplatesAPI.readInstanceGroups).not.toHaveBeenCalled();
wrapper.unmount();
jest.clearAllMocks();
});