Merge pull request #5752 from marshmalien/details-delete-job-template

Add delete button to Job Template details

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-01-27 15:08:54 +00:00
committed by GitHub
4 changed files with 432 additions and 434 deletions

View File

@@ -1,5 +1,5 @@
import React, { Component, Fragment } from 'react'; import React, { Fragment, useState, useEffect } from 'react';
import { Link, withRouter } from 'react-router-dom'; import { Link, useHistory, useParams } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { import {
Button, Button,
@@ -11,12 +11,15 @@ import {
import styled from 'styled-components'; import styled from 'styled-components';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import AlertModal from '@components/AlertModal';
import { CardBody, CardActionsRow } from '@components/Card'; import { CardBody, CardActionsRow } from '@components/Card';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import LaunchButton from '@components/LaunchButton';
import ContentLoading from '@components/ContentLoading'; import ContentLoading from '@components/ContentLoading';
import { ChipGroup, Chip, CredentialChip } from '@components/Chip'; import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
import { DetailList, Detail, UserDateDetail } from '@components/DetailList'; import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
import DeleteButton from '@components/DeleteButton';
import ErrorDetail from '@components/ErrorDetail';
import LaunchButton from '@components/LaunchButton';
import { JobTemplatesAPI } from '@api'; import { JobTemplatesAPI } from '@api';
const MissingDetail = styled(Detail)` const MissingDetail = styled(Detail)`
@@ -25,319 +28,325 @@ const MissingDetail = styled(Detail)`
} }
`; `;
class JobTemplateDetail extends Component { function JobTemplateDetail({ i18n, template }) {
constructor(props) { const {
super(props); ask_inventory_on_launch,
this.state = { allow_simultaneous,
contentError: null, become_enabled,
hasContentLoading: true, created,
instanceGroups: [], description,
}; diff_mode,
this.readInstanceGroups = this.readInstanceGroups.bind(this); forks,
} host_config_key,
job_slice_count,
job_tags,
job_type,
name,
limit,
modified,
playbook,
skip_tags,
timeout,
summary_fields,
use_fact_cache,
url,
verbosity,
} = template;
const [contentError, setContentError] = useState(null);
const [deletionError, setDeletionError] = useState(null);
const [hasContentLoading, setHasContentLoading] = useState(false);
const [instanceGroups, setInstanceGroups] = useState([]);
const { id: templateId } = useParams();
const history = useHistory();
componentDidMount() { useEffect(() => {
this.readInstanceGroups(); (async () => {
} setContentError(null);
setHasContentLoading(true);
try {
const {
data: { results = [] },
} = await JobTemplatesAPI.readInstanceGroups(templateId);
setInstanceGroups(results);
} catch (error) {
setContentError(error);
} finally {
setHasContentLoading(false);
}
})();
}, [templateId]);
async readInstanceGroups() { const handleDelete = async () => {
const { match } = this.props; setHasContentLoading(true);
try { try {
const { data } = await JobTemplatesAPI.readInstanceGroups( await JobTemplatesAPI.destroy(templateId);
match.params.id history.push(`/templates`);
); } catch (error) {
this.setState({ instanceGroups: [...data.results] }); setDeletionError(error);
} catch (err) {
this.setState({ contentError: err });
} finally {
this.setState({ hasContentLoading: false });
} }
} setHasContentLoading(false);
};
render() { const canLaunch =
const { summary_fields.user_capabilities && summary_fields.user_capabilities.start;
template: { const verbosityOptions = [
ask_inventory_on_launch, { verbosity: 0, details: i18n._(t`0 (Normal)`) },
allow_simultaneous, { verbosity: 1, details: i18n._(t`1 (Verbose)`) },
become_enabled, { verbosity: 2, details: i18n._(t`2 (More Verbose)`) },
created, { verbosity: 3, details: i18n._(t`3 (Debug)`) },
description, { verbosity: 4, details: i18n._(t`4 (Connection Debug)`) },
diff_mode, { verbosity: 5, details: i18n._(t`5 (WinRM Debug)`) },
forks, ];
host_config_key, const verbosityDetails = verbosityOptions.filter(
job_slice_count, option => option.verbosity === verbosity
job_tags, );
job_type, const generateCallBackUrl = `${window.location.origin + url}callback/`;
name, const renderOptionsField =
limit, become_enabled || host_config_key || allow_simultaneous || use_fact_cache;
modified,
playbook,
skip_tags,
timeout,
summary_fields,
use_fact_cache,
url,
verbosity,
},
hasTemplateLoading,
template,
i18n,
match,
} = this.props;
const canLaunch = summary_fields.user_capabilities.start;
const { instanceGroups, hasContentLoading, contentError } = this.state;
const verbosityOptions = [
{ verbosity: 0, details: i18n._(t`0 (Normal)`) },
{ verbosity: 1, details: i18n._(t`1 (Verbose)`) },
{ verbosity: 2, details: i18n._(t`2 (More Verbose)`) },
{ verbosity: 3, details: i18n._(t`3 (Debug)`) },
{ verbosity: 4, details: i18n._(t`4 (Connection Debug)`) },
{ verbosity: 5, details: i18n._(t`5 (WinRM Debug)`) },
];
const verbosityDetails = verbosityOptions.filter(
option => option.verbosity === verbosity
);
const generateCallBackUrl = `${window.location.origin + url}callback/`;
const isInitialized = !hasTemplateLoading && !hasContentLoading;
const renderOptionsField = const renderOptions = (
become_enabled || host_config_key || allow_simultaneous || use_fact_cache; <TextList component={TextListVariants.ul}>
{become_enabled && (
<TextListItem component={TextListItemVariants.li}>
{i18n._(t`Enable Privilege Escalation`)}
</TextListItem>
)}
{host_config_key && (
<TextListItem component={TextListItemVariants.li}>
{i18n._(t`Allow Provisioning Callbacks`)}
</TextListItem>
)}
{allow_simultaneous && (
<TextListItem component={TextListItemVariants.li}>
{i18n._(t`Enable Concurrent Jobs`)}
</TextListItem>
)}
{use_fact_cache && (
<TextListItem component={TextListItemVariants.li}>
{i18n._(t`Use Fact Cache`)}
</TextListItem>
)}
</TextList>
);
const renderOptions = ( const renderMissingDataDetail = value => (
<TextList component={TextListVariants.ul}> <MissingDetail label={value} value={i18n._(t`Deleted`)} />
{become_enabled && ( );
<TextListItem component={TextListItemVariants.li}>
{i18n._(t`Enable Privilege Escalation`)}
</TextListItem>
)}
{host_config_key && (
<TextListItem component={TextListItemVariants.li}>
{i18n._(t`Allow Provisioning Callbacks`)}
</TextListItem>
)}
{allow_simultaneous && (
<TextListItem component={TextListItemVariants.li}>
{i18n._(t`Enable Concurrent Jobs`)}
</TextListItem>
)}
{use_fact_cache && (
<TextListItem component={TextListItemVariants.li}>
{i18n._(t`Use Fact Cache`)}
</TextListItem>
)}
</TextList>
);
const renderMissingDataDetail = value => ( const inventoryValue = (kind, id) => {
<MissingDetail label={value} value={i18n._(t`Deleted`)} /> const inventorykind = kind === 'smart' ? 'smart_inventory' : 'inventory';
);
const inventoryValue = (kind, id) => { return ask_inventory_on_launch ? (
const inventorykind = <Fragment>
kind === 'smart' ? (kind = 'smart_inventory') : (kind = 'inventory');
return ask_inventory_on_launch ? (
<Fragment>
<Link to={`/inventories/${inventorykind}/${id}/details`}>
{summary_fields.inventory.name}
</Link>
<span> {i18n._(t`(Prompt on Launch)`)}</span>
</Fragment>
) : (
<Link to={`/inventories/${inventorykind}/${id}/details`}> <Link to={`/inventories/${inventorykind}/${id}/details`}>
{summary_fields.inventory.name} {summary_fields.inventory.name}
</Link> </Link>
); <span> {i18n._(t`(Prompt on Launch)`)}</span>
}; </Fragment>
) : (
<Link to={`/inventories/${inventorykind}/${id}/details`}>
{summary_fields.inventory.name}
</Link>
);
};
if (contentError) { if (contentError) {
return <ContentError error={contentError} />; return <ContentError error={contentError} />;
} }
if (hasContentLoading) { if (hasContentLoading) {
return <ContentLoading />; return <ContentLoading />;
} }
return ( return (
isInitialized && ( <CardBody>
<CardBody> <DetailList gutter="sm">
<DetailList gutter="sm"> <Detail label={i18n._(t`Name`)} value={name} dataCy="jt-detail-name" />
<Detail <Detail label={i18n._(t`Description`)} value={description} />
label={i18n._(t`Name`)} <Detail label={i18n._(t`Job Type`)} value={job_type} />
value={name} {summary_fields.inventory ? (
dataCy="jt-detail-name" <Detail
/> label={i18n._(t`Inventory`)}
<Detail label={i18n._(t`Description`)} value={description} /> value={inventoryValue(
<Detail label={i18n._(t`Job Type`)} value={job_type} /> summary_fields.inventory.kind,
summary_fields.inventory.id
{summary_fields.inventory ? (
<Detail
label={i18n._(t`Inventory`)}
value={inventoryValue(
summary_fields.inventory.kind,
summary_fields.inventory.id
)}
/>
) : (
!ask_inventory_on_launch &&
renderMissingDataDetail(i18n._(t`Inventory`))
)} )}
{summary_fields.project ? ( />
<Detail ) : (
label={i18n._(t`Project`)} !ask_inventory_on_launch &&
value={ renderMissingDataDetail(i18n._(t`Inventory`))
<Link to={`/projects/${summary_fields.project.id}/details`}> )}
{summary_fields.project {summary_fields.project ? (
? summary_fields.project.name <Detail
: i18n._(t`Deleted`)} label={i18n._(t`Project`)}
</Link> value={
} <Link to={`/projects/${summary_fields.project.id}/details`}>
/> {summary_fields.project.name}
) : ( </Link>
renderMissingDataDetail(i18n._(t`Project`)) }
)} />
<Detail label={i18n._(t`SCM Branch`)} value={template.scm_branch} /> ) : (
<Detail label={i18n._(t`Playbook`)} value={playbook} /> renderMissingDataDetail(i18n._(t`Project`))
<Detail label={i18n._(t`Forks`)} value={forks || '0'} /> )}
<Detail label={i18n._(t`Limit`)} value={limit} /> <Detail label={i18n._(t`SCM Branch`)} value={template.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={verbosityDetails[0].details}
/>
<Detail label={i18n._(t`Timeout`)} value={timeout || '0'} />
<UserDateDetail
label={i18n._(t`Created`)}
date={created}
user={summary_fields.created_by}
/>
<UserDateDetail
label={i18n._(t`Last Modified`)}
date={modified}
user={summary_fields.modified_by}
/>
<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 <Detail
label={i18n._(t`Verbosity`)} label={i18n._(t`Host Config Key`)}
value={verbosityDetails[0].details} value={host_config_key}
/>
<Detail label={i18n._(t`Timeout`)} value={timeout || '0'} />
<UserDateDetail
label={i18n._(t`Created`)}
date={created}
user={summary_fields.created_by}
/>
<UserDateDetail
label={i18n._(t`Last Modified`)}
date={modified}
user={summary_fields.modified_by}
/> />
<Detail <Detail
label={i18n._(t`Show Changes`)} label={i18n._(t`Provisioning Callback URL`)}
value={diff_mode ? 'On' : 'Off'} value={generateCallBackUrl}
/> />
<Detail label={i18n._(t` Job Slicing`)} value={job_slice_count} /> </React.Fragment>
{host_config_key && ( )}
<React.Fragment> {renderOptionsField && (
<Detail <Detail label={i18n._(t`Options`)} value={renderOptions} />
label={i18n._(t`Host Config Key`)} )}
value={host_config_key} {summary_fields.credentials && summary_fields.credentials.length > 0 && (
/> <Detail
<Detail fullWidth
label={i18n._(t`Provisioning Callback URL`)} label={i18n._(t`Credentials`)}
value={generateCallBackUrl} value={
/> <ChipGroup numChips={5}>
</React.Fragment> {summary_fields.credentials.map(c => (
)} <CredentialChip key={c.id} credential={c} isReadOnly />
{renderOptionsField && ( ))}
<Detail label={i18n._(t`Options`)} value={renderOptions} /> </ChipGroup>
)} }
{summary_fields.credentials && />
summary_fields.credentials.length > 0 && ( )}
<Detail {summary_fields.labels && summary_fields.labels.results.length > 0 && (
fullWidth <Detail
label={i18n._(t`Credentials`)} fullWidth
value={ label={i18n._(t`Labels`)}
<ChipGroup numChips={5}> value={
{summary_fields.credentials.map(c => ( <ChipGroup numChips={5}>
<CredentialChip key={c.id} credential={c} isReadOnly /> {summary_fields.labels.results.map(l => (
))} <Chip key={l.id} isReadOnly>
</ChipGroup> {l.name}
} </Chip>
/> ))}
)} </ChipGroup>
{summary_fields.labels && summary_fields.labels.results.length > 0 && ( }
<Detail />
fullWidth )}
label={i18n._(t`Labels`)} {instanceGroups.length > 0 && (
value={ <Detail
<ChipGroup numChips={5}> fullWidth
{summary_fields.labels.results.map(l => ( label={i18n._(t`Instance Groups`)}
<Chip key={l.id} isReadOnly> value={
{l.name} <ChipGroup numChips={5}>
</Chip> {instanceGroups.map(ig => (
))} <Chip key={ig.id} isReadOnly>
</ChipGroup> {ig.name}
} </Chip>
/> ))}
)} </ChipGroup>
{instanceGroups.length > 0 && ( }
<Detail />
fullWidth )}
label={i18n._(t`Instance Groups`)} {job_tags && job_tags.length > 0 && (
value={ <Detail
<ChipGroup numChips={5}> fullWidth
{instanceGroups.map(ig => ( label={i18n._(t`Job tags`)}
<Chip key={ig.id} isReadOnly> value={
{ig.name} <ChipGroup numChips={5}>
</Chip> {job_tags.split(',').map(jobTag => (
))} <Chip key={jobTag} isReadOnly>
</ChipGroup> {jobTag}
} </Chip>
/> ))}
)} </ChipGroup>
{job_tags && job_tags.length > 0 && ( }
<Detail />
fullWidth )}
label={i18n._(t`Job tags`)} {skip_tags && skip_tags.length > 0 && (
value={ <Detail
<ChipGroup numChips={5}> fullWidth
{job_tags.split(',').map(jobTag => ( label={i18n._(t`Skip tags`)}
<Chip key={jobTag} isReadOnly> value={
{jobTag} <ChipGroup numChips={5}>
</Chip> {skip_tags.split(',').map(skipTag => (
))} <Chip key={skipTag} isReadOnly>
</ChipGroup> {skipTag}
} </Chip>
/> ))}
)} </ChipGroup>
{skip_tags && skip_tags.length > 0 && ( }
<Detail />
fullWidth )}
label={i18n._(t`Skip tags`)} </DetailList>
value={ <CardActionsRow>
<ChipGroup numChips={5}> {summary_fields.user_capabilities &&
{skip_tags.split(',').map(skipTag => ( summary_fields.user_capabilities.edit && (
<Chip key={skipTag} isReadOnly> <Button
{skipTag} component={Link}
</Chip> to={`/templates/job_template/${templateId}/edit`}
))} aria-label={i18n._(t`Edit`)}
</ChipGroup> >
} {i18n._(t`Edit`)}
/> </Button>
)} )}
</DetailList> {canLaunch && (
<CardActionsRow> <LaunchButton resource={template} aria-label={i18n._(t`Launch`)}>
{summary_fields.user_capabilities.edit && ( {({ handleLaunch }) => (
<Button <Button variant="secondary" type="submit" onClick={handleLaunch}>
component={Link} {i18n._(t`Launch`)}
to={`/templates/job_template/${match.params.id}/edit`}
aria-label={i18n._(t`Edit`)}
>
{i18n._(t`Edit`)}
</Button> </Button>
)} )}
{canLaunch && ( </LaunchButton>
<LaunchButton resource={template} aria-label={i18n._(t`Launch`)}> )}
{({ handleLaunch }) => ( {summary_fields.user_capabilities &&
<Button summary_fields.user_capabilities.delete && (
variant="secondary" <DeleteButton
type="submit" name={name}
onClick={handleLaunch} modalTitle={i18n._(t`Delete Job Template`)}
> onConfirm={handleDelete}
{i18n._(t`Launch`)} >
</Button> {i18n._(t`Delete`)}
)} </DeleteButton>
</LaunchButton> )}
)} </CardActionsRow>
</CardActionsRow> {/* Update delete modal to show dependencies https://github.com/ansible/awx/issues/5546 */}
</CardBody> {deletionError && (
) <AlertModal
); isOpen={deletionError}
} variant="danger"
title={i18n._(t`Error!`)}
onClose={() => setDeletionError(null)}
>
{i18n._(t`Failed to delete job template.`)}
<ErrorDetail error={deletionError} />
</AlertModal>
)}
</CardBody>
);
} }
export { JobTemplateDetail as _JobTemplateDetail }; export { JobTemplateDetail as _JobTemplateDetail };
export default withI18n()(withRouter(JobTemplateDetail)); export default withI18n()(JobTemplateDetail);

View File

@@ -1,161 +1,141 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import JobTemplateDetail, { _JobTemplateDetail } from './JobTemplateDetail'; import JobTemplateDetail from './JobTemplateDetail';
import { JobTemplatesAPI } from '@api'; import { JobTemplatesAPI } from '@api';
import mockTemplate from '../shared/data.job_template.json';
jest.mock('@api'); jest.mock('@api');
const mockInstanceGroups = {
count: 5,
data: {
results: [{ id: 1, name: 'IG1' }, { id: 2, name: 'IG2' }],
},
};
describe('<JobTemplateDetail />', () => { describe('<JobTemplateDetail />', () => {
const template = { let wrapper;
forks: 1,
host_config_key: 'ssh',
name: 'Temp 1',
job_type: 'run',
inventory: 1,
limit: '1',
project: 7,
playbook: '',
id: 1,
verbosity: 1,
summary_fields: {
user_capabilities: { edit: true },
created_by: { id: 1, username: 'Joe' },
modified_by: { id: 1, username: 'Joe' },
credentials: [
{ id: 1, kind: 'ssh', name: 'Credential 1' },
{ id: 2, kind: 'awx', name: 'Credential 2' },
],
inventory: { name: 'Inventory' },
project: { name: 'Project' },
},
created: '2020-04-25T01:23:45.678901Z',
modified: '2020-04-25T01:23:45.678901Z',
};
const mockInstanceGroups = { beforeEach(async () => {
count: 5,
data: {
results: [{ id: 1, name: 'IG1' }, { id: 2, name: 'IG2' }],
},
};
const readInstanceGroups = jest.spyOn(
_JobTemplateDetail.prototype,
'readInstanceGroups'
);
beforeEach(() => {
JobTemplatesAPI.readInstanceGroups.mockResolvedValue(mockInstanceGroups); JobTemplatesAPI.readInstanceGroups.mockResolvedValue(mockInstanceGroups);
await act(async () => {
wrapper = mountWithContexts(
<JobTemplateDetail template={mockTemplate} />
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
}); });
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
test('Can load with missing summary fields', async () => { test('should render successfully with missing summary fields', async () => {
const mockTemplate = { ...template }; await act(async () => {
mockTemplate.summary_fields = { user_capabilities: {} }; wrapper = mountWithContexts(
<JobTemplateDetail
const wrapper = mountWithContexts( template={{
<JobTemplateDetail template={mockTemplate} /> ...mockTemplate,
); become_enabled: true,
summary_fields: { user_capabilities: {} },
}}
/>
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
await waitForElement( await waitForElement(
wrapper, wrapper,
'Detail[label="Description"]', 'Detail[label="Name"]',
el => el.length === 1 el => el.length === 1
); );
}); });
test('When component mounts API is called to get instance groups', async done => { test('should request instance groups from api', async () => {
const wrapper = mountWithContexts(
<JobTemplateDetail template={template} />
);
await waitForElement(
wrapper,
'JobTemplateDetail',
el => el.state('hasContentLoading') === true
);
expect(readInstanceGroups).toHaveBeenCalled();
await waitForElement(
wrapper,
'JobTemplateDetail',
el => el.state('hasContentLoading') === false
);
expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalledTimes(1); expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
done();
}); });
test('Edit button is absent when user does not have edit privilege', async done => { test('should hide edit button for users without edit permission', async () => {
const regularUser = { JobTemplatesAPI.readInstanceGroups.mockResolvedValue({ data: {} });
forks: 1, await act(async () => {
host_config_key: 'ssh', wrapper = mountWithContexts(
name: 'Temp 1', <JobTemplateDetail
job_tags: 'cookies,pizza', template={{
job_type: 'run', ...mockTemplate,
inventory: 1, diff_mode: true,
limit: '1', host_config_key: 'key',
project: 7, summary_fields: { user_capabilities: { edit: false } },
playbook: '', }}
id: 1, />
verbosity: 0, );
created_by: 'Alex',
skip_tags: 'coffe,tea',
summary_fields: {
user_capabilities: { edit: false },
created_by: { id: 1, username: 'Joe' },
modified_by: { id: 1, username: 'Joe' },
inventory: { name: 'Inventory' },
project: { name: 'Project' },
labels: { count: 1, results: [{ name: 'Label', id: 1 }] },
},
created: '2020-04-25T01:23:45.678901Z',
modified: '2020-04-25T01:23:45.678901Z',
};
const wrapper = mountWithContexts(
<JobTemplateDetail template={regularUser} />
);
const jobTemplateDetail = wrapper.find('JobTemplateDetail');
const editButton = jobTemplateDetail.find('button[aria-label="Edit"]');
jobTemplateDetail.setState({
instanceGroups: mockInstanceGroups,
hasContentLoading: false,
contentError: false,
}); });
expect(editButton.length).toBe(0); expect(wrapper.find('button[aria-label="Edit"]').length).toBe(0);
done();
}); });
test('should render CredentialChip', () => { test('should render credential chips', () => {
template.summary_fields.credentials = [{ id: 1, name: 'cred', kind: null }]; const chips = wrapper.find('CredentialChip');
const wrapper = mountWithContexts( expect(chips).toHaveLength(2);
<JobTemplateDetail template={template} /> chips.forEach((chip, id) => {
); expect(chip.prop('credential')).toEqual(
wrapper.find('JobTemplateDetail').setState({ mockTemplate.summary_fields.credentials[id]
instanceGroups: mockInstanceGroups, );
hasContentLoading: false,
contentError: false,
}); });
const chip = wrapper.find('CredentialChip');
expect(chip).toHaveLength(1);
expect(chip.prop('credential')).toEqual(
template.summary_fields.credentials[0]
);
}); });
test('should render SCM_Branch', async () => { test('should render SCM_Branch', async () => {
const mockTemplate = { ...template };
mockTemplate.scm_branch = 'Foo branch';
const wrapper = mountWithContexts(
<JobTemplateDetail template={mockTemplate} />
);
await waitForElement(
wrapper,
'JobTemplateDetail',
el => el.state('hasContentLoading') === false
);
const SCMBranch = wrapper.find('Detail[label="SCM Branch"]'); const SCMBranch = wrapper.find('Detail[label="SCM Branch"]');
expect(SCMBranch.prop('value')).toBe('Foo branch'); expect(SCMBranch.prop('value')).toBe('Foo branch');
}); });
test('should show content error for failed instance group fetch', async () => {
JobTemplatesAPI.readInstanceGroups.mockImplementationOnce(() =>
Promise.reject(new Error())
);
await act(async () => {
wrapper = mountWithContexts(
<JobTemplateDetail
template={{
...mockTemplate,
allow_simultaneous: true,
ask_inventory_on_launch: true,
summary_fields: {
inventory: {
kind: 'smart',
},
},
}}
/>
);
});
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
});
test('expected api calls are made for delete', async () => {
await act(async () => {
wrapper.find('DeleteButton').invoke('onConfirm')();
});
expect(JobTemplatesAPI.destroy).toHaveBeenCalledTimes(1);
});
test('Error dialog shown for failed deletion', async () => {
JobTemplatesAPI.destroy.mockImplementationOnce(() =>
Promise.reject(new Error())
);
await act(async () => {
wrapper.find('DeleteButton').invoke('onConfirm')();
});
await waitForElement(
wrapper,
'Modal[title="Error!"]',
el => el.length === 1
);
await act(async () => {
wrapper.find('Modal[title="Error!"]').invoke('onClose')();
});
await waitForElement(
wrapper,
'Modal[title="Error!"]',
el => el.length === 0
);
});
}); });

View File

@@ -1,4 +1 @@
import JobTemplateDetail from './JobTemplateDetail'; export { default } from './JobTemplateDetail';
export { JobTemplateDetail as _JobTemplateDetail };
export default JobTemplateDetail;

View File

@@ -101,9 +101,14 @@
"copy": true "copy": true
}, },
"labels": { "labels": {
"count": 0, "count": 1,
"results": [] "results": [
}, {
"id": 91,
"name": "L_91o2"
}
]
},
"survey": { "survey": {
"title": "", "title": "",
"description": "" "description": ""
@@ -117,7 +122,14 @@
} }
], ],
"extra_credentials": [], "extra_credentials": [],
"credentials": [] "credentials": [
{
"id": 1, "kind": "ssh" , "name": "Credential 1"
},
{
"id": 2, "kind": "awx" , "name": "Credential 2"
}
]
}, },
"created": "2019-09-30T16:18:34.564820Z", "created": "2019-09-30T16:18:34.564820Z",
"modified": "2019-10-01T14:47:31.818431Z", "modified": "2019-10-01T14:47:31.818431Z",
@@ -127,17 +139,17 @@
"inventory": 1, "inventory": 1,
"project": 6, "project": 6,
"playbook": "ping.yml", "playbook": "ping.yml",
"scm_branch": "", "scm_branch": "Foo branch",
"forks": 0, "forks": 0,
"limit": "", "limit": "",
"verbosity": 0, "verbosity": 0,
"extra_vars": "", "extra_vars": "",
"job_tags": "", "job_tags": "T_100,T_200",
"force_handlers": false, "force_handlers": false,
"skip_tags": "", "skip_tags": "S_100,S_200",
"start_at_task": "", "start_at_task": "",
"timeout": 0, "timeout": 0,
"use_fact_cache": false, "use_fact_cache": true,
"last_job_run": "2019-10-01T14:34:35.142483Z", "last_job_run": "2019-10-01T14:34:35.142483Z",
"last_job_failed": false, "last_job_failed": false,
"next_job_run": null, "next_job_run": null,