mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 10:30:03 -03:30
Merge pull request #4816 from marshmalien/add-missing-job-detail-fields
Add missing job detail fields Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
commit
3a7756393e
@ -11,7 +11,7 @@ const DetailList = ({ children, stacked, ...props }) => (
|
||||
export default styled(DetailList)`
|
||||
display: grid;
|
||||
grid-gap: 20px;
|
||||
align-items: baseline;
|
||||
align-items: flex-start;
|
||||
${props =>
|
||||
props.stacked
|
||||
? `
|
||||
|
||||
@ -75,11 +75,12 @@ const SkippedBottom = styled.div`
|
||||
|
||||
const StatusIcon = ({ status, ...props }) => {
|
||||
return (
|
||||
<div {...props}>
|
||||
<div className="at-c-statusIcon" {...props}>
|
||||
{status === 'running' && <RunningJob />}
|
||||
{(status === 'new' || status === 'pending' || status === 'waiting') && (
|
||||
<WaitingJob />
|
||||
)}
|
||||
{(status === 'new' ||
|
||||
status === 'pending' ||
|
||||
status === 'waiting' ||
|
||||
status === 'never updated') && <WaitingJob />}
|
||||
{(status === 'failed' || status === 'error' || status === 'canceled') && (
|
||||
<FinishedJob>
|
||||
<FailedTop />
|
||||
|
||||
@ -10,6 +10,7 @@ import { DetailList, Detail } from '@components/DetailList';
|
||||
import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
|
||||
import { VariablesInput as _VariablesInput } from '@components/CodeMirrorInput';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import { StatusIcon } from '@components/Sparkline';
|
||||
import { toTitleCase } from '@util/strings';
|
||||
import { Job } from '../../../types';
|
||||
import {
|
||||
@ -37,6 +38,14 @@ const VariablesInput = styled(_VariablesInput)`
|
||||
}
|
||||
`;
|
||||
|
||||
const StatusDetailValue = styled.div`
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
.at-c-statusIcon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const VERBOSITY = {
|
||||
0: '0 (Normal)',
|
||||
1: '1 (Verbose)',
|
||||
@ -45,18 +54,50 @@ const VERBOSITY = {
|
||||
4: '4 (Connection Debug)',
|
||||
};
|
||||
|
||||
const getLaunchedByDetails = ({ summary_fields = {}, related = {} }) => {
|
||||
const {
|
||||
created_by: createdBy,
|
||||
job_template: jobTemplate,
|
||||
schedule,
|
||||
} = summary_fields;
|
||||
const { schedule: relatedSchedule } = related;
|
||||
|
||||
if (!createdBy && !schedule) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let link;
|
||||
let value;
|
||||
|
||||
if (createdBy) {
|
||||
link = `/users/${createdBy.id}`;
|
||||
value = createdBy.username;
|
||||
} else if (relatedSchedule && jobTemplate) {
|
||||
link = `/templates/job_template/${jobTemplate.id}/schedules/${schedule.id}`;
|
||||
value = schedule.name;
|
||||
} else {
|
||||
link = null;
|
||||
value = schedule.name;
|
||||
}
|
||||
|
||||
return { link, value };
|
||||
};
|
||||
|
||||
function JobDetail({ job, i18n, history }) {
|
||||
const {
|
||||
job_template: jobTemplate,
|
||||
project,
|
||||
inventory,
|
||||
instance_group: instanceGroup,
|
||||
credentials,
|
||||
instance_group: instanceGroup,
|
||||
inventory,
|
||||
job_template: jobTemplate,
|
||||
labels,
|
||||
project,
|
||||
} = job.summary_fields;
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState();
|
||||
|
||||
const { value: launchedByValue, link: launchedByLink } =
|
||||
getLaunchedByDetails(job) || {};
|
||||
|
||||
const deleteJob = async () => {
|
||||
try {
|
||||
switch (job.type) {
|
||||
@ -84,11 +125,20 @@ function JobDetail({ job, i18n, history }) {
|
||||
setIsDeleteModalOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
<DetailList>
|
||||
{/* TODO: add status icon? */}
|
||||
<Detail label={i18n._(t`Status`)} value={toTitleCase(job.status)} />
|
||||
{/* TODO: hookup status to websockets */}
|
||||
<Detail
|
||||
label={i18n._(t`Status`)}
|
||||
value={
|
||||
<StatusDetailValue>
|
||||
{job.status && <StatusIcon status={job.status} />}
|
||||
{toTitleCase(job.status)}
|
||||
</StatusDetailValue>
|
||||
}
|
||||
/>
|
||||
<Detail label={i18n._(t`Started`)} value={job.started} />
|
||||
<Detail label={i18n._(t`Finished`)} value={job.finished} />
|
||||
{jobTemplate && (
|
||||
@ -102,6 +152,16 @@ function JobDetail({ job, i18n, history }) {
|
||||
/>
|
||||
)}
|
||||
<Detail label={i18n._(t`Job Type`)} value={toTitleCase(job.job_type)} />
|
||||
<Detail
|
||||
label={i18n._(t`Launched By`)}
|
||||
value={
|
||||
launchedByLink ? (
|
||||
<Link to={`${launchedByLink}`}>{launchedByValue}</Link>
|
||||
) : (
|
||||
launchedByValue
|
||||
)
|
||||
}
|
||||
/>
|
||||
{inventory && (
|
||||
<Detail
|
||||
label={i18n._(t`Inventory`)}
|
||||
@ -110,19 +170,23 @@ function JobDetail({ job, i18n, history }) {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* TODO: show project status icon */}
|
||||
{project && (
|
||||
<Detail
|
||||
label={i18n._(t`Project`)}
|
||||
value={<Link to={`/projects/${project.id}`}>{project.name}</Link>}
|
||||
value={
|
||||
<StatusDetailValue>
|
||||
{project.status && <StatusIcon status={project.status} />}
|
||||
<Link to={`/projects/${project.id}`}>{project.name}</Link>
|
||||
</StatusDetailValue>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Detail label={i18n._(t`Revision`)} value={job.scm_revision} />
|
||||
<Detail label={i18n._(t`Playbook`)} value={null} />
|
||||
<Detail label={i18n._(t`Playbook`)} value={job.playbook} />
|
||||
<Detail label={i18n._(t`Limit`)} value={job.limit} />
|
||||
<Detail label={i18n._(t`Verbosity`)} value={VERBOSITY[job.verbosity]} />
|
||||
<Detail label={i18n._(t`Environment`)} value={null} />
|
||||
<Detail label={i18n._(t`Execution Node`)} value={job.exucution_node} />
|
||||
<Detail label={i18n._(t`Environment`)} value={job.custom_virtualenv} />
|
||||
<Detail label={i18n._(t`Execution Node`)} value={job.execution_node} />
|
||||
{instanceGroup && (
|
||||
<Detail
|
||||
label={i18n._(t`Instance Group`)}
|
||||
|
||||
@ -4,78 +4,77 @@ import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { sleep } from '@testUtils/testUtils';
|
||||
import JobDetail from './JobDetail';
|
||||
import { JobsAPI, ProjectUpdatesAPI } from '@api';
|
||||
import mockJobData from '../shared/data.job.json';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
describe('<JobDetail />', () => {
|
||||
let job;
|
||||
|
||||
beforeEach(() => {
|
||||
job = {
|
||||
name: 'Foo',
|
||||
summary_fields: {},
|
||||
};
|
||||
});
|
||||
|
||||
test('initially renders succesfully', () => {
|
||||
mountWithContexts(<JobDetail job={job} />);
|
||||
mountWithContexts(<JobDetail job={mockJobData} />);
|
||||
});
|
||||
|
||||
test('should display a Close button', () => {
|
||||
const wrapper = mountWithContexts(<JobDetail job={job} />);
|
||||
const wrapper = mountWithContexts(<JobDetail job={mockJobData} />);
|
||||
|
||||
expect(wrapper.find('Button[aria-label="close"]').length).toBe(1);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('should display details', () => {
|
||||
job.status = 'Successful';
|
||||
job.started = '2019-07-02T17:35:22.753817Z';
|
||||
job.finished = '2019-07-02T17:35:34.910800Z';
|
||||
const wrapper = mountWithContexts(<JobDetail job={mockJobData} />);
|
||||
|
||||
const wrapper = mountWithContexts(<JobDetail job={job} />);
|
||||
const details = wrapper.find('Detail');
|
||||
|
||||
function assertDetail(detail, label, value) {
|
||||
expect(detail.prop('label')).toEqual(label);
|
||||
expect(detail.prop('value')).toEqual(value);
|
||||
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(details.at(0), 'Status', 'Successful');
|
||||
assertDetail(details.at(1), 'Started', job.started);
|
||||
assertDetail(details.at(2), 'Finished', job.finished);
|
||||
assertDetail('Status', 'Successful');
|
||||
assertDetail('Started', mockJobData.started);
|
||||
assertDetail('Finished', mockJobData.finished);
|
||||
assertDetail('Template', mockJobData.summary_fields.job_template.name);
|
||||
assertDetail('Job Type', 'Run');
|
||||
assertDetail('Launched By', mockJobData.summary_fields.created_by.username);
|
||||
assertDetail('Inventory', mockJobData.summary_fields.inventory.name);
|
||||
assertDetail('Project', mockJobData.summary_fields.project.name);
|
||||
assertDetail('Revision', mockJobData.scm_revision);
|
||||
assertDetail('Playbook', mockJobData.playbook);
|
||||
assertDetail('Verbosity', '0 (Normal)');
|
||||
assertDetail('Environment', mockJobData.custom_virtualenv);
|
||||
assertDetail('Execution Node', mockJobData.execution_node);
|
||||
assertDetail(
|
||||
'Instance Group',
|
||||
mockJobData.summary_fields.instance_group.name
|
||||
);
|
||||
assertDetail('Job Slice', '0/1');
|
||||
assertDetail('Credentials', 'SSH: Demo Credential');
|
||||
});
|
||||
|
||||
test('should display credentials', () => {
|
||||
job.summary_fields.credentials = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Foo',
|
||||
cloud: false,
|
||||
kind: 'ssh',
|
||||
},
|
||||
];
|
||||
const wrapper = mountWithContexts(<JobDetail job={job} />);
|
||||
const wrapper = mountWithContexts(<JobDetail job={mockJobData} />);
|
||||
const credentialChip = wrapper.find('CredentialChip');
|
||||
|
||||
expect(credentialChip.prop('credential')).toEqual(
|
||||
job.summary_fields.credentials[0]
|
||||
mockJobData.summary_fields.credentials[0]
|
||||
);
|
||||
});
|
||||
|
||||
test('should display successful job status icon', () => {
|
||||
const wrapper = mountWithContexts(<JobDetail job={mockJobData} />);
|
||||
const statusDetail = wrapper.find('Detail[label="Status"]');
|
||||
expect(statusDetail.find('StatusIcon__SuccessfulTop')).toHaveLength(1);
|
||||
expect(statusDetail.find('StatusIcon__SuccessfulBottom')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should display successful project status icon', () => {
|
||||
const wrapper = mountWithContexts(<JobDetail job={mockJobData} />);
|
||||
const statusDetail = wrapper.find('Detail[label="Project"]');
|
||||
expect(statusDetail.find('StatusIcon__SuccessfulTop')).toHaveLength(1);
|
||||
expect(statusDetail.find('StatusIcon__SuccessfulBottom')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should properly delete job', async () => {
|
||||
job = {
|
||||
name: 'Rage',
|
||||
id: 1,
|
||||
type: 'job',
|
||||
summary_fields: {
|
||||
job_template: { name: 'Spud' },
|
||||
},
|
||||
};
|
||||
const wrapper = mountWithContexts(<JobDetail job={job} />);
|
||||
wrapper
|
||||
.find('button')
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
const wrapper = mountWithContexts(<JobDetail job={mockJobData} />);
|
||||
wrapper.find('button[aria-label="Delete"]').simulate('click');
|
||||
await sleep(1);
|
||||
wrapper.update();
|
||||
const modal = wrapper.find('Modal');
|
||||
@ -83,17 +82,12 @@ describe('<JobDetail />', () => {
|
||||
modal.find('button[aria-label="Delete"]').simulate('click');
|
||||
expect(JobsAPI.destroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
// The test below is skipped until react can be upgraded to at least 16.9.0. An upgrade to
|
||||
// react - router will likely be necessary also.
|
||||
/*
|
||||
The test below is skipped until react can be upgraded to at least 16.9.0. An upgrade to
|
||||
react - router will likely be necessary also.
|
||||
See: https://github.com/ansible/awx/issues/4817
|
||||
*/
|
||||
test.skip('should display error modal when a job does not delete properly', async () => {
|
||||
job = {
|
||||
name: 'Angry',
|
||||
id: 'a',
|
||||
type: 'project_update',
|
||||
summary_fields: {
|
||||
job_template: { name: 'Peanut' },
|
||||
},
|
||||
};
|
||||
ProjectUpdatesAPI.destroy.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
@ -106,7 +100,7 @@ describe('<JobDetail />', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
const wrapper = mountWithContexts(<JobDetail job={job} />);
|
||||
const wrapper = mountWithContexts(<JobDetail job={mockJobData} />);
|
||||
|
||||
wrapper
|
||||
.find('button')
|
||||
|
||||
@ -32,7 +32,7 @@ const Modal = styled(PFModal)`
|
||||
const HostNameDetailValue = styled.div`
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
> div {
|
||||
.at-c-statusIcon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import JobOutput from './JobOutput';
|
||||
import { JobsAPI } from '@api';
|
||||
import mockJobData from './data.job.json';
|
||||
import mockJobData from '../shared/data.job.json';
|
||||
import mockJobEventsData from './data.job_events.json';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
@ -321,9 +321,9 @@ exports[`<OrganizationAccessItem /> initially renders succesfully 1`] = `
|
||||
"componentStyle": ComponentStyle {
|
||||
"componentId": "DetailList-sc-12g7m4-0",
|
||||
"isStatic": false,
|
||||
"lastClassName": "gmERnX",
|
||||
"lastClassName": "eYaZBv",
|
||||
"rules": Array [
|
||||
"display:grid;grid-gap:20px;align-items:baseline;",
|
||||
"display:grid;grid-gap:20px;align-items:flex-start;",
|
||||
[Function],
|
||||
],
|
||||
},
|
||||
@ -341,15 +341,15 @@ exports[`<OrganizationAccessItem /> initially renders succesfully 1`] = `
|
||||
stacked={true}
|
||||
>
|
||||
<DetailList
|
||||
className="DetailList-sc-12g7m4-0 gmERnX"
|
||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
||||
stacked={true}
|
||||
>
|
||||
<TextList
|
||||
className="DetailList-sc-12g7m4-0 gmERnX"
|
||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
||||
component="dl"
|
||||
>
|
||||
<dl
|
||||
className="DetailList-sc-12g7m4-0 gmERnX"
|
||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
||||
data-pf-content={true}
|
||||
>
|
||||
<Detail
|
||||
@ -484,9 +484,9 @@ exports[`<OrganizationAccessItem /> initially renders succesfully 1`] = `
|
||||
"componentStyle": ComponentStyle {
|
||||
"componentId": "DetailList-sc-12g7m4-0",
|
||||
"isStatic": false,
|
||||
"lastClassName": "gmERnX",
|
||||
"lastClassName": "eYaZBv",
|
||||
"rules": Array [
|
||||
"display:grid;grid-gap:20px;align-items:baseline;",
|
||||
"display:grid;grid-gap:20px;align-items:flex-start;",
|
||||
[Function],
|
||||
],
|
||||
},
|
||||
@ -504,15 +504,15 @@ exports[`<OrganizationAccessItem /> initially renders succesfully 1`] = `
|
||||
stacked={true}
|
||||
>
|
||||
<DetailList
|
||||
className="DetailList-sc-12g7m4-0 gmERnX"
|
||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
||||
stacked={true}
|
||||
>
|
||||
<TextList
|
||||
className="DetailList-sc-12g7m4-0 gmERnX"
|
||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
||||
component="dl"
|
||||
>
|
||||
<dl
|
||||
className="DetailList-sc-12g7m4-0 gmERnX"
|
||||
className="DetailList-sc-12g7m4-0 eYaZBv"
|
||||
data-pf-content={true}
|
||||
>
|
||||
<Detail
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user