Merge pull request #6385 from AlexSCorey/6317-ConvertJTFormstoFormikHooks

Uses formik hooks for JT Form

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-04-02 15:52:35 +00:00 committed by GitHub
commit a682565758
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 164 additions and 249 deletions

View File

@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom';
import { Card, PageSection } from '@patternfly/react-core';
import { CardBody } from '@components/Card';
import JobTemplateForm from '../shared/JobTemplateForm';
import { JobTemplatesAPI, OrganizationsAPI } from '@api';
import { JobTemplatesAPI } from '@api';
function JobTemplateAdd() {
const [formSubmitError, setFormSubmitError] = useState(null);
@ -12,7 +12,6 @@ function JobTemplateAdd() {
async function handleSubmit(values) {
const {
labels,
organizationId,
instanceGroups,
initialInstanceGroups,
credentials,
@ -20,12 +19,13 @@ function JobTemplateAdd() {
} = values;
setFormSubmitError(null);
remainingValues.project = remainingValues.project.id;
try {
const {
data: { id, type },
} = await JobTemplatesAPI.create(remainingValues);
await Promise.all([
submitLabels(id, labels, organizationId),
submitLabels(id, labels, values.project.summary_fields.organization.id),
submitInstanceGroups(id, instanceGroups),
submitCredentials(id, credentials),
]);
@ -35,16 +35,7 @@ function JobTemplateAdd() {
}
}
async function submitLabels(templateId, labels = [], formOrg) {
let orgId = formOrg;
if (!orgId && labels.length > 0) {
const {
data: { results },
} = await OrganizationsAPI.read();
orgId = results[0].id;
}
async function submitLabels(templateId, labels = [], orgId) {
const associationPromises = labels.map(label =>
JobTemplatesAPI.associateLabel(templateId, label, orgId)
);

View File

@ -33,7 +33,7 @@ const jobTemplateData = {
limit: '',
name: '',
playbook: '',
project: 1,
project: { id: 1, summary_fields: { organization: { id: 1 } } },
scm_branch: '',
skip_tags: '',
timeout: 0,
@ -123,6 +123,7 @@ describe('<JobTemplateAdd />', () => {
wrapper.find('ProjectLookup').invoke('onChange')({
id: 2,
name: 'project',
summary_fields: { organization: { id: 1, name: 'Org Foo' } },
});
wrapper.update();
wrapper
@ -161,6 +162,7 @@ describe('<JobTemplateAdd />', () => {
id: 1,
type: 'job_template',
...jobTemplateData,
project: jobTemplateData.project.id,
},
});
let wrapper;
@ -181,6 +183,7 @@ describe('<JobTemplateAdd />', () => {
wrapper.find('ProjectLookup').invoke('onChange')({
id: 2,
name: 'project',
summary_fields: { organization: { id: 1, name: 'Org Foo' } },
});
wrapper.update();
wrapper

View File

@ -4,7 +4,7 @@ import { withRouter, Redirect } from 'react-router-dom';
import { CardBody } from '@components/Card';
import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import { JobTemplatesAPI, OrganizationsAPI, ProjectsAPI } from '@api';
import { JobTemplatesAPI, ProjectsAPI } from '@api';
import { JobTemplate } from '@types';
import { getAddedAndRemoved } from '@util/lists';
import JobTemplateForm from '../shared/JobTemplateForm';
@ -97,7 +97,6 @@ class JobTemplateEdit extends Component {
const { template, history } = this.props;
const {
labels,
organizationId,
instanceGroups,
initialInstanceGroups,
credentials,
@ -105,10 +104,14 @@ class JobTemplateEdit extends Component {
} = values;
this.setState({ formSubmitError: null });
remainingValues.project = values.project.id;
try {
await JobTemplatesAPI.update(template.id, remainingValues);
await Promise.all([
this.submitLabels(labels, organizationId),
this.submitLabels(
labels,
values.project.summary_fields.organization.id
),
this.submitInstanceGroups(instanceGroups, initialInstanceGroups),
this.submitCredentials(credentials),
]);
@ -118,22 +121,14 @@ class JobTemplateEdit extends Component {
}
}
async submitLabels(labels = [], formOrgId) {
async submitLabels(labels = [], orgId) {
const { template } = this.props;
let orgId = formOrgId;
const { added, removed } = getAddedAndRemoved(
template.summary_fields.labels.results,
labels
);
if (!orgId && added.length > 0) {
const {
data: { results },
} = await OrganizationsAPI.read();
orgId = results[0].id;
}
const disassociationPromises = removed.map(label =>
JobTemplatesAPI.disassociateLabel(template.id, label)
);

View File

@ -35,7 +35,7 @@ const mockJobTemplate = {
limit: '',
name: 'Foo',
playbook: 'Baz',
project: 3,
project: { id: 3, summary_fields: { organization: { id: 1 } } },
scm_branch: '',
skip_tags: '',
summary_fields: {
@ -239,6 +239,7 @@ describe('<JobTemplateEdit />', () => {
const expected = {
...mockJobTemplate,
project: mockJobTemplate.project.id,
...updatedTemplateData,
};
delete expected.summary_fields;

View File

@ -3,7 +3,7 @@ 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 { withFormik, useField, useFormikContext } from 'formik';
import {
Form,
FormGroup,
@ -47,11 +47,9 @@ function JobTemplateForm({
validateField,
handleCancel,
handleSubmit,
handleBlur,
setFieldValue,
submitError,
i18n,
touched,
}) {
const [contentError, setContentError] = useState(false);
const [project, setProject] = useState(null);
@ -62,6 +60,35 @@ function JobTemplateForm({
Boolean(template?.host_config_key)
);
const { values: formikValues } = useFormikContext();
const [jobTypeField, jobTypeMeta, jobTypeHelpers] = useField({
name: 'job_type',
validate: required(null, i18n),
});
const [, inventoryMeta, inventoryHelpers] = useField('inventory');
const [projectField, projectMeta, projectHelpers] = useField({
name: 'project',
validate: () => handleProjectValidation(),
});
const [scmField, , scmHelpers] = useField('scm_branch');
const [playbookField, playbookMeta, playbookHelpers] = useField({
name: 'playbook',
validate: required(i18n._(t`Select a value for this field`), i18n),
});
const [credentialField, , credentialHelpers] = useField('credentials');
const [labelsField, , labelsHelpers] = useField('labels');
const [limitField, limitMeta] = useField('limit');
const [verbosityField] = useField('verbosity');
const [diffModeField, , diffModeHelpers] = useField('diff_mode');
const [instanceGroupsField, , instanceGroupsHelpers] = useField(
'instanceGroups'
);
const [jobTagsField, , jobTagsHelpers] = useField('job_tags');
const [skipTagsField, , skipTagsHelpers] = useField('skip_tags');
const {
request: fetchProject,
error: projectContentError,
@ -100,7 +127,7 @@ function JobTemplateForm({
}, [loadRelatedInstanceGroups]);
const handleProjectValidation = () => {
if (!project && touched.project) {
if (!project && projectMeta.touched) {
return i18n._(t`Select a value for this field`);
}
if (project && project.status === 'never updated') {
@ -112,11 +139,11 @@ function JobTemplateForm({
const handleProjectUpdate = useCallback(
newProject => {
setProject(newProject);
setFieldValue('project', newProject.id);
setFieldValue('playbook', 0);
setFieldValue('scm_branch', '');
projectHelpers.setValue(newProject);
playbookHelpers.setValue(0);
scmHelpers.setValue('');
},
[setFieldValue, setProject]
[setProject, projectHelpers, playbookHelpers, scmHelpers]
);
const jobTypeOptions = [
@ -184,26 +211,15 @@ function JobTemplateForm({
test environment setup, and report problems without
executing the playbook.`)}
>
<Field
name="job_type"
validate={required(null, i18n)}
onBlur={handleBlur}
>
{({ form, field }) => {
const isValid = !form.touched.job_type || !form.errors.job_type;
return (
<AnsibleSelect
isValid={isValid}
id="template-job-type"
data={jobTypeOptions}
{...field}
onChange={(event, value) => {
form.setFieldValue('job_type', value);
}}
/>
);
<AnsibleSelect
{...jobTypeField}
isValid={!jobTypeMeta.touched || !jobTypeMeta.error}
id="template-job-type"
data={jobTypeOptions}
onChange={(event, value) => {
jobTypeHelpers.setValue(value);
}}
</Field>
/>
</FieldWithPrompt>
<FieldWithPrompt
fieldId="template-inventory"
@ -214,108 +230,71 @@ function JobTemplateForm({
tooltip={i18n._(t`Select the inventory containing the hosts
you want this job to manage.`)}
>
<Field name="inventory">
{({ form }) => (
<>
<InventoryLookup
value={inventory}
onBlur={() => {
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 && (
<div
className="pf-c-form__helper-text pf-m-error"
aria-live="polite"
>
{form.errors.inventory}
</div>
)}
</>
<InventoryLookup
value={inventory}
onBlur={() => inventoryHelpers.setTouched()}
onChange={value => {
inventoryHelpers.setValue(value.id);
setInventory(value);
}}
required
touched={inventoryMeta.touched}
error={inventoryMeta.error}
/>
{(inventoryMeta.touched || formikValues.ask_inventory_on_launch) &&
inventoryMeta.error && (
<div
className="pf-c-form__helper-text pf-m-error"
aria-live="polite"
>
{inventoryMeta.error}
</div>
)}
</Field>
</FieldWithPrompt>
<Field name="project" validate={handleProjectValidation}>
{({ form }) => (
<ProjectLookup
value={project}
onBlur={() => form.setFieldTouched('project')}
tooltip={i18n._(t`Select the project containing the playbook
<ProjectLookup
value={project}
onBlur={() => projectHelpers.setTouched()}
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
/>
)}
</Field>
{project && project.allow_override && (
isValid={!projectMeta.touched || !projectMeta.error}
helperTextInvalid={projectMeta.error}
onChange={handleProjectUpdate}
required
/>
{project?.allow_override && (
<FieldWithPrompt
fieldId="template-scm-branch"
label={i18n._(t`SCM Branch`)}
promptId="template-ask-scm-branch-on-launch"
promptName="ask_scm_branch_on_launch"
>
<Field name="scm_branch">
{({ field }) => {
return (
<TextInput
id="template-scm-branch"
{...field}
onChange={(value, event) => {
field.onChange(event);
}}
/>
);
<TextInput
id="template-scm-branch"
{...scmField}
onChange={(value, event) => {
scmField.onChange(event);
}}
</Field>
/>
</FieldWithPrompt>
)}
<Field
name="playbook"
validate={required(i18n._(t`Select a value for this field`), i18n)}
onBlur={handleBlur}
<FormGroup
fieldId="template-playbook"
helperTextInvalid={playbookMeta.error}
isRequired
label={i18n._(t`Playbook`)}
>
{({ field, form }) => {
const isValid = !form.touched.playbook || !form.errors.playbook;
return (
<FormGroup
fieldId="template-playbook"
helperTextInvalid={form.errors.playbook}
isRequired
isValid={isValid}
label={i18n._(t`Playbook`)}
>
<FieldTooltip
content={i18n._(
t`Select the playbook to be executed by this job.`
)}
/>
<PlaybookSelect
projectId={form.values.project}
isValid={isValid}
form={form}
field={field}
onBlur={() => form.setFieldTouched('playbook')}
onError={setContentError}
/>
</FormGroup>
);
}}
</Field>
<FieldTooltip
content={i18n._(t`Select the playbook to be executed by this job.`)}
/>
<PlaybookSelect
projectId={project?.id || projectField.value?.id}
isValid={!(playbookMeta.touched || playbookMeta.error)}
field={playbookField}
onBlur={() => playbookHelpers.setTouched()}
onError={setContentError}
/>
</FormGroup>
<FormFullWidthLayout>
<FieldWithPrompt
fieldId="template-credentials"
@ -328,36 +307,26 @@ function JobTemplateForm({
credential at run time. If you select credentials and check "Prompt on launch", the selected
credential(s) become the defaults that can be updated at run time.`)}
>
<Field name="credentials" fieldId="template-credentials">
{({ field }) => {
return (
<MultiCredentialsLookup
value={field.value}
onChange={newCredentials =>
setFieldValue('credentials', newCredentials)
}
onError={setContentError}
/>
);
}}
</Field>
<MultiCredentialsLookup
value={credentialField.value}
onChange={newCredentials =>
credentialHelpers.setValue(newCredentials)
}
onError={setContentError}
/>
</FieldWithPrompt>
<Field name="labels">
{({ field }) => (
<FormGroup label={i18n._(t`Labels`)} fieldId="template-labels">
<FieldTooltip
content={i18n._(t`Optional labels that describe this job template,
<FormGroup label={i18n._(t`Labels`)} fieldId="template-labels">
<FieldTooltip
content={i18n._(t`Optional labels that describe this job template,
such as 'dev' or 'test'. Labels can be used to group and filter
job templates and completed jobs.`)}
/>
<LabelSelect
value={field.value}
onChange={labels => setFieldValue('labels', labels)}
onError={setContentError}
/>
</FormGroup>
)}
</Field>
/>
<LabelSelect
value={labelsField.value}
onChange={labels => labelsHelpers.setValue(labels)}
onError={setContentError}
/>
</FormGroup>
<VariablesField
id="template-variables"
name="extra_vars"
@ -394,20 +363,14 @@ function JobTemplateForm({
playbook. Multiple patterns are allowed. Refer to Ansible
documentation for more information and examples on patterns.`)}
>
<Field name="limit">
{({ form, field }) => {
return (
<TextInput
id="template-limit"
{...field}
isValid={!form.touched.job_type || !form.errors.job_type}
onChange={(value, event) => {
field.onChange(event);
}}
/>
);
<TextInput
id="template-limit"
{...limitField}
isValid={!limitMeta.touched || !limitMeta.error}
onChange={(value, event) => {
limitField.onChange(event);
}}
</Field>
/>
</FieldWithPrompt>
<FieldWithPrompt
fieldId="template-verbosity"
@ -417,15 +380,11 @@ function JobTemplateForm({
tooltip={i18n._(t`Control the level of output ansible will
produce as the playbook executes.`)}
>
<Field name="verbosity">
{({ field }) => (
<AnsibleSelect
id="template-verbosity"
data={verbosityOptions}
{...field}
/>
)}
</Field>
<AnsibleSelect
id="template-verbosity"
data={verbosityOptions}
{...verbosityField}
/>
</FieldWithPrompt>
<FormField
id="template-job-slicing"
@ -456,32 +415,20 @@ function JobTemplateForm({
Ansible tasks, where supported. This is equivalent
to Ansible&#x2019s --diff mode.`)}
>
<Field name="diff_mode">
{({ form, field }) => {
return (
<Switch
id="template-show-changes"
label={field.value ? i18n._(t`On`) : i18n._(t`Off`)}
isChecked={field.value}
onChange={checked =>
form.setFieldValue(field.name, checked)
}
/>
);
}}
</Field>
<Switch
id="template-show-changes"
label={diffModeField.value ? i18n._(t`On`) : i18n._(t`Off`)}
isChecked={diffModeField.value}
onChange={checked => diffModeHelpers.setValue(checked)}
/>
</FieldWithPrompt>
<FormFullWidthLayout>
<Field name="instanceGroups">
{({ field, form }) => (
<InstanceGroupsLookup
value={field.value}
onChange={value => form.setFieldValue(field.name, value)}
tooltip={i18n._(t`Select the Instance Groups for this Organization
<InstanceGroupsLookup
value={instanceGroupsField.value}
onChange={value => instanceGroupsHelpers.setValue(value)}
tooltip={i18n._(t`Select the Instance Groups for this Organization
to run on.`)}
/>
)}
</Field>
/>
<FieldWithPrompt
fieldId="template-tags"
label={i18n._(t`Job Tags`)}
@ -493,14 +440,10 @@ function JobTemplateForm({
Refer to Ansible Tower documentation for details on
the usage of tags.`)}
>
<Field name="job_tags">
{({ field, form }) => (
<TagMultiSelect
value={field.value}
onChange={value => form.setFieldValue(field.name, value)}
/>
)}
</Field>
<TagMultiSelect
value={jobTagsField.value}
onChange={value => jobTagsHelpers.setValue(value)}
/>
</FieldWithPrompt>
<FieldWithPrompt
fieldId="template-skip-tags"
@ -513,14 +456,10 @@ function JobTemplateForm({
to Ansible Tower documentation for details on the usage
of tags.`)}
>
<Field name="skip_tags">
{({ field, form }) => (
<TagMultiSelect
value={field.value}
onChange={value => form.setFieldValue(field.name, value)}
/>
)}
</Field>
<TagMultiSelect
value={skipTagsField.value}
onChange={value => skipTagsHelpers.setValue(value)}
/>
</FieldWithPrompt>
<FormGroup
fieldId="template-option-checkboxes"
@ -636,10 +575,6 @@ const FormikApp = withFormik({
},
} = 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,
@ -655,7 +590,7 @@ const FormikApp = withFormik({
description: template.description || '',
job_type: template.job_type || 'run',
inventory: template.inventory || null,
project: template.project || '',
project: template.project || null,
scm_branch: template.scm_branch || '',
playbook: template.playbook || '',
labels: summary_fields.labels.results || [],
@ -672,7 +607,6 @@ const FormikApp = withFormik({
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 || [],

View File

@ -14,7 +14,7 @@ describe('<JobTemplateForm />', () => {
description: 'Bar',
job_type: 'run',
inventory: 2,
project: 3,
project: { id: 3, summary_fields: { organization: { id: 1 } } },
playbook: 'Baz',
type: 'job_template',
scm_branch: 'Foo',

View File

@ -5,15 +5,7 @@ import { t } from '@lingui/macro';
import AnsibleSelect from '@components/AnsibleSelect';
import { ProjectsAPI } from '@api';
function PlaybookSelect({
projectId,
isValid,
form,
field,
onBlur,
onError,
i18n,
}) {
function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
const [options, setOptions] = useState([]);
useEffect(() => {
@ -47,7 +39,6 @@ function PlaybookSelect({
id="template-playbook"
data={options}
isValid={isValid}
form={form}
{...field}
onBlur={onBlur}
/>

View File

@ -70,7 +70,7 @@ export const JobTemplate = shape({
inventory: number,
job_type: oneOf(['run', 'check']),
playbook: string,
project: number,
project: shape({}),
});
export const Inventory = shape({