From e87f804c92ae7b36087eb640e4b2404a9a047250 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Tue, 17 Mar 2020 16:21:06 -0400 Subject: [PATCH] Moves JT Form to using react hooks and custom hooks --- .../Template/shared/JobTemplateForm.jsx | 1055 ++++++++--------- .../Template/shared/JobTemplateForm.test.jsx | 1 + 2 files changed, 512 insertions(+), 544 deletions(-) diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index 19ca4d3467..0640dab906 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { withRouter } from 'react-router-dom'; import { withI18n } from '@lingui/react'; @@ -15,6 +15,8 @@ import ContentError from '@components/ContentError'; import ContentLoading from '@components/ContentLoading'; import AnsibleSelect from '@components/AnsibleSelect'; import { TagMultiSelect } from '@components/MultiSelect'; +import useRequest from '@util/useRequest'; + import FormActionGroup from '@components/FormActionGroup'; import FormField, { CheckboxField, @@ -40,287 +42,362 @@ import { JobTemplatesAPI, ProjectsAPI } from '@api'; import LabelSelect from './LabelSelect'; import PlaybookSelect from './PlaybookSelect'; -class JobTemplateForm extends Component { - static propTypes = { - template: JobTemplate, - handleCancel: PropTypes.func.isRequired, - handleSubmit: PropTypes.func.isRequired, - submitError: PropTypes.shape({}), - }; +function JobTemplateForm({ + template, + validateField, + handleCancel, + handleSubmit, + handleBlur, + setFieldValue, + submitError, + i18n, + touched, +}) { + const [contentError, setContentError] = useState(false); + const [project, setProject] = useState(null); + const [inventory, setInventory] = useState( + template?.summary_fields?.inventory + ); + const [allowCallbacks, setAllowCallbacks] = useState( + Boolean(template?.host_config_key) + ); - 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 }); + const { + request: fetchProject, + error: projectContentError, + contentLoading: hasProjectLoading, + } = useRequest( + useCallback(async () => { + let projectData; + if (template?.project) { + projectData = await ProjectsAPI.readDetail(template?.project); validateField('project'); + setProject(projectData.data); } - ); - } - - 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 }); + }, [template, validateField]) + ); + const { + request: loadRelatedInstanceGroups, + error: instanceGroupError, + contentLoading: instanceGroupLoading, + } = useRequest( + useCallback(async () => { + if (!template?.id) { + return; } - } - } - - 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 }); + }, [setFieldValue, template]) + ); + + useEffect(() => { + fetchProject(); + }, [fetchProject]); + + useEffect(() => { + loadRelatedInstanceGroups(); + }, [loadRelatedInstanceGroups]); + + const handleProjectValidation = () => { + 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; + }; - 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); + const handleProjectUpdate = newProject => { + setProject(newProject); + setFieldValue('project', newProject.id); setFieldValue('playbook', 0); setFieldValue('scm_branch', ''); - this.setState({ project }); + }; + + 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?.related) { + const { origin } = document.location; + const path = template.related.callback || `${template.url}callback`; + callbackUrl = `${origin}${path}`; } - setContentError(contentError) { - this.setState({ contentError }); + if (instanceGroupLoading || hasProjectLoading) { + return ; } - render() { - const { - contentError, - hasContentLoading, - inventory, - project, - allowCallbacks, - } = this.state; - const { - handleCancel, - handleSubmit, - handleBlur, - setFieldValue, - template, - submitError, - i18n, - } = this.props; + if (instanceGroupError || projectContentError) { + return ; + } - 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; + {({ 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, + }); + setInventory(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} +
+ )} + + )} +
+
+ handleProjectValidation()}> + {({ 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={handleProjectUpdate} + required + /> + )} + + {project && project.allow_override && ( + + + {({ field }) => { return ( - { - form.setFieldValue('job_type', value); + onChange={(value, event) => { + field.onChange(event); }} /> ); }} + )} + + {({ field, form }) => { + const isValid = !form.touched.playbook || !form.errors.playbook; + return ( + + + form.setFieldTouched('playbook')} + onError={setContentError} + /> + + ); + }} + + - - {({ 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} + + {({ field }) => { + return ( + + setFieldValue('credentials', newCredentials) + } + onError={setContentError} /> - {(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 - /> + + {({ field }) => ( + + + setFieldValue('labels', labels)} + onError={setContentError} + /> + )} - {project && project.allow_override && ( + + + + {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, field }) => { return ( { field.onChange(event); }} @@ -329,335 +406,225 @@ class JobTemplateForm extends Component { }} - )} - - {({ field, form }) => { - const isValid = !form.touched.playbook || !form.errors.playbook; - return ( - - - form.setFieldTouched('playbook')} - onError={this.setContentError} - /> - - ); - }} - - - - {({ field }) => { + + {({ field }) => ( + + )} + + + + + + + {({ form, field }) => { return ( - - setFieldValue('credentials', newCredentials) + + form.setFieldValue(field.name, checked) } - onError={this.setContentError} /> ); }} - - {({ field }) => ( - - - + + {({ field, form }) => ( + 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 + onChange={value => 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)} + /> + )} + + + - - {({ field, form }) => ( - - form.setFieldValue(field.name, value) - } - /> - )} - - - - - + + {({ field, form }) => ( + form.setFieldValue(field.name, value)} /> - - {i18n._(t`Provisioning Callbacks`)} -   - +
+ + + + + {i18n._(t`Provisioning Callbacks`)} +   + - - } - id="option-callbacks" - isChecked={allowCallbacks} - onChange={checked => { - this.setState({ allowCallbacks: checked }); - }} - /> - - - - - - {allowCallbacks && ( - <> - {callbackUrl && ( - - - - )} - + + } + id="option-callbacks" + isChecked={allowCallbacks} + onChange={checked => { + setAllowCallbacks(checked); + }} /> - - )} -
- - - - - - ); - } + + + + + + {allowCallbacks && ( + <> + {callbackUrl && ( + + + + )} + + + )} + + + + + + + ); } +JobTemplateForm.propTypes = { + template: JobTemplate, + handleCancel: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, + submitError: PropTypes.shape({}), +}; +JobTemplateForm.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, +}; const FormikApp = withFormik({ - mapPropsToValues(props) { - const { template = {} } = props; + mapPropsToValues({ template = {} }) { const { summary_fields = { labels: { results: [] }, 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 338af6f714..383bef39f0 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx @@ -157,6 +157,7 @@ describe('', () => { name: 'inventory', }); }); + wrapper.update(); await act(async () => { wrapper.find('input#template-scm-branch').simulate('change', {