From 05820796063de6f2522b4ce35b0b526c01e6db3c Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 19 Feb 2020 15:23:11 -0500 Subject: [PATCH] Adds prompt on launch support to the rest of the relevant fields in the Job template form. Adds extra variables field to the job template form. Removes the advanced section in favor of a straight form. --- .../CodeMirrorInput/VariablesField.jsx | 103 +++ .../src/components/Lookup/InventoryLookup.jsx | 31 +- .../Lookup/MultiCredentialsLookup.jsx | 194 +++++ .../src/screens/Host/shared/HostForm.jsx | 136 ++++ .../JobTemplateAdd/JobTemplateAdd.test.jsx | 62 +- .../JobTemplateDetail/JobTemplateDetail.jsx | 7 + .../JobTemplateEdit/JobTemplateEdit.test.jsx | 21 +- .../Template/shared/JobTemplateForm.jsx | 746 ++++++++++++++++++ .../Template/shared/JobTemplateForm.test.jsx | 26 +- 9 files changed, 1270 insertions(+), 56 deletions(-) diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx index 91818e84c7..46a502df8c 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx @@ -65,3 +65,106 @@ VariablesField.defaultProps = { }; export default VariablesField; + + +/* +import React, { useState } from 'react'; +import { string, bool } from 'prop-types'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Field, useFormikContext } from 'formik'; +import { Split, SplitItem } from '@patternfly/react-core'; +import { yamlToJson, jsonToYaml, isJson } from '@util/yaml'; +import { CheckboxField } from '@components/FormField'; +import styled from 'styled-components'; +import CodeMirrorInput from './CodeMirrorInput'; +import YamlJsonToggle from './YamlJsonToggle'; +import { JSON_MODE, YAML_MODE } from './constants'; + +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 { values, setFieldError, setFieldValue } = useFormikContext(); + const value = values[name]; + const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE); + + return ( +
+ + + + + + + { + try { + const newVal = + newMode === YAML_MODE + ? jsonToYaml(value) + : yamlToJson(value); + setFieldValue(name, newVal); + setMode(newMode); + } catch (err) { + setFieldError(name, err.message); + } + }} + /> + + + {promptId && ( + + )} + + + {({ field, form }) => ( + <> + { + form.setFieldValue(name, newVal); + }} + hasErrors={!!form.errors[field.name]} + /> + {form.errors[field.name] ? ( +
+ {form.errors[field.name]} +
+ ) : null} + + )} +
+
+ ); +} +VariablesField.propTypes = { + id: string.isRequired, + name: string.isRequired, + label: string.isRequired, + readOnly: bool, +}; +VariablesField.defaultProps = { + readOnly: false, +}; + +export default withI18n()(VariablesField); +*/ diff --git a/awx/ui_next/src/components/Lookup/InventoryLookup.jsx b/awx/ui_next/src/components/Lookup/InventoryLookup.jsx index 938ab80273..eacb269dd9 100644 --- a/awx/ui_next/src/components/Lookup/InventoryLookup.jsx +++ b/awx/ui_next/src/components/Lookup/InventoryLookup.jsx @@ -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,9 @@ 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 }) { + // some stuff was stripped out of this component - need to propagate those changes + // out to other forms that use this lookup const [inventories, setInventories] = useState([]); const [count, setCount] = useState(0); const [error, setError] = useState(null); @@ -47,14 +37,7 @@ function InventoryLookup({ }, [history.location]); return ( - - {tooltip && } + <> - + ); } InventoryLookup.propTypes = { value: Inventory, - tooltip: string, onChange: func.isRequired, required: bool, }; InventoryLookup.defaultProps = { value: null, - tooltip: '', required: false, }; diff --git a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx index cf05f48e89..12d660ffef 100644 --- a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx @@ -194,3 +194,197 @@ MultiCredentialsLookup.defaultProps = { export { MultiCredentialsLookup as _MultiCredentialsLookup }; export default withI18n()(withRouter(MultiCredentialsLookup)); + + +/* +import React, { Fragment, useState, useEffect } from 'react'; +import { withRouter } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { ToolbarItem } from '@patternfly/react-core'; +import { CredentialsAPI, CredentialTypesAPI } from '@api'; +import AnsibleSelect from '@components/AnsibleSelect'; +import CredentialChip from '@components/CredentialChip'; +import VerticalSeperator from '@components/VerticalSeparator'; +import { getQSConfig, parseQueryString } from '@util/qs'; +import Lookup from './Lookup'; +import OptionsList from './shared/OptionsList'; + +const QS_CONFIG = getQSConfig('credentials', { + page: 1, + page_size: 5, + order_by: 'name', +}); + +async function loadCredentialTypes() { + const { data } = await CredentialTypesAPI.read(); + const acceptableTypes = ['machine', 'cloud', 'net', 'ssh', 'vault']; + return data.results.filter(type => acceptableTypes.includes(type.kind)); +} + +async function loadCredentials(params, selectedCredentialTypeId) { + params.credential_type = selectedCredentialTypeId || 1; + const { data } = await CredentialsAPI.read(params); + return data; +} + +function MultiCredentialsLookup(props) { + const { value, onChange, onError, history, i18n } = props; + const [credentialTypes, setCredentialTypes] = useState([]); + const [selectedType, setSelectedType] = useState(null); + const [credentials, setCredentials] = useState([]); + const [credentialsCount, setCredentialsCount] = useState(0); + + useEffect(() => { + (async () => { + try { + const types = await loadCredentialTypes(); + setCredentialTypes(types); + const match = types.find(type => type.kind === 'ssh') || types[0]; + setSelectedType(match); + } catch (err) { + onError(err); + } + })(); + }, [onError]); + + useEffect(() => { + (async () => { + if (!selectedType) { + return; + } + try { + const params = parseQueryString(QS_CONFIG, history.location.search); + const { results, count } = await loadCredentials( + params, + selectedType.id + ); + setCredentials(results); + setCredentialsCount(count); + } catch (err) { + onError(err); + } + })(); + }, [selectedType, history.location.search, onError]); + + const renderChip = ({ item, removeItem, canDelete }) => ( + removeItem(item)} + isReadOnly={!canDelete} + credential={item} + /> + ); + + const isMultiple = selectedType && selectedType.kind === 'vault'; + + return ( + { + return ( + + {credentialTypes && credentialTypes.length > 0 && ( + +
{i18n._(t`Selected Category`)}
+ + ({ + key: type.id, + value: type.id, + label: type.name, + isDisabled: false, + }))} + value={selectedType && selectedType.id} + onChange={(e, id) => { + setSelectedType( + credentialTypes.find(o => o.id === parseInt(id, 10)) + ); + }} + /> +
+ )} + { + if (isMultiple) { + return dispatch({ type: 'SELECT_ITEM', item }); + } + const selectedItems = state.selectedItems.filter( + i => i.kind !== item.kind + ); + selectedItems.push(item); + return dispatch({ + type: 'SET_SELECTED_ITEMS', + selectedItems, + }); + }} + deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })} + renderItemChip={renderChip} + /> +
+ ); + }} + /> + ); +} + +MultiCredentialsLookup.propTypes = { + value: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number, + name: PropTypes.string, + description: PropTypes.string, + kind: PropTypes.string, + clound: PropTypes.bool, + }) + ), + onChange: PropTypes.func.isRequired, + onError: PropTypes.func.isRequired, +}; + +MultiCredentialsLookup.defaultProps = { + value: [], +}; + +export { MultiCredentialsLookup as _MultiCredentialsLookup }; +export default withI18n()(withRouter(MultiCredentialsLookup)); +*/ diff --git a/awx/ui_next/src/screens/Host/shared/HostForm.jsx b/awx/ui_next/src/screens/Host/shared/HostForm.jsx index af5fb0a2ec..de7b22101d 100644 --- a/awx/ui_next/src/screens/Host/shared/HostForm.jsx +++ b/awx/ui_next/src/screens/Host/shared/HostForm.jsx @@ -122,3 +122,139 @@ HostForm.defaultProps = { export { HostForm as _HostForm }; export default withI18n()(HostForm); + + + +/* +import React, { useState } from 'react'; +import { func, shape } from 'prop-types'; + +import { useRouteMatch } from 'react-router-dom'; +import { Formik, Field } from 'formik'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; + +import { Form, FormGroup } from '@patternfly/react-core'; + +import FormRow from '@components/FormRow'; +import FormField, { + FormSubmitError, + FieldTooltip, +} from '@components/FormField'; +import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; +import { VariablesField } from '@components/CodeMirrorInput'; +import { required } from '@util/validators'; +import { InventoryLookup } from '@components/Lookup'; + +function HostForm({ handleSubmit, handleCancel, host, submitError, i18n }) { + const [inventory, setInventory] = useState( + host ? host.summary_fields.inventory : '' + ); + + const hostAddMatch = useRouteMatch('/hosts/add'); + + return ( + + {formik => ( +
+ + + + {hostAddMatch && ( + + {({ form }) => ( + + + form.setFieldTouched('inventory')} + onChange={value => { + form.setFieldValue('inventory', value.id); + setInventory(value); + }} + required + touched={form.touched.inventory} + error={form.errors.inventory} + /> + + )} + + )} + + + + + + + + )} +
+ ); +} + +HostForm.propTypes = { + handleSubmit: func.isRequired, + handleCancel: func.isRequired, + host: shape({}), + submitError: shape({}), +}; + +HostForm.defaultProps = { + host: { + name: '', + description: '', + inventory: undefined, + variables: '---\n', + summary_fields: { + inventory: null, + }, + }, + submitError: null, +}; + +export { HostForm as _HostForm }; +export default withI18n()(HostForm); +*/ diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx index 748b71dbcf..84847af506 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx @@ -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('', () => { 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,18 +129,28 @@ describe('', () => { .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(); await act(async () => { wrapper.find('form').simulate('submit'); }); 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('', () => { 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('', () => { }); }); wrapper.update(); + act(() => { + wrapper.find('InventoryLookup').invoke('onChange')({ + id: 1, + organization: 1, + }); + }); + wrapper.update(); await act(async () => { wrapper.find('form').simulate('submit'); }); diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx index 999af636e2..0b8a118f4a 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx @@ -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 }) { } /> )} + {summary_fields.user_capabilities && diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx index 8798249169..92c47ecf05 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx @@ -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('', () => { ); }); const updatedTemplateData = { + job_type: 'check', name: 'new name', inventory: 1, }; @@ -206,14 +218,18 @@ describe('', () => { 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('', () => { const expected = { ...mockJobTemplate, ...updatedTemplateData, - become_enabled: false, }; delete expected.summary_fields; delete expected.id; diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index 82ceeb3313..fd8a81d4ba 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -642,3 +642,749 @@ const FormikApp = withFormik({ export { JobTemplateForm as _JobTemplateForm }; export default withI18n()(withRouter(FormikApp)); + + + + + +/* +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withRouter } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { withFormik, Field } from 'formik'; +import { + Form, + FormGroup, + Switch, + Checkbox, + TextInput, +} from '@patternfly/react-core'; +import ContentError from '@components/ContentError'; +import ContentLoading from '@components/ContentLoading'; +import AnsibleSelect from '@components/AnsibleSelect'; +import { TagMultiSelect } from '@components/MultiSelect'; +import FormActionGroup from '@components/FormActionGroup'; +import FormField, { + CheckboxField, + FieldTooltip, + FormSubmitError, +} from '@components/FormField'; +import FieldWithPrompt from '@components/FieldWithPrompt'; +import FormRow from '@components/FormRow'; +import { required } from '@util/validators'; +import styled from 'styled-components'; +import { JobTemplate } from '@types'; +import { + InventoryLookup, + InstanceGroupsLookup, + ProjectLookup, + MultiCredentialsLookup, +} from '@components/Lookup'; +import { VariablesField } from '@components/CodeMirrorInput'; +import { JobTemplatesAPI, ProjectsAPI } from '@api'; +import LabelSelect from './LabelSelect'; +import PlaybookSelect from './PlaybookSelect'; + +const GridFormGroup = styled(FormGroup)` + & > label { + grid-column: 1 / -1; + } + + && { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + } +`; + +class JobTemplateForm extends Component { + static propTypes = { + template: JobTemplate, + handleCancel: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, + submitError: PropTypes.shape({}), + }; + + static defaultProps = { + template: { + name: '', + description: '', + job_type: 'run', + inventory: undefined, + project: undefined, + playbook: '', + summary_fields: { + inventory: null, + labels: { results: [] }, + project: null, + credentials: [], + }, + isNew: true, + }, + submitError: null, + }; + + constructor(props) { + super(props); + this.state = { + hasContentLoading: true, + contentError: false, + project: props.template.summary_fields.project, + inventory: props.template.summary_fields.inventory, + allowCallbacks: !!props.template.host_config_key, + }; + this.handleProjectValidation = this.handleProjectValidation.bind(this); + this.loadRelatedInstanceGroups = this.loadRelatedInstanceGroups.bind(this); + this.handleProjectUpdate = this.handleProjectUpdate.bind(this); + this.setContentError = this.setContentError.bind(this); + this.fetchProject = this.fetchProject.bind(this); + } + + componentDidMount() { + const { validateField } = this.props; + this.setState({ contentError: null, hasContentLoading: true }); + // TODO: determine when LabelSelect has finished loading labels + Promise.all([this.loadRelatedInstanceGroups(), this.fetchProject()]).then( + () => { + this.setState({ hasContentLoading: false }); + validateField('project'); + } + ); + } + + async fetchProject() { + const { project } = this.state; + if (project && project.id) { + try { + const { data: projectData } = await ProjectsAPI.readDetail(project.id); + this.setState({ project: projectData }); + } catch (err) { + this.setState({ contentError: err }); + } + } + } + + async loadRelatedInstanceGroups() { + const { setFieldValue, template } = this.props; + if (!template.id) { + return; + } + try { + const { data } = await JobTemplatesAPI.readInstanceGroups(template.id); + setFieldValue('initialInstanceGroups', data.results); + setFieldValue('instanceGroups', [...data.results]); + } catch (err) { + this.setState({ contentError: err }); + } + } + + handleProjectValidation() { + const { i18n, touched } = this.props; + const { project } = this.state; + return () => { + if (!project && touched.project) { + return i18n._(t`Select a value for this field`); + } + if (project && project.status === 'never updated') { + return i18n._(t`This project needs to be updated`); + } + return undefined; + }; + } + + handleProjectUpdate(project) { + const { setFieldValue } = this.props; + setFieldValue('project', project.id); + setFieldValue('playbook', 0); + setFieldValue('scm_branch', ''); + this.setState({ project }); + } + + setContentError(contentError) { + this.setState({ contentError }); + } + + render() { + const { + contentError, + hasContentLoading, + inventory, + project, + allowCallbacks, + } = this.state; + const { + handleCancel, + handleSubmit, + handleBlur, + setFieldValue, + template, + submitError, + i18n, + } = this.props; + + const jobTypeOptions = [ + { + value: '', + key: '', + label: i18n._(t`Choose a job type`), + isDisabled: true, + }, + { value: 'run', key: 'run', label: i18n._(t`Run`), isDisabled: false }, + { + value: 'check', + key: 'check', + label: i18n._(t`Check`), + isDisabled: false, + }, + ]; + const verbosityOptions = [ + { value: '0', key: '0', label: i18n._(t`0 (Normal)`) }, + { value: '1', key: '1', label: i18n._(t`1 (Verbose)`) }, + { value: '2', key: '2', label: i18n._(t`2 (More Verbose)`) }, + { value: '3', key: '3', label: i18n._(t`3 (Debug)`) }, + { value: '4', key: '4', label: i18n._(t`4 (Connection Debug)`) }, + ]; + let callbackUrl; + if (template && template.related) { + const { origin } = document.location; + const path = template.related.callback || `${template.url}callback`; + callbackUrl = `${origin}${path}`; + } + + if (hasContentLoading) { + return ; + } + + if (contentError) { + return ; + } + + return ( +
+ + + + + + {({ form, field }) => { + const isValid = !form.touched.job_type || !form.errors.job_type; + return ( + { + form.setFieldValue('job_type', value); + }} + /> + ); + }} + + + + + {({ form }) => ( + <> + { + form.setFieldTouched('inventory'); + }} + onChange={value => { + 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 && ( +
+ {form.errors.inventory} +
+ )} + + )} +
+
+ + {({ form }) => ( + form.setFieldTouched('project')} + tooltip={i18n._(t`Select the project containing the playbook + you want this job to execute.`)} + isValid={!form.touched.project || !form.errors.project} + helperTextInvalid={form.errors.project} + onChange={this.handleProjectUpdate} + required + /> + )} + + {project && project.allow_override && ( + + + {({ field }) => { + return ( + { + field.onChange(event); + }} + /> + ); + }} + + + )} + + {({ field, form }) => { + const isValid = !form.touched.playbook || !form.errors.playbook; + return ( + + + form.setFieldTouched('playbook')} + onError={this.setContentError} + /> + + ); + }} + +
+ + + {({ field }) => ( + + + setFieldValue('labels', labels)} + onError={this.setContentError} + /> + + )} + + + + + + {({ field }) => { + return ( + + setFieldValue('credentials', newCredentials) + } + onError={this.setContentError} + /> + ); + }} + + + + + + {i18n._(t`The number of parallel or simultaneous + processes to use while executing the playbook. An empty value, + or a value less than 1 will use the Ansible default which is + usually 5. The default number of forks can be overwritten + with a change to`)}{' '} + ansible.cfg.{' '} + {i18n._(t`Refer to the Ansible documentation for details + about the configuration file.`)} + + } + /> + + + {({ form, field }) => { + return ( + { + field.onChange(event); + }} + /> + ); + }} + + + + + {({ field }) => ( + + )} + + + + + + + {({ form, field }) => { + return ( + + form.setFieldValue(field.name, checked) + } + /> + ); + }} + + + + + {({ field, form }) => ( + form.setFieldValue(field.name, value)} + tooltip={i18n._(t`Select the Instance Groups for this Organization + to run on.`)} + /> + )} + + + + {({ field, form }) => ( + form.setFieldValue(field.name, value)} + /> + )} + + + + + {({ field, form }) => ( + form.setFieldValue(field.name, value)} + /> + )} + + + + + + {i18n._(t`Provisioning Callbacks`)} +   + + + } + id="option-callbacks" + isChecked={allowCallbacks} + onChange={checked => { + this.setState({ allowCallbacks: checked }); + }} + /> + + + +
+ + {callbackUrl && ( + + + + )} + + +
+ + + + + + + ); + } +} + +const FormikApp = withFormik({ + mapPropsToValues(props) { + const { template = {} } = props; + const { + summary_fields = { + labels: { results: [] }, + inventory: { organization: null }, + }, + } = template; + const hasInventory = summary_fields.inventory + ? 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', + inventory: template.inventory || '', + project: template.project || '', + scm_branch: template.scm_branch || '', + playbook: template.playbook || '', + labels: summary_fields.labels.results || [], + forks: template.forks || 0, + limit: template.limit || '', + verbosity: template.verbosity || '0', + job_slice_count: template.job_slice_count || 1, + timeout: template.timeout || 0, + diff_mode: template.diff_mode || false, + job_tags: template.job_tags || '', + skip_tags: template.skip_tags || '', + become_enabled: template.become_enabled || false, + allow_callbacks: template.allow_callbacks || false, + allow_simultaneous: template.allow_simultaneous || false, + use_fact_cache: template.use_fact_cache || false, + host_config_key: template.host_config_key || '', + organizationId: hasInventory, + initialInstanceGroups: [], + instanceGroups: [], + credentials: summary_fields.credentials || [], + extra_vars: template.extra_vars || '---\n', + }; + }, + handleSubmit: async (values, { props, setErrors }) => { + try { + await props.handleSubmit(values); + } catch (errors) { + setErrors(errors); + } + }, + validate: values => { + const errors = {}; + + if ( + (!values.inventory || values.inventory === '') && + !values.ask_inventory_on_launch + ) { + errors.inventory = + 'Please select an Inventory or check the Prompt on Launch option.'; + } + + return errors; + }, +})(JobTemplateForm); + +export { JobTemplateForm as _JobTemplateForm }; +export default withI18n()(withRouter(FormikApp)); +*/ diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx index 95a02c7edb..338af6f714 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx @@ -140,13 +140,10 @@ describe('', () => { 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('', () => { }); 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('', () => { ); 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('', () => { 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');