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:
softwarefactory-project-zuul[bot] 2019-09-26 19:40:10 +00:00 committed by GitHub
commit 3a7756393e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 144 additions and 85 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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');

View File

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