Add JT details to wf node modal

This commit is contained in:
Marliana Lara 2020-04-08 15:45:50 -04:00
parent dbe949a2c2
commit 98e8a09ad3
No known key found for this signature in database
GPG Key ID: 38C73B40DFA809EE
6 changed files with 484 additions and 8 deletions

View File

@ -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 <CardBody>Coming soon :)</CardBody>;
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 = (
<List>
{become_enabled && (
<ListItem>{i18n._(t`Enable Privilege Escalation`)}</ListItem>
)}
{host_config_key && (
<ListItem>{i18n._(t`Allow Provisioning Callbacks`)}</ListItem>
)}
{allow_simultaneous && (
<ListItem>{i18n._(t`Enable Concurrent Jobs`)}</ListItem>
)}
{use_fact_cache && <ListItem>{i18n._(t`Use Fact Storage`)}</ListItem>}
</List>
);
}
return (
<>
<Detail label={i18n._(t`Job Type`)} value={job_type} />
{summary_fields?.inventory && (
<Detail
label={i18n._(t`Inventory`)}
value={
<Link to={`/inventories/${summary_fields.inventory?.id}/details`}>
{summary_fields.inventory?.name}
</Link>
}
/>
)}
{summary_fields?.project && (
<Detail
label={i18n._(t`Project`)}
value={
<Link to={`/projects/${summary_fields.project?.id}/details`}>
{summary_fields.project?.name}
</Link>
}
/>
)}
<Detail label={i18n._(t`SCM Branch`)} value={scm_branch} />
<Detail label={i18n._(t`Playbook`)} value={playbook} />
<Detail label={i18n._(t`Forks`)} value={forks || '0'} />
<Detail label={i18n._(t`Limit`)} value={limit} />
<Detail label={i18n._(t`Verbosity`)} value={VERBOSITY[verbosity]} />
<Detail
label={i18n._(t`Show Changes`)}
value={diff_mode ? 'On' : 'Off'}
/>
<Detail label={i18n._(t` Job Slicing`)} value={job_slice_count} />
{host_config_key && (
<React.Fragment>
<Detail label={i18n._(t`Host Config Key`)} value={host_config_key} />
<Detail
label={i18n._(t`Provisioning Callback URL`)}
value={`${window.location.origin + url}callback/`}
/>
</React.Fragment>
)}
{summary_fields?.credentials?.length > 0 && (
<Detail
fullWidth
label={i18n._(t`Credentials`)}
value={summary_fields.credentials.map(chip => (
<CredentialChip key={chip.id} credential={chip} isReadOnly />
))}
/>
)}
{summary_fields?.labels?.results?.length > 0 && (
<Detail
fullWidth
label={i18n._(t`Labels`)}
value={
<ChipGroup numChips={5}>
{summary_fields.labels.results.map(label => (
<Chip key={label.id} isReadOnly>
{label.name}
</Chip>
))}
</ChipGroup>
}
/>
)}
{instance_groups?.length > 0 && (
<Detail
fullWidth
label={i18n._(t`Instance Groups`)}
value={
<ChipGroup numChips={5}>
{instance_groups.map(ig => (
<Chip key={ig.id} isReadOnly>
{ig.name}
</Chip>
))}
</ChipGroup>
}
/>
)}
{job_tags?.length > 0 && (
<Detail
fullWidth
label={i18n._(t`Job Tags`)}
value={
<ChipGroup numChips={5}>
{job_tags.split(',').map(jobTag => (
<Chip key={jobTag} isReadOnly>
{jobTag}
</Chip>
))}
</ChipGroup>
}
/>
)}
{skip_tags?.length > 0 && (
<Detail
fullWidth
label={i18n._(t`Skip Tags`)}
value={
<ChipGroup numChips={5}>
{skip_tags.split(',').map(skipTag => (
<Chip key={skipTag} isReadOnly>
{skipTag}
</Chip>
))}
</ChipGroup>
}
/>
)}
{optionsList && <Detail label={i18n._(t`Options`)} value={optionsList} />}
{extra_vars && (
<VariablesDetail
label={i18n._(t`Variables`)}
rows={4}
value={extra_vars}
/>
)}
</>
);
}
export default PromptJobTemplateDetail;
export default withI18n()(PromptJobTemplateDetail);

View File

@ -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(<PromptJobTemplateDetail resource={mockJT} />);
});
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([
<span>
<strong>SSH:</strong>Credential 1
</span>,
<span>
<strong>Awx:</strong>Credential 2
</span>,
])
).toEqual(true);
expect(
wrapper
.find('Detail[label="Labels"]')
.containsAllMatchingElements([<span>L_91o2</span>, <span>L_91o3</span>])
).toEqual(true);
expect(
wrapper
.find('Detail[label="Instance Groups"]')
.containsAllMatchingElements([<span>ig1</span>, <span>ig2</span>])
).toEqual(true);
expect(
wrapper
.find('Detail[label="Job Tags"]')
.containsAllMatchingElements([<span>T_100</span>, <span>T_200</span>])
).toEqual(true);
expect(
wrapper
.find('Detail[label="Skip Tags"]')
.containsAllMatchingElements([<span>S_100</span>, <span>S_200</span>])
).toEqual(true);
expect(
wrapper
.find('Detail[label="Options"]')
.containsAllMatchingElements([
<li>Enable Privilege Escalation</li>,
<li>Allow Provisioning Callbacks</li>,
<li>Enable Concurrent Jobs</li>,
<li>Use Fact Storage</li>,
])
).toEqual(true);
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
'---foo: bar'
);
});
});

View File

@ -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
}

View File

@ -294,7 +294,7 @@ function JobTemplateDetail({ i18n, template }) {
{job_tags && job_tags.length > 0 && (
<Detail
fullWidth
label={i18n._(t`Job tags`)}
label={i18n._(t`Job Tags`)}
value={
<ChipGroup numChips={5}>
{job_tags.split(',').map(jobTag => (
@ -309,7 +309,7 @@ function JobTemplateDetail({ i18n, template }) {
{skip_tags && skip_tags.length > 0 && (
<Detail
fullWidth
label={i18n._(t`Skip tags`)}
label={i18n._(t`Skip Tags`)}
value={
<ChipGroup numChips={5}>
{skip_tags.split(',').map(skipTag => (

View File

@ -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

View File

@ -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(
<WorkflowDispatchContext.Provider value={dispatch}>
@ -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();
});