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