From 05820796063de6f2522b4ce35b0b526c01e6db3c Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 19 Feb 2020 15:23:11 -0500 Subject: [PATCH 1/3] 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'); From 0e663921d686772d7d613f543b779f3cc3227ec4 Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 19 Feb 2020 15:50:12 -0500 Subject: [PATCH 2/3] Removes rogue comment and marks JT inventory validation string for translation. --- awx/ui_next/src/components/Lookup/InventoryLookup.jsx | 2 -- .../src/screens/Template/shared/JobTemplateForm.jsx | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/awx/ui_next/src/components/Lookup/InventoryLookup.jsx b/awx/ui_next/src/components/Lookup/InventoryLookup.jsx index eacb269dd9..c80b0040fe 100644 --- a/awx/ui_next/src/components/Lookup/InventoryLookup.jsx +++ b/awx/ui_next/src/components/Lookup/InventoryLookup.jsx @@ -17,8 +17,6 @@ const QS_CONFIG = getQSConfig('inventory', { }); 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); diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index fd8a81d4ba..c6a2c3b594 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -1370,15 +1370,16 @@ const FormikApp = withFormik({ setErrors(errors); } }, - validate: values => { + validate: (values, { i18n }) => { 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.'; + errors.inventory = i18n._( + t`Please select an Inventory or check the Prompt on Launch option.` + ); } return errors; From e80e3f7410409adee64957442eae9e8b2bf4874d Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 26 Feb 2020 16:59:45 -0500 Subject: [PATCH 3/3] Reapply prompt on launch for job template fields after rebasing. --- .../CodeMirrorInput/VariablesField.jsx | 131 +- .../Lookup/MultiCredentialsLookup.jsx | 206 +-- .../src/screens/Host/shared/HostForm.jsx | 186 +-- .../Template/shared/JobTemplateForm.jsx | 1218 ++++------------- 4 files changed, 341 insertions(+), 1400 deletions(-) diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx index 46a502df8c..336a3fa996 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx @@ -1,82 +1,12 @@ import React, { useState } from 'react'; import { string, bool } from 'prop-types'; -import { useField } from 'formik'; -import { Split, SplitItem } from '@patternfly/react-core'; -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 [field, meta, helpers] = useField(name); - const [mode, setMode] = useState(isJson(field.value) ? JSON_MODE : YAML_MODE); - - return ( - <> - - - - - - { - try { - const newVal = - newMode === YAML_MODE - ? jsonToYaml(field.value) - : yamlToJson(field.value); - helpers.setValue(newVal); - setMode(newMode); - } catch (err) { - helpers.setError(err.message); - } - }} - /> - - - { - helpers.setValue(newVal); - }} - hasErrors={!!meta.error} - /> - {meta.error ? ( -
- {meta.error} -
- ) : null} - - ); -} -VariablesField.propTypes = { - id: string.isRequired, - name: string.isRequired, - label: string.isRequired, - readOnly: bool, -}; -VariablesField.defaultProps = { - readOnly: false, -}; - -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 { 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'; @@ -91,9 +21,8 @@ const StyledCheckboxField = styled(CheckboxField)` `; 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); + const [field, meta, helpers] = useField(name); + const [mode, setMode] = useState(isJson(field.value) ? JSON_MODE : YAML_MODE); return (
@@ -111,12 +40,12 @@ function VariablesField({ i18n, id, name, label, readOnly, promptId }) { try { const newVal = newMode === YAML_MODE - ? jsonToYaml(value) - : yamlToJson(value); - setFieldValue(name, newVal); + ? jsonToYaml(field.value) + : yamlToJson(field.value); + helpers.setValue(newVal); setMode(newMode); } catch (err) { - setFieldError(name, err.message); + helpers.setError(err.message); } }} /> @@ -130,29 +59,20 @@ function VariablesField({ i18n, id, name, label, readOnly, promptId }) { /> )} - - {({ field, form }) => ( - <> - { - form.setFieldValue(name, newVal); - }} - hasErrors={!!form.errors[field.name]} - /> - {form.errors[field.name] ? ( -
- {form.errors[field.name]} -
- ) : null} - - )} -
+ { + helpers.setValue(newVal); + }} + hasErrors={!!meta.error} + /> + {meta.error ? ( +
+ {meta.error} +
+ ) : null}
); } @@ -161,10 +81,11 @@ VariablesField.propTypes = { name: string.isRequired, label: string.isRequired, readOnly: bool, + promptId: string, }; VariablesField.defaultProps = { readOnly: false, + promptId: null, }; export default withI18n()(VariablesField); -*/ diff --git a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx index 12d660ffef..9a24316c74 100644 --- a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx @@ -3,210 +3,10 @@ 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 { 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'; -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 { tooltip, 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 ( - - {tooltip && } - { - 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 = { - tooltip: PropTypes.string, - 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 = { - tooltip: '', - value: [], -}; - -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'; @@ -293,8 +93,9 @@ function MultiCredentialsLookup(props) { {credentialTypes && credentialTypes.length > 0 && ( -
{i18n._(t`Selected Category`)}
- +
+ {i18n._(t`Selected Category`)} +
{hostAddMatch && ( - inventoryHelpers.setTouched()} - tooltip={i18n._( - t`Select the inventory that this host will belong to.` - )} + { - inventoryHelpers.setValuealue(value.id); - setInventory(value); - }} - required - touched={inventoryMeta.touched} - error={inventoryMeta.error} - /> + > + + inventoryHelpers.setTouched()} + tooltip={i18n._( + t`Select the inventory that this host will belong to.` + )} + isValid={!inventoryMeta.touched || !inventoryMeta.error} + helperTextInvalid={inventoryMeta.error} + onChange={value => { + inventoryHelpers.setValue(value.id); + setInventory(value); + }} + required + touched={inventoryMeta.touched} + error={inventoryMeta.error} + /> + )} - {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/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index c6a2c3b594..f9f68507cb 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -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,667 +202,9 @@ class JobTemplateForm extends Component { return ; } - const AdvancedFieldsWrapper = template.isNew ? CollapsibleSection : 'div'; - return (
- - - - - {({ form, field }) => { - const isValid = !form.touched.job_type || !form.errors.job_type; - return ( - - ); - }} - - - - {({ form }) => ( - form.setFieldTouched('inventory')} - 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} - onChange={value => { - form.setFieldValue('inventory', value.id); - form.setFieldValue('organizationId', value.organization); - this.setState({ inventory: value }); - }} - required - touched={form.touched.inventory} - error={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, form }) => { - const isValid = !form.touched.playbook || !form.errors.playbook; - return ( - - - form.setFieldTouched('playbook')} - onError={this.setContentError} - /> - - ); - }} - - - - {({ field }) => ( - - - setFieldValue('labels', labels)} - onError={this.setContentError} - /> - - )} - - - {({ field }) => ( - - 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.` - )} - /> - )} - - - - - {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.`)} - - } - /> - - - {({ field }) => ( - - - - - )} - - - - - {({ field, form }) => ( - - -
- - 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 }); - }} - /> - - - - - - {allowCallbacks && ( - <> - {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_job_type_on_launch: template.ask_job_type_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 || [], - }; - }, - handleSubmit: async (values, { props, setErrors }) => { - try { - await props.handleSubmit(values); - } catch (errors) { - setErrors(errors); - } - }, -})(JobTemplateForm); - -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 ( -
- - - - - {({ field }) => ( - - - setFieldValue('labels', labels)} - onError={this.setContentError} - /> - - )} - - - - - - {({ field }) => { - return ( - + + + {({ field }) => { + return ( + + setFieldValue('credentials', newCredentials) + } + onError={this.setContentError} + /> + ); + }} + + + + {({ field }) => ( + + + - setFieldValue('credentials', newCredentials) - } + onChange={labels => setFieldValue('labels', labels)} 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)} + + + {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.`)} + + } /> - )} - - - - - {({ field, form }) => ( - form.setFieldValue(field.name, value)} - /> - )} - - - - - - {i18n._(t`Provisioning Callbacks`)} -   - - - } - id="option-callbacks" - isChecked={allowCallbacks} - onChange={checked => { - this.setState({ allowCallbacks: checked }); - }} - /> - - - -
- - {callbackUrl && ( - - - - )} - - -
- - - - - + + {({ 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 }); + }} + /> + + + + + + {allowCallbacks && ( + <> + {callbackUrl && ( + + + + )} + + + )} + + + + + ); } @@ -1388,4 +729,3 @@ const FormikApp = withFormik({ export { JobTemplateForm as _JobTemplateForm }; export default withI18n()(withRouter(FormikApp)); -*/