mirror of
https://github.com/ansible/awx.git
synced 2026-03-19 09:57:33 -02:30
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:
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1 @@
|
|||||||
import JobTemplateDetail from './JobTemplateDetail';
|
export { default } from './JobTemplateDetail';
|
||||||
|
|
||||||
export { JobTemplateDetail as _JobTemplateDetail };
|
|
||||||
export default JobTemplateDetail;
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user