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(); });