Merge pull request #6001 from mabashian/4967-jt-prompt-on-launch

Adds prompt on launch support to the rest of the relevant jt fields

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-02-27 09:13:29 +00:00
committed by GitHub
9 changed files with 593 additions and 439 deletions

View File

@@ -1,18 +1,32 @@
import React, { useState } from 'react';
import { string, bool } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { useField } from 'formik';
import styled from 'styled-components';
import { Split, SplitItem } from '@patternfly/react-core';
import { CheckboxField } from '@components/FormField';
import { yamlToJson, jsonToYaml, isJson } from '@util/yaml';
import CodeMirrorInput from './CodeMirrorInput';
import YamlJsonToggle from './YamlJsonToggle';
import { JSON_MODE, YAML_MODE } from './constants';
function VariablesField({ id, name, label, readOnly }) {
const FieldHeader = styled.div`
display: flex;
justify-content: space-between;
`;
const StyledCheckboxField = styled(CheckboxField)`
--pf-c-check__label--FontSize: var(--pf-c-form__label--FontSize);
`;
function VariablesField({ i18n, id, name, label, readOnly, promptId }) {
const [field, meta, helpers] = useField(name);
const [mode, setMode] = useState(isJson(field.value) ? JSON_MODE : YAML_MODE);
return (
<>
<div className="pf-c-form__group">
<FieldHeader>
<Split gutter="sm">
<SplitItem>
<label htmlFor={id} className="pf-c-form__label">
@@ -37,6 +51,14 @@ function VariablesField({ id, name, label, readOnly }) {
/>
</SplitItem>
</Split>
{promptId && (
<StyledCheckboxField
id="template-ask-variables-on-launch"
label={i18n._(t`Prompt On Launch`)}
name="ask_variables_on_launch"
/>
)}
</FieldHeader>
<CodeMirrorInput
mode={mode}
readOnly={readOnly}
@@ -51,7 +73,7 @@ function VariablesField({ id, name, label, readOnly }) {
{meta.error}
</div>
) : null}
</>
</div>
);
}
VariablesField.propTypes = {
@@ -59,9 +81,11 @@ VariablesField.propTypes = {
name: string.isRequired,
label: string.isRequired,
readOnly: bool,
promptId: string,
};
VariablesField.defaultProps = {
readOnly: false,
promptId: null,
};
export default VariablesField;
export default withI18n()(VariablesField);

View File

@@ -1,13 +1,11 @@
import React, { useState, useEffect } from 'react';
import { string, func, bool } from 'prop-types';
import { func, bool } from 'prop-types';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { FormGroup } from '@patternfly/react-core';
import { InventoriesAPI } from '@api';
import { Inventory } from '@types';
import Lookup from '@components/Lookup';
import { FieldTooltip } from '@components/FormField';
import { getQSConfig, parseQueryString } from '@util/qs';
import OptionsList from './shared/OptionsList';
import LookupErrorMessage from './shared/LookupErrorMessage';
@@ -18,17 +16,7 @@ const QS_CONFIG = getQSConfig('inventory', {
order_by: 'name',
});
function InventoryLookup({
value,
tooltip,
onChange,
onBlur,
required,
isValid,
helperTextInvalid,
i18n,
history,
}) {
function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) {
const [inventories, setInventories] = useState([]);
const [count, setCount] = useState(0);
const [error, setError] = useState(null);
@@ -47,14 +35,7 @@ function InventoryLookup({
}, [history.location]);
return (
<FormGroup
label={i18n._(t`Inventory`)}
isRequired={required}
fieldId="inventory-lookup"
isValid={isValid}
helperTextInvalid={helperTextInvalid}
>
{tooltip && <FieldTooltip content={tooltip} />}
<>
<Lookup
id="inventory-lookup"
header={i18n._(t`Inventory`)}
@@ -100,20 +81,18 @@ function InventoryLookup({
)}
/>
<LookupErrorMessage error={error} />
</FormGroup>
</>
);
}
InventoryLookup.propTypes = {
value: Inventory,
tooltip: string,
onChange: func.isRequired,
required: bool,
};
InventoryLookup.defaultProps = {
value: null,
tooltip: '',
required: false,
};

View File

@@ -3,10 +3,9 @@ import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { FormGroup, ToolbarItem } from '@patternfly/react-core';
import { ToolbarItem } from '@patternfly/react-core';
import { CredentialsAPI, CredentialTypesAPI } from '@api';
import AnsibleSelect from '@components/AnsibleSelect';
import { FieldTooltip } from '@components/FormField';
import CredentialChip from '@components/CredentialChip';
import { getQSConfig, parseQueryString } from '@util/qs';
import Lookup from './Lookup';
@@ -31,7 +30,7 @@ async function loadCredentials(params, selectedCredentialTypeId) {
}
function MultiCredentialsLookup(props) {
const { tooltip, value, onChange, onError, history, i18n } = props;
const { value, onChange, onError, history, i18n } = props;
const [credentialTypes, setCredentialTypes] = useState([]);
const [selectedType, setSelectedType] = useState(null);
const [credentials, setCredentials] = useState([]);
@@ -81,8 +80,6 @@ function MultiCredentialsLookup(props) {
const isMultiple = selectedType && selectedType.kind === 'vault';
return (
<FormGroup label={i18n._(t`Credentials`)} fieldId="multiCredential">
{tooltip && <FieldTooltip content={tooltip} />}
<Lookup
id="multiCredential"
header={i18n._(t`Credentials`)}
@@ -168,12 +165,10 @@ function MultiCredentialsLookup(props) {
);
}}
/>
</FormGroup>
);
}
MultiCredentialsLookup.propTypes = {
tooltip: PropTypes.string,
value: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number,
@@ -188,7 +183,6 @@ MultiCredentialsLookup.propTypes = {
};
MultiCredentialsLookup.defaultProps = {
tooltip: '',
value: [],
};

View File

@@ -6,9 +6,12 @@ import { Formik, useField } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Form } from '@patternfly/react-core';
import { Form, FormGroup } from '@patternfly/react-core';
import FormField, { FormSubmitError } from '@components/FormField';
import FormField, {
FormSubmitError,
FieldTooltip,
} from '@components/FormField';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
import { VariablesField } from '@components/CodeMirrorInput';
import { required } from '@util/validators';
@@ -23,7 +26,7 @@ function HostFormFields({ host, i18n }) {
const hostAddMatch = useRouteMatch('/hosts/add');
const inventoryFieldArr = useField({
name: 'inventory',
validate: required(i18n._(t`Select aå value for this field`), i18n),
validate: required(i18n._(t`Select a value for this field`), i18n),
});
const inventoryMeta = inventoryFieldArr[1];
const inventoryHelpers = inventoryFieldArr[2];
@@ -45,6 +48,18 @@ function HostFormFields({ host, i18n }) {
label={i18n._(t`Description`)}
/>
{hostAddMatch && (
<FormGroup
label={i18n._(t`Inventory`)}
isRequired
fieldId="inventory-lookup"
isValid={!inventoryMeta.touched || !inventoryMeta.error}
helperTextInvalid={inventoryMeta.error}
>
<FieldTooltip
content={i18n._(
t`Select the inventory that this host will belong to.`
)}
/>
<InventoryLookup
value={inventory}
onBlur={() => inventoryHelpers.setTouched()}
@@ -54,13 +69,14 @@ function HostFormFields({ host, i18n }) {
isValid={!inventoryMeta.touched || !inventoryMeta.error}
helperTextInvalid={inventoryMeta.error}
onChange={value => {
inventoryHelpers.setValuealue(value.id);
inventoryHelpers.setValue(value.id);
setInventory(value);
}}
required
touched={inventoryMeta.touched}
error={inventoryMeta.error}
/>
</FormGroup>
)}
<FormFullWidthLayout>
<VariablesField

View File

@@ -10,9 +10,20 @@ jest.mock('@api');
const jobTemplateData = {
allow_callbacks: false,
allow_simultaneous: false,
ask_credential_on_launch: false,
ask_diff_mode_on_launch: false,
ask_inventory_on_launch: false,
ask_job_type_on_launch: false,
description: 'Baz',
ask_limit_on_launch: false,
ask_scm_branch_on_launch: false,
ask_skip_tags_on_launch: false,
ask_tags_on_launch: false,
ask_variables_on_launch: false,
ask_verbosity_on_launch: false,
become_enabled: false,
description: '',
diff_mode: false,
extra_vars: '---\n',
forks: 0,
host_config_key: '',
inventory: 1,
@@ -20,9 +31,9 @@ const jobTemplateData = {
job_tags: '',
job_type: 'run',
limit: '',
name: 'Foo',
playbook: 'Bar',
project: 2,
name: '',
playbook: '',
project: 1,
scm_branch: '',
skip_tags: '',
timeout: 0,
@@ -103,13 +114,12 @@ describe('<JobTemplateAdd />', () => {
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
act(() => {
wrapper.find('input#template-name').simulate('change', {
target: { value: 'Foo', name: 'name' },
});
wrapper.find('AnsibleSelect#template-job-type').invoke('onChange')('run');
wrapper.find('InventoryLookup').invoke('onChange')({
id: 1,
organization: 1,
target: { value: 'Bar', name: 'name' },
});
wrapper.find('AnsibleSelect#template-job-type').prop('onChange')(
null,
'check'
);
wrapper.find('ProjectLookup').invoke('onChange')({
id: 2,
name: 'project',
@@ -119,7 +129,14 @@ describe('<JobTemplateAdd />', () => {
.find('PlaybookSelect')
.prop('field')
.onChange({
target: { value: 'Bar', name: 'playbook' },
target: { value: 'Baz', name: 'playbook' },
});
});
wrapper.update();
act(() => {
wrapper.find('InventoryLookup').invoke('onChange')({
id: 2,
organization: 1,
});
});
wrapper.update();
@@ -129,8 +146,11 @@ describe('<JobTemplateAdd />', () => {
wrapper.update();
expect(JobTemplatesAPI.create).toHaveBeenCalledWith({
...jobTemplateData,
description: '',
become_enabled: false,
name: 'Bar',
job_type: 'check',
project: 2,
playbook: 'Baz',
inventory: 2,
});
});
@@ -154,11 +174,10 @@ describe('<JobTemplateAdd />', () => {
wrapper.find('input#template-name').simulate('change', {
target: { value: 'Foo', name: 'name' },
});
wrapper.find('AnsibleSelect#template-job-type').invoke('onChange')('run');
wrapper.find('InventoryLookup').invoke('onChange')({
id: 1,
organization: 1,
});
wrapper.find('AnsibleSelect#template-job-type').prop('onChange')(
null,
'check'
);
wrapper.find('ProjectLookup').invoke('onChange')({
id: 2,
name: 'project',
@@ -172,6 +191,13 @@ describe('<JobTemplateAdd />', () => {
});
});
wrapper.update();
act(() => {
wrapper.find('InventoryLookup').invoke('onChange')({
id: 1,
organization: 1,
});
});
wrapper.update();
await act(async () => {
wrapper.find('form').simulate('submit');
});

View File

@@ -22,6 +22,7 @@ import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
import DeleteButton from '@components/DeleteButton';
import ErrorDetail from '@components/ErrorDetail';
import LaunchButton from '@components/LaunchButton';
import { VariablesDetail } from '@components/CodeMirrorInput';
import { JobTemplatesAPI } from '@api';
const MissingDetail = styled(Detail)`
@@ -38,6 +39,7 @@ function JobTemplateDetail({ i18n, template }) {
created,
description,
diff_mode,
extra_vars,
forks,
host_config_key,
job_slice_count,
@@ -302,6 +304,11 @@ function JobTemplateDetail({ i18n, template }) {
}
/>
)}
<VariablesDetail
value={extra_vars}
rows={4}
label={i18n._(t`Variables`)}
/>
</DetailList>
<CardActionsRow>
{summary_fields.user_capabilities &&

View File

@@ -11,9 +11,20 @@ jest.mock('@api');
const mockJobTemplate = {
allow_callbacks: false,
allow_simultaneous: false,
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,
become_enabled: false,
description: 'Bar',
diff_mode: false,
extra_vars: '---',
forks: 0,
host_config_key: '',
id: 1,
@@ -192,6 +203,7 @@ describe('<JobTemplateEdit />', () => {
);
});
const updatedTemplateData = {
job_type: 'check',
name: 'new name',
inventory: 1,
};
@@ -206,14 +218,18 @@ describe('<JobTemplateEdit />', () => {
wrapper.find('input#template-name').simulate('change', {
target: { value: 'new name', name: 'name' },
});
wrapper.find('AnsibleSelect#template-job-type').invoke('onChange')(
wrapper.find('AnsibleSelect#template-job-type').prop('onChange')(
null,
'check'
);
wrapper.find('LabelSelect').invoke('onChange')(labels);
});
wrapper.update();
act(() => {
wrapper.find('InventoryLookup').invoke('onChange')({
id: 1,
organization: 1,
});
wrapper.find('LabelSelect').invoke('onChange')(labels);
});
wrapper.update();
await act(async () => {
@@ -224,7 +240,6 @@ describe('<JobTemplateEdit />', () => {
const expected = {
...mockJobTemplate,
...updatedTemplateData,
become_enabled: false,
};
delete expected.summary_fields;
delete expected.id;

View File

@@ -27,7 +27,7 @@ import {
FormFullWidthLayout,
FormCheckboxLayout,
} from '@components/FormLayout';
import CollapsibleSection from '@components/CollapsibleSection';
import { VariablesField } from '@components/CodeMirrorInput';
import { required } from '@util/validators';
import { JobTemplate } from '@types';
import {
@@ -202,8 +202,6 @@ class JobTemplateForm extends Component {
return <ContentError error={contentError} />;
}
const AdvancedFieldsWrapper = template.isNew ? CollapsibleSection : 'div';
return (
<Form autoComplete="off" onSubmit={handleSubmit}>
<FormColumnLayout>
@@ -245,34 +243,57 @@ class JobTemplateForm extends Component {
id="template-job-type"
data={jobTypeOptions}
{...field}
onChange={(event, value) => {
form.setFieldValue('job_type', value);
}}
/>
);
}}
</Field>
</FieldWithPrompt>
<Field
name="inventory"
validate={required(i18n._(t`Select a value for this field`), i18n)}
>
{({ form }) => (
<InventoryLookup
value={inventory}
onBlur={() => form.setFieldTouched('inventory')}
<FieldWithPrompt
fieldId="template-inventory"
isRequired
label={i18n._(t`Inventory`)}
promptId="template-ask-inventory-on-launch"
promptName="ask_inventory_on_launch"
tooltip={i18n._(t`Select the inventory containing the hosts
you want this job to manage.`)}
isValid={!form.touched.inventory || !form.errors.inventory}
helperTextInvalid={form.errors.inventory}
>
<Field name="inventory">
{({ form }) => (
<>
<InventoryLookup
value={inventory}
onBlur={() => {
form.setFieldTouched('inventory');
}}
onChange={value => {
form.setFieldValue('inventory', value.id);
form.setFieldValue('organizationId', value.organization);
form.setValues({
...form.values,
inventory: value.id,
organizationId: value.organization,
});
this.setState({ inventory: value });
}}
required
touched={form.touched.inventory}
error={form.errors.inventory}
/>
{(form.touched.inventory ||
form.touched.ask_inventory_on_launch) &&
form.errors.inventory && (
<div
className="pf-c-form__helper-text pf-m-error"
aria-live="polite"
>
{form.errors.inventory}
</div>
)}
</>
)}
</Field>
</FieldWithPrompt>
<Field name="project" validate={this.handleProjectValidation()}>
{({ form }) => (
<ProjectLookup
@@ -288,12 +309,26 @@ class JobTemplateForm extends Component {
)}
</Field>
{project && project.allow_override && (
<FormField
id="scm_branch"
name="scm_branch"
type="text"
<FieldWithPrompt
fieldId="template-scm-branch"
label={i18n._(t`SCM Branch`)}
promptId="template-ask-scm-branch-on-launch"
promptName="ask_scm_branch_on_launch"
>
<Field name="scm_branch">
{({ field }) => {
return (
<TextInput
id="template-scm-branch"
{...field}
onChange={(value, event) => {
field.onChange(event);
}}
/>
);
}}
</Field>
</FieldWithPrompt>
)}
<Field
name="playbook"
@@ -328,6 +363,29 @@ class JobTemplateForm extends Component {
}}
</Field>
<FormFullWidthLayout>
<FieldWithPrompt
fieldId="template-credentials"
label={i18n._(t`Credentials`)}
promptId="template-ask-credential-on-launch"
promptName="ask_credential_on_launch"
tooltip={i18n._(
t`Select credentials that allow Tower to access the nodes this job will be ran against. You can only select one credential of each type. For machine credentials (SSH), checking "Prompt on launch" without selecting credentials will require you to select a machine credential at run time. If you select credentials and check "Prompt on launch", the selected credential(s) become the defaults that can be updated at run time.`
)}
>
<Field name="credentials" fieldId="template-credentials">
{({ field }) => {
return (
<MultiCredentialsLookup
value={field.value}
onChange={newCredentials =>
setFieldValue('credentials', newCredentials)
}
onError={this.setContentError}
/>
);
}}
</Field>
</FieldWithPrompt>
<Field name="labels">
{({ field }) => (
<FormGroup label={i18n._(t`Labels`)} fieldId="template-labels">
@@ -344,21 +402,12 @@ class JobTemplateForm extends Component {
</FormGroup>
)}
</Field>
<Field name="credentials" fieldId="template-credentials">
{({ field }) => (
<MultiCredentialsLookup
value={field.value}
onChange={newCredentials =>
setFieldValue('credentials', newCredentials)
}
onError={this.setContentError}
tooltip={i18n._(
t`Select credentials that allow Tower to access the nodes this job will be ran against. You can only select one credential of each type. For machine credentials (SSH), checking "Prompt on launch" without selecting credentials will require you to select a machine credential at run time. If you select credentials and check "Prompt on launch", the selected credential(s) become the defaults that can be updated at run time.`
)}
<VariablesField
id="template-variables"
name="extra_vars"
label={i18n._(t`Variables`)}
promptId="template-ask-variables-on-launch"
/>
)}
</Field>
<AdvancedFieldsWrapper label="Advanced">
<FormColumnLayout>
<FormField
id="template-forks"
@@ -379,34 +428,51 @@ class JobTemplateForm extends Component {
</span>
}
/>
<FormField
id="template-limit"
name="limit"
type="text"
<FieldWithPrompt
fieldId="template-limit"
label={i18n._(t`Limit`)}
promptId="template-ask-limit-on-launch"
promptName="ask_limit_on_launch"
tooltip={i18n._(t`Provide a host pattern to further constrain
the list of hosts that will be managed or affected by the
playbook. Multiple patterns are allowed. Refer to Ansible
documentation for more information and examples on patterns.`)}
>
<Field name="limit">
{({ form, field }) => {
return (
<TextInput
id="template-limit"
{...field}
isValid={
!form.touched.job_type || !form.errors.job_type
}
onChange={(value, event) => {
field.onChange(event);
}}
/>
<Field name="verbosity">
{({ field }) => (
<FormGroup
);
}}
</Field>
</FieldWithPrompt>
<FieldWithPrompt
fieldId="template-verbosity"
label={i18n._(t`Verbosity`)}
>
<FieldTooltip
content={i18n._(t`Control the level of output ansible will
promptId="template-ask-verbosity-on-launch"
promptName="ask_verbosity_on_launch"
tooltip={i18n._(t`Control the level of output ansible will
produce as the playbook executes.`)}
/>
>
<Field name="verbosity">
{({ field }) => (
<AnsibleSelect
id="template-verbosity"
data={verbosityOptions}
{...field}
/>
</FormGroup>
)}
</Field>
</FieldWithPrompt>
<FormField
id="template-job-slicing"
name="job_slice_count"
@@ -427,18 +493,18 @@ class JobTemplateForm extends Component {
before the task is canceled. Defaults to 0 for no job
timeout.`)}
/>
<Field name="diff_mode">
{({ field, form }) => (
<FormGroup
fieldId="template-show-changes"
<FieldWithPrompt
fieldId="template-diff-mode"
label={i18n._(t`Show Changes`)}
>
<FieldTooltip
content={i18n._(t`If enabled, show the changes made by
promptId="template-ask-diff-mode-on-launch"
promptName="ask_diff_mode_on_launch"
tooltip={i18n._(t`If enabled, show the changes made by
Ansible tasks, where supported. This is equivalent
to Ansible&#x2019s --diff mode.`)}
/>
<div>
>
<Field name="diff_mode">
{({ form, field }) => {
return (
<Switch
id="template-show-changes"
label={field.value ? i18n._(t`On`) : i18n._(t`Off`)}
@@ -447,67 +513,65 @@ class JobTemplateForm extends Component {
form.setFieldValue(field.name, checked)
}
/>
</div>
</FormGroup>
)}
);
}}
</Field>
</FieldWithPrompt>
<FormFullWidthLayout>
<Field name="instanceGroups">
{({ field, form }) => (
<InstanceGroupsLookup
value={field.value}
onChange={value =>
form.setFieldValue(field.name, value)
}
onChange={value => form.setFieldValue(field.name, value)}
tooltip={i18n._(t`Select the Instance Groups for this Organization
to run on.`)}
/>
)}
</Field>
<Field name="job_tags">
{({ field, form }) => (
<FormGroup
<FieldWithPrompt
fieldId="template-tags"
label={i18n._(t`Job Tags`)}
fieldId="template-job-tags"
>
<FieldTooltip
content={i18n._(t`Tags are useful when you have a large
promptId="template-ask-tags-on-launch"
promptName="ask_tags_on_launch"
tooltip={i18n._(t`Tags are useful when you have a large
playbook, and you want to run a specific part of a
play or task. Use commas to separate multiple tags.
Refer to Ansible Tower documentation for details on
the usage of tags.`)}
/>
>
<Field name="job_tags">
{({ field, form }) => (
<TagMultiSelect
value={field.value}
onChange={value =>
form.setFieldValue(field.name, value)
}
/>
</FormGroup>
)}
</Field>
<Field name="skip_tags">
{({ field, form }) => (
<FormGroup
label={i18n._(t`Skip Tags`)}
</FieldWithPrompt>
<FieldWithPrompt
fieldId="template-skip-tags"
>
<FieldTooltip
content={i18n._(t`Skip tags are useful when you have a
label={i18n._(t`Skip Tags`)}
promptId="template-ask-skip-tags-on-launch"
promptName="ask_skip_tags_on_launch"
tooltip={i18n._(t`Skip tags are useful when you have a
large playbook, and you want to skip specific parts of a
play or task. Use commas to separate multiple tags. Refer
to Ansible Tower documentation for details on the usage
of tags.`)}
/>
>
<Field name="skip_tags">
{({ field, form }) => (
<TagMultiSelect
value={field.value}
onChange={value =>
form.setFieldValue(field.name, value)
}
/>
</FormGroup>
)}
</Field>
</FieldWithPrompt>
<FormGroup
fieldId="template-option-checkboxes"
label={i18n._(t`Options`)}
@@ -580,7 +644,6 @@ class JobTemplateForm extends Component {
</>
)}
</FormColumnLayout>
</AdvancedFieldsWrapper>
</FormFullWidthLayout>
<FormSubmitError error={submitError} />
<FormActionGroup onCancel={handleCancel} onSubmit={handleSubmit} />
@@ -603,7 +666,16 @@ const FormikApp = withFormik({
? summary_fields.inventory.organization_id
: null;
return {
ask_credential_on_launch: template.ask_credential_on_launch || false,
ask_diff_mode_on_launch: template.ask_diff_mode_on_launch || false,
ask_inventory_on_launch: template.ask_inventory_on_launch || false,
ask_job_type_on_launch: template.ask_job_type_on_launch || false,
ask_limit_on_launch: template.ask_limit_on_launch || false,
ask_scm_branch_on_launch: template.ask_scm_branch_on_launch || false,
ask_skip_tags_on_launch: template.ask_skip_tags_on_launch || false,
ask_tags_on_launch: template.ask_tags_on_launch || false,
ask_variables_on_launch: template.ask_variables_on_launch || false,
ask_verbosity_on_launch: template.ask_verbosity_on_launch || false,
name: template.name || '',
description: template.description || '',
job_type: template.job_type || 'run',
@@ -629,6 +701,7 @@ const FormikApp = withFormik({
initialInstanceGroups: [],
instanceGroups: [],
credentials: summary_fields.credentials || [],
extra_vars: template.extra_vars || '---\n',
};
},
handleSubmit: async (values, { props, setErrors }) => {
@@ -638,6 +711,20 @@ const FormikApp = withFormik({
setErrors(errors);
}
},
validate: (values, { i18n }) => {
const errors = {};
if (
(!values.inventory || values.inventory === '') &&
!values.ask_inventory_on_launch
) {
errors.inventory = i18n._(
t`Please select an Inventory or check the Prompt on Launch option.`
);
}
return errors;
},
})(JobTemplateForm);
export { JobTemplateForm as _JobTemplateForm };

View File

@@ -140,13 +140,10 @@ describe('<JobTemplateForm />', () => {
wrapper.find('input#template-description').simulate('change', {
target: { value: 'new bar', name: 'description' },
});
wrapper.find('AnsibleSelect[name="job_type"]').simulate('change', {
target: { value: 'new job type', name: 'job_type' },
});
wrapper.find('InventoryLookup').invoke('onChange')({
id: 3,
name: 'inventory',
});
wrapper.find('AnsibleSelect#template-job-type').prop('onChange')(
null,
'check'
);
wrapper.find('ProjectLookup').invoke('onChange')({
id: 4,
name: 'project',
@@ -155,7 +152,14 @@ describe('<JobTemplateForm />', () => {
});
wrapper.update();
await act(async () => {
wrapper.find('input#scm_branch').simulate('change', {
wrapper.find('InventoryLookup').invoke('onChange')({
id: 3,
name: 'inventory',
});
});
wrapper.update();
await act(async () => {
wrapper.find('input#template-scm-branch').simulate('change', {
target: { value: 'devel', name: 'scm_branch' },
});
wrapper.find('AnsibleSelect[name="playbook"]').simulate('change', {
@@ -179,7 +183,7 @@ describe('<JobTemplateForm />', () => {
);
expect(
wrapper.find('AnsibleSelect[name="job_type"]').prop('value')
).toEqual('new job type');
).toEqual('check');
expect(wrapper.find('InventoryLookup').prop('value')).toEqual({
id: 3,
name: 'inventory',
@@ -189,7 +193,9 @@ describe('<JobTemplateForm />', () => {
name: 'project',
allow_override: true,
});
expect(wrapper.find('input#scm_branch').prop('value')).toEqual('devel');
expect(wrapper.find('input#template-scm-branch').prop('value')).toEqual(
'devel'
);
expect(
wrapper.find('AnsibleSelect[name="playbook"]').prop('value')
).toEqual('new baz type');