Merge pull request #6547 from AlexSCorey/6384-ConvertWFJTToHooks

Converts WFJTForm to Formik hooks

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-04-14 16:33:45 +00:00
committed by GitHub
12 changed files with 460 additions and 435 deletions

View File

@@ -35,7 +35,7 @@ const mockJobTemplate = {
limit: '', limit: '',
name: 'Foo', name: 'Foo',
playbook: 'Baz', playbook: 'Baz',
project: { id: 3, summary_fields: { organization: { id: 1 } } }, project: 3,
scm_branch: '', scm_branch: '',
skip_tags: '', skip_tags: '',
summary_fields: { summary_fields: {

View File

@@ -32,7 +32,6 @@ class WorkflowJobTemplate extends Component {
contentError: null, contentError: null,
hasContentLoading: true, hasContentLoading: true,
template: null, template: null,
webhook_key: null,
isNotifAdmin: false, isNotifAdmin: false,
}; };
this.createSchedule = this.createSchedule.bind(this); this.createSchedule = this.createSchedule.bind(this);
@@ -59,11 +58,9 @@ class WorkflowJobTemplate extends Component {
this.setState({ contentError: null }); this.setState({ contentError: null });
try { try {
const { data } = await WorkflowJobTemplatesAPI.readDetail(id); const { data } = await WorkflowJobTemplatesAPI.readDetail(id);
let webhookKey;
if (data?.related?.webhook_key) { if (data?.related?.webhook_key) {
const { webhookKey = await WorkflowJobTemplatesAPI.readWebhookKey(id);
data: { webhook_key },
} = await WorkflowJobTemplatesAPI.readWebhookKey(id);
this.setState({ webhook_key });
} }
if (data?.summary_fields?.webhook_credential) { if (data?.summary_fields?.webhook_credential) {
const { const {
@@ -83,7 +80,7 @@ class WorkflowJobTemplate extends Component {
}); });
setBreadcrumb(data); setBreadcrumb(data);
this.setState({ this.setState({
template: data, template: { ...data, webhook_key: webhookKey.data.webhook_key },
isNotifAdmin: notifAdminRes.data.results.length > 0, isNotifAdmin: notifAdminRes.data.results.length > 0,
}); });
} catch (err) { } catch (err) {
@@ -114,7 +111,6 @@ class WorkflowJobTemplate extends Component {
contentError, contentError,
hasContentLoading, hasContentLoading,
template, template,
webhook_key,
isNotifAdmin, isNotifAdmin,
} = this.state; } = this.state;
@@ -211,10 +207,7 @@ class WorkflowJobTemplate extends Component {
key="wfjt-details" key="wfjt-details"
path="/templates/workflow_job_template/:id/details" path="/templates/workflow_job_template/:id/details"
> >
<WorkflowJobTemplateDetail <WorkflowJobTemplateDetail template={template} />
template={template}
webhook_key={webhook_key}
/>
</Route> </Route>
)} )}
{template && ( {template && (
@@ -239,10 +232,7 @@ class WorkflowJobTemplate extends Component {
key="wfjt-edit" key="wfjt-edit"
path="/templates/workflow_job_template/:id/edit" path="/templates/workflow_job_template/:id/edit"
> >
<WorkflowJobTemplateEdit <WorkflowJobTemplateEdit template={template} />
template={template}
webhook_key={webhook_key}
/>
</Route> </Route>
)} )}
{template && ( {template && (

View File

@@ -12,11 +12,23 @@ function WorkflowJobTemplateAdd() {
const [formSubmitError, setFormSubmitError] = useState(null); const [formSubmitError, setFormSubmitError] = useState(null);
const handleSubmit = async values => { const handleSubmit = async values => {
const { labels, organizationId, ...remainingValues } = values; const {
labels,
inventory,
organization,
webhook_credential,
webhook_key,
...templatePayload
} = values;
templatePayload.inventory = inventory?.id;
templatePayload.organization = organization?.id;
templatePayload.webhook_credential = webhook_credential?.id;
const organizationId =
organization?.id || inventory?.summary_fields?.organization.id;
try { try {
const { const {
data: { id }, data: { id },
} = await WorkflowJobTemplatesAPI.create(remainingValues); } = await WorkflowJobTemplatesAPI.create(templatePayload);
await Promise.all(await submitLabels(id, labels, organizationId)); await Promise.all(await submitLabels(id, labels, organizationId));
history.push(`/templates/workflow_job_template/${id}/details`); history.push(`/templates/workflow_job_template/${id}/details`);
} catch (err) { } catch (err) {

View File

@@ -15,9 +15,11 @@ jest.mock('@api/models/Inventories');
describe('<WorkflowJobTemplateAdd/>', () => { describe('<WorkflowJobTemplateAdd/>', () => {
let wrapper; let wrapper;
let history; let history;
const handleSubmit = jest.fn();
const handleCancel = jest.fn();
beforeEach(async () => { beforeEach(async () => {
WorkflowJobTemplatesAPI.create.mockResolvedValue({ data: { id: 1 } }); WorkflowJobTemplatesAPI.create.mockResolvedValue({ data: { id: 1 } });
OrganizationsAPI.read.mockResolvedValue({ results: [{ id: 1 }] }); OrganizationsAPI.read.mockResolvedValue({ data: { results: [{ id: 1 }] } });
LabelsAPI.read.mockResolvedValue({ LabelsAPI.read.mockResolvedValue({
data: { data: {
results: [ results: [
@@ -36,7 +38,12 @@ describe('<WorkflowJobTemplateAdd/>', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<Route <Route
path="/templates/workflow_job_template/add" path="/templates/workflow_job_template/add"
component={() => <WorkflowJobTemplateAdd />} component={() => (
<WorkflowJobTemplateAdd
handleSubmit={handleSubmit}
handleCancel={handleCancel}
/>
)}
/>, />,
{ {
context: { context: {
@@ -63,16 +70,48 @@ describe('<WorkflowJobTemplateAdd/>', () => {
test('calls workflowJobTemplatesAPI with correct information on submit', async () => { test('calls workflowJobTemplatesAPI with correct information on submit', async () => {
await act(async () => { await act(async () => {
await wrapper.find('WorkflowJobTemplateForm').invoke('handleSubmit')({ wrapper.find('input#wfjt-name').simulate('change', {
name: 'Alex', target: { value: 'Alex', name: 'name' },
labels: [{ name: 'Foo', id: 1 }, { name: 'bar', id: 2 }],
organizationId: 1,
}); });
wrapper
.find('LabelSelect')
.find('SelectToggle')
.simulate('click');
}); });
expect(WorkflowJobTemplatesAPI.create).toHaveBeenCalledWith({
wrapper.update();
act(() => {
wrapper
.find('SelectOption')
.find('button[aria-label="Label 3"]')
.prop('onClick')();
});
wrapper.update();
await act(async () => {
wrapper.find('form').simulate('submit');
});
await expect(WorkflowJobTemplatesAPI.create).toHaveBeenCalledWith({
name: 'Alex', name: 'Alex',
allow_simultaneous: false,
ask_inventory_on_launch: false,
ask_limit_on_launch: false,
ask_scm_branch_on_launch: false,
ask_variables_on_launch: false,
description: '',
extra_vars: '---',
inventory: undefined,
limit: '',
organization: undefined,
scm_branch: '',
webhook_credential: undefined,
webhook_service: '',
webhook_url: '',
}); });
expect(WorkflowJobTemplatesAPI.associateLabel).toHaveBeenCalledTimes(2);
expect(WorkflowJobTemplatesAPI.associateLabel).toHaveBeenCalledTimes(1);
}); });
test('handleCancel navigates the user to the /templates', async () => { test('handleCancel navigates the user to the /templates', async () => {
@@ -95,10 +134,16 @@ describe('<WorkflowJobTemplateAdd/>', () => {
WorkflowJobTemplatesAPI.create.mockRejectedValue(error); WorkflowJobTemplatesAPI.create.mockRejectedValue(error);
await act(async () => { await act(async () => {
wrapper.find('WorkflowJobTemplateForm').invoke('handleSubmit')({ wrapper.find('input#wfjt-name').simulate('change', {
name: 'Foo', target: { value: 'Alex', name: 'name' },
}); });
}); });
wrapper.update();
await act(async () => {
wrapper.find('form').simulate('submit');
});
expect(WorkflowJobTemplatesAPI.create).toHaveBeenCalled(); expect(WorkflowJobTemplatesAPI.create).toHaveBeenCalled();
wrapper.update(); wrapper.update();
expect(wrapper.find('WorkflowJobTemplateForm').prop('submitError')).toEqual( expect(wrapper.find('WorkflowJobTemplateForm').prop('submitError')).toEqual(

View File

@@ -25,7 +25,7 @@ import LaunchButton from '@components/LaunchButton';
import Sparkline from '@components/Sparkline'; import Sparkline from '@components/Sparkline';
import { toTitleCase } from '@util/strings'; import { toTitleCase } from '@util/strings';
function WorkflowJobTemplateDetail({ template, i18n, webhook_key }) { function WorkflowJobTemplateDetail({ template, i18n }) {
const { const {
id, id,
ask_inventory_on_launch, ask_inventory_on_launch,
@@ -38,6 +38,7 @@ function WorkflowJobTemplateDetail({ template, i18n, webhook_key }) {
summary_fields, summary_fields,
related, related,
webhook_credential, webhook_credential,
webhook_key,
} = template; } = template;
const urlOrigin = window.location.origin; const urlOrigin = window.location.origin;

View File

@@ -39,6 +39,7 @@ describe('<WorkflowJobTemplateDetail/>', () => {
user_capabilities: { edit: true, delete: true }, user_capabilities: { edit: true, delete: true },
}, },
webhook_service: 'Github', webhook_service: 'Github',
webhook_key: 'Foo webhook key',
}; };
beforeEach(async () => { beforeEach(async () => {
@@ -52,7 +53,6 @@ describe('<WorkflowJobTemplateDetail/>', () => {
component={() => ( component={() => (
<WorkflowJobTemplateDetail <WorkflowJobTemplateDetail
template={template} template={template}
webhook_key="Foo webhook key"
hasContentLoading={false} hasContentLoading={false}
onSetContentLoading={() => {}} onSetContentLoading={() => {}}
/> />

View File

@@ -6,17 +6,30 @@ import { getAddedAndRemoved } from '@util/lists';
import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '@api'; import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '@api';
import { WorkflowJobTemplateForm } from '../shared'; import { WorkflowJobTemplateForm } from '../shared';
function WorkflowJobTemplateEdit({ template, webhook_key }) { function WorkflowJobTemplateEdit({ template }) {
const history = useHistory(); const history = useHistory();
const [formSubmitError, setFormSubmitError] = useState(null); const [formSubmitError, setFormSubmitError] = useState(null);
const handleSubmit = async values => { const handleSubmit = async values => {
const { labels, ...remainingValues } = values; const {
labels,
inventory,
organization,
webhook_credential,
webhook_key,
...templatePayload
} = values;
templatePayload.inventory = inventory?.id;
templatePayload.organization = organization?.id;
templatePayload.webhook_credential = webhook_credential?.id || null;
const formOrgId =
organization?.id || inventory?.summary_fields?.organization.id || null;
try { try {
await Promise.all( await Promise.all(
await submitLabels(labels, values.organization, template.organization) await submitLabels(labels, formOrgId, template.organization)
); );
await WorkflowJobTemplatesAPI.update(template.id, remainingValues); await WorkflowJobTemplatesAPI.update(template.id, templatePayload);
history.push(`/templates/workflow_job_template/${template.id}/details`); history.push(`/templates/workflow_job_template/${template.id}/details`);
} catch (err) { } catch (err) {
setFormSubmitError(err); setFormSubmitError(err);
@@ -60,7 +73,6 @@ function WorkflowJobTemplateEdit({ template, webhook_key }) {
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
handleCancel={handleCancel} handleCancel={handleCancel}
template={template} template={template}
webhook_key={webhook_key}
submitError={formSubmitError} submitError={formSubmitError}
/> />
</CardBody> </CardBody>

View File

@@ -29,10 +29,15 @@ const mockTemplate = {
describe('<WorkflowJobTemplateEdit/>', () => { describe('<WorkflowJobTemplateEdit/>', () => {
let wrapper; let wrapper;
let history; let history;
beforeEach(async () => { beforeEach(async () => {
LabelsAPI.read.mockResolvedValue({ LabelsAPI.read.mockResolvedValue({
data: { data: {
results: [{ name: 'Label 1', id: 1 }, { name: 'Label 2', id: 2 }], results: [
{ name: 'Label 1', id: 1 },
{ name: 'Label 2', id: 2 },
{ name: 'Label 3', id: 3 },
],
}, },
}); });
OrganizationsAPI.read.mockResolvedValue({ results: [{ id: 1 }] }); OrganizationsAPI.read.mockResolvedValue({ results: [{ id: 1 }] });
@@ -71,29 +76,65 @@ describe('<WorkflowJobTemplateEdit/>', () => {
}); });
test('api is called to properly to save the updated template.', async () => { test('api is called to properly to save the updated template.', async () => {
await act(async () => { act(() => {
await wrapper.find('WorkflowJobTemplateForm').invoke('handleSubmit')({ wrapper.find('input#wfjt-name').simulate('change', {
id: 6, target: { value: 'Alex', name: 'name' },
name: 'Alex',
description: 'Apollo and Athena',
inventory: 1,
organization: 1,
labels: [{ name: 'Label 2', id: 2 }, { name: 'Generated Label' }],
scm_branch: 'master',
limit: '5000',
variables: '---',
}); });
wrapper.find('input#wfjt-description').simulate('change', {
target: { value: 'Apollo and Athena', name: 'description' },
});
wrapper.find('input#wfjt-description').simulate('change', {
target: { value: 'master', name: 'scm_branch' },
});
wrapper.find('input#wfjt-description').simulate('change', {
target: { value: '5000', name: 'limit' },
});
wrapper
.find('LabelSelect')
.find('SelectToggle')
.simulate('click');
});
wrapper.update();
act(() => {
wrapper
.find('SelectOption')
.find('button[aria-label="Label 3"]')
.prop('onClick')();
});
wrapper.update();
act(() =>
wrapper
.find('SelectOption')
.find('button[aria-label="Label 1"]')
.prop('onClick')()
);
wrapper.update();
await act(async () => {
wrapper.find('WorkflowJobTemplateForm').invoke('handleSubmit')();
}); });
expect(WorkflowJobTemplatesAPI.update).toHaveBeenCalledWith(6, { expect(WorkflowJobTemplatesAPI.update).toHaveBeenCalledWith(6, {
id: 6,
name: 'Alex', name: 'Alex',
description: 'Apollo and Athena', description: 'Apollo and Athena',
inventory: 1, inventory: 1,
organization: 1, organization: 1,
scm_branch: 'master', scm_branch: 'master',
limit: '5000', limit: '5000',
variables: '---', extra_vars: '---',
webhook_credential: null,
webhook_url: '',
webhook_service: '',
allow_simultaneous: false,
ask_inventory_on_launch: false,
ask_limit_on_launch: false,
ask_scm_branch_on_launch: false,
ask_variables_on_launch: false,
}); });
wrapper.update(); wrapper.update();
await expect(WorkflowJobTemplatesAPI.disassociateLabel).toBeCalledWith(6, { await expect(WorkflowJobTemplatesAPI.disassociateLabel).toBeCalledWith(6, {

View File

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

View File

@@ -47,7 +47,7 @@ function LabelSelect({ value, placeholder, onChange, onError }) {
const renderOptions = opts => { const renderOptions = opts => {
return opts.map(option => ( return opts.map(option => (
<SelectOption key={option.id} value={option}> <SelectOption key={option.id} aria-label={option.name} value={option}>
{option.name} {option.name}
</SelectOption> </SelectOption>
)); ));

View File

@@ -2,10 +2,10 @@ import React, { useState, useEffect, useCallback } from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useRouteMatch, useParams } from 'react-router-dom'; import { useRouteMatch, useParams } from 'react-router-dom';
import { func, shape } from 'prop-types'; import PropTypes, { shape } from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { Formik, Field } from 'formik'; import { useField, withFormik } from 'formik';
import { import {
Form, Form,
FormGroup, FormGroup,
@@ -40,38 +40,49 @@ import ContentError from '@components/ContentError';
import CheckboxField from '@components/FormField/CheckboxField'; import CheckboxField from '@components/FormField/CheckboxField';
import LabelSelect from './LabelSelect'; import LabelSelect from './LabelSelect';
const urlOrigin = window.location.origin;
function WorkflowJobTemplateForm({ function WorkflowJobTemplateForm({
handleSubmit, handleSubmit,
handleCancel, handleCancel,
i18n, i18n,
template = {},
webhook_key,
submitError, submitError,
}) { }) {
const urlOrigin = window.location.origin;
const { id } = useParams(); const { id } = useParams();
const wfjtAddMatch = useRouteMatch('/templates/workflow_job_template/add'); const wfjtAddMatch = useRouteMatch('/templates/workflow_job_template/add');
const [hasContentError, setContentError] = useState(null); const [hasContentError, setContentError] = useState(null);
const [webhook_url, setWebhookUrl] = useState(
template?.related?.webhook_receiver const [organizationField, organizationMeta, organizationHelpers] = useField(
? `${urlOrigin}${template.related.webhook_receiver}` 'organization'
: ''
); );
const [inventory, setInventory] = useState( const [inventoryField, inventoryMeta, inventoryHelpers] = useField(
template?.summary_fields?.inventory || null 'inventory'
); );
const [organization, setOrganization] = useState( const [labelsField, , labelsHelpers] = useField('labels');
template?.summary_fields?.organization || null
const [
webhookServiceField,
webhookServiceMeta,
webhookServiceHelpers,
] = useField('webhook_service');
const [webhookKeyField, webhookKeyMeta, webhookKeyHelpers] = useField(
'webhook_key'
); );
const [webhookCredential, setWebhookCredential] = useState(
template?.summary_fields?.webhook_credential || null const [hasWebhooks, setHasWebhooks] = useState(
Boolean(webhookServiceField.value)
); );
const [webhookKey, setWebHookKey] = useState(webhook_key);
const [webhookService, setWebHookService] = useState( const [
template.webhook_service || '' webhookCredentialField,
webhookCredentialMeta,
webhookCredentialHelpers,
] = useField('webhook_credential');
const [webhookUrlField, webhookUrlMeta, webhookUrlHelpers] = useField(
'webhook_url'
); );
const [hasWebhooks, setHasWebhooks] = useState(Boolean(webhookService));
const webhookServiceOptions = [ const webhookServiceOptions = [
{ {
@@ -93,6 +104,38 @@ function WorkflowJobTemplateForm({
isDisabled: false, isDisabled: false,
}, },
]; ];
const storeWebhookValues = webhookServiceValue => {
if (
webhookServiceValue === webhookServiceMeta.initialValue ||
webhookServiceValue === ''
) {
webhookCredentialHelpers.setValue(webhookCredentialMeta.initialValue);
webhookUrlHelpers.setValue(webhookUrlMeta.initialValue);
webhookServiceHelpers.setValue(webhookServiceMeta.initialValue);
webhookKeyHelpers.setValue(webhookKeyMeta.initialValue);
} else {
webhookCredentialHelpers.setValue(null);
webhookUrlHelpers.setValue(
`${urlOrigin}/api/v2/workflow_job_templates/${id}/${webhookServiceValue}/`
);
webhookKeyHelpers.setValue(
i18n._(t`a new webhook key will be generated on save.`).toUpperCase()
);
}
};
const handleWebhookEnablement = (enabledWebhooks, webhookServiceValue) => {
if (!enabledWebhooks) {
webhookCredentialHelpers.setValue(null);
webhookServiceHelpers.setValue('');
webhookUrlHelpers.setValue('');
webhookKeyHelpers.setValue('');
} else {
storeWebhookValues(webhookServiceValue);
}
};
const { const {
request: loadCredentialType, request: loadCredentialType,
error: contentError, error: contentError,
@@ -101,15 +144,15 @@ function WorkflowJobTemplateForm({
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
let results; let results;
if (webhookService) { if (webhookServiceField.value) {
results = await CredentialTypesAPI.read({ results = await CredentialTypesAPI.read({
namespace: `${webhookService}_token`, namespace: `${webhookServiceField.value}_token`,
}); });
// TODO: Consider how to handle the situation where the results returns // TODO: Consider how to handle the situation where the results returns
// and empty array, or any of the other values is undefined or null (data, results, id) // and empty array, or any of the other values is undefined or null (data, results, id)
} }
return results?.data?.results[0]?.id; return results?.data?.results[0]?.id;
}, [webhookService]) }, [webhookServiceField.value])
); );
useEffect(() => { useEffect(() => {
@@ -124,66 +167,12 @@ function WorkflowJobTemplateForm({
const { const {
data: { webhook_key: key }, data: { webhook_key: key },
} = await WorkflowJobTemplatesAPI.updateWebhookKey(id); } = await WorkflowJobTemplatesAPI.updateWebhookKey(id);
setWebHookKey(key); webhookKeyHelpers.setValue(key);
} catch (err) { } catch (err) {
setContentError(err); setContentError(err);
} }
}; };
let initialWebhookKey = webhook_key;
const initialWebhookCredential = template?.summary_fields?.webhook_credential;
const storeWebhookValues = (form, webhookServiceValue) => {
if (
webhookServiceValue === form.initialValues.webhook_service ||
webhookServiceValue === ''
) {
form.setFieldValue(
'webhook_credential',
form.initialValues.webhook_credential
);
setWebhookCredential(initialWebhookCredential);
setWebhookUrl(
template?.related?.webhook_receiver
? `${urlOrigin}${template.related.webhook_receiver}`
: ''
);
form.setFieldValue('webhook_service', form.initialValues.webhook_service);
setWebHookService(form.initialValues.webhook_service);
setWebHookKey(initialWebhookKey);
} else {
form.setFieldValue('webhook_credential', null);
setWebhookCredential(null);
setWebhookUrl(
`${urlOrigin}/api/v2/workflow_job_templates/${template.id}/${webhookServiceValue}/`
);
setWebHookKey(
i18n._(t`a new webhook key will be generated on save.`).toUpperCase()
);
}
};
const handleWebhookEnablement = (
form,
enabledWebhooks,
webhookServiceValue
) => {
if (!enabledWebhooks) {
initialWebhookKey = webhookKey;
form.setFieldValue('webhook_credential', null);
form.setFieldValue('webhook_service', '');
setWebhookUrl('');
setWebHookService('');
setWebHookKey('');
} else {
storeWebhookValues(form, webhookServiceValue);
}
};
if (hasContentError || contentError) { if (hasContentError || contentError) {
return <ContentError error={contentError || hasContentError} />; return <ContentError error={contentError || hasContentError} />;
} }
@@ -193,312 +182,213 @@ function WorkflowJobTemplateForm({
} }
return ( return (
<Formik <Form autoComplete="off" onSubmit={handleSubmit}>
onSubmit={values => { <FormColumnLayout>
if (values.webhook_service === '') { <FormField
values.webhook_credential = ''; id="wfjt-name"
} name="name"
return handleSubmit(values); type="text"
}} label={i18n._(t`Name`)}
initialValues={{ validate={required(null, i18n)}
name: template.name || '', isRequired
description: template.description || '', />
inventory: template?.summary_fields?.inventory?.id || null, <FormField
organization: template?.summary_fields?.organization?.id || null, id="wfjt-description"
labels: template.summary_fields?.labels?.results || [], name="description"
extra_vars: template.extra_vars || '---', type="text"
limit: template.limit || '', label={i18n._(t`Description`)}
scm_branch: template.scm_branch || '', />
allow_simultaneous: template.allow_simultaneous || false, <OrganizationLookup
webhook_credential: helperTextInvalid={organizationMeta.error}
template?.summary_fields?.webhook_credential?.id || null, onChange={value => {
webhook_service: template.webhook_service || '', organizationHelpers.setValue(value || null);
ask_limit_on_launch: template.ask_limit_on_launch || false, }}
ask_inventory_on_launch: template.ask_inventory_on_launch || false, value={organizationField.value}
ask_variables_on_launch: template.ask_variables_on_launch || false, isValid={!organizationMeta.error}
ask_scm_branch_on_launch: template.ask_scm_branch_on_launch || false, />
}} <FormGroup label={i18n._(t`Inventory`)} fieldId="wfjt-inventory">
> <FieldTooltip
{formik => ( content={i18n._(
<Form autoComplete="off" onSubmit={formik.handleSubmit}> t`Select an inventory for the workflow. This inventory is applied to all job template nodes that prompt for an inventory.`
<FormColumnLayout> )}
<FormField />
id="wfjt-name" <InventoryLookup
name="name" value={inventoryField.value}
type="text" isValid={!inventoryMeta.error}
label={i18n._(t`Name`)} helperTextInvalid={inventoryMeta.error}
validate={required(null, i18n)} onChange={value => {
isRequired inventoryHelpers.setValue(value || null);
/> }}
<FormField />
id="wfjt-description" </FormGroup>
name="description" <FormField
type="text" type="text"
label={i18n._(t`Description`)} name="limit"
/> id="wfjt-limit"
<Field label={i18n._(t`Limit`)}
id="wfjt-organization" tooltip={i18n._(
label={i18n._(t`Organization`)} t`Provide a host pattern to further constrain the list of hosts that will be managed or affected by the workflow. This limit is applied to all job template nodes that prompt for a limit. Refer to Ansible documentation for more information and examples on patterns.`
name="organization" )}
> />
{({ form }) => ( <FormField
<OrganizationLookup type="text"
helperTextInvalid={form.errors.organization} label={i18n._(t`SCM Branch`)}
onChange={value => { tooltip={i18n._(
form.setFieldValue('organization', value?.id || null); t`Select a branch for the workflow. This branch is applied to all job template nodes that prompt for a branch.`
setOrganization(value); )}
}} id="wfjt-scm_branch"
value={organization} name="scm_branch"
isValid={!form.errors.organization} />
/> </FormColumnLayout>
)} <FormFullWidthLayout>
</Field> <FormGroup label={i18n._(t`Labels`)} fieldId="template-labels">
<Field name="inventory"> <FieldTooltip
{({ form }) => ( content={i18n._(t`Optional labels that describe this job template,
<FormGroup
label={i18n._(t`Inventory`)}
fieldId="wfjt-inventory"
>
<FieldTooltip
content={i18n._(
t`Select an inventory for the workflow. This inventory is applied to all job template nodes that prompt for an inventory.`
)}
/>
<InventoryLookup
value={inventory}
isValid={!form.errors.inventory}
helperTextInvalid={form.errors.inventory}
onChange={value => {
form.setFieldValue('inventory', value?.id || null);
setInventory(value);
form.setFieldValue('organizationId', value?.organization);
}}
/>
</FormGroup>
)}
</Field>
<FormField
type="text"
name="limit"
id="wfjt-limit"
label={i18n._(t`Limit`)}
tooltip={i18n._(
t`Provide a host pattern to further constrain the list of hosts that will be managed or affected by the workflow. This limit is applied to all job template nodes that prompt for a limit. Refer to Ansible documentation for more information and examples on patterns.`
)}
/>
<FormField
type="text"
label={i18n._(t`Source Control Branch`)}
tooltip={i18n._(
t`Select a branch for the workflow. This branch is applied to all job template nodes that prompt for a branch.`
)}
id="wfjt-scm_branch"
name="scm_branch"
/>
</FormColumnLayout>
<FormFullWidthLayout>
<Field name="labels">
{({ form, field }) => (
<FormGroup
label={i18n._(t`Labels`)}
helperTextInvalid={form.errors.webhook_service}
isValid={!(form.touched.labels || form.errors.labels)}
name="wfjt-labels"
fieldId="wfjt-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 such as 'dev' or 'test'. Labels can be used to group and filter
job templates and completed jobs.`)} job templates and completed jobs.`)}
/>
<LabelSelect
value={field.value}
onChange={labels => form.setFieldValue('labels', labels)}
onError={err => setContentError(err)}
/>
</FormGroup>
)}
</Field>
</FormFullWidthLayout>
<FormFullWidthLayout>
<VariablesField
id="wfjt-variables"
name="extra_vars"
label={i18n._(t`Variables`)}
tooltip={i18n._(
t`Pass extra command line variables to the playbook. This is the -e or --extra-vars command line parameter for ansible-playbook. Provide key/value pairs using either YAML or JSON. Refer to the Ansible Tower documentation for example syntax.`
)}
/>
</FormFullWidthLayout>
<FormCheckboxLayout
fieldId="options"
isInline
label={i18n._(t`Options`)}
>
<Field id="wfjt-webhooks" name="hasWebhooks">
{({ form }) => (
<Checkbox
aria-label={i18n._(t`Enable Webhooks`)}
label={
<span>
{i18n._(t`Enable Webhooks`)}
&nbsp;
<FieldTooltip
content={i18n._(
t`Enable webhooks for this workflow job template.`
)}
/>
</span>
}
id="wfjt-enabled-webhooks"
isChecked={
Boolean(form.values.webhook_service) || hasWebhooks
}
onChange={checked => {
setHasWebhooks(checked);
handleWebhookEnablement(form, checked, webhookService);
}}
/>
)}
</Field>
<CheckboxField
name="allow_simultaneous"
id="allow_simultaneous"
tooltip={i18n._(
t`If enabled, simultaneous runs of this workflow job template will be allowed.`
)}
label={i18n._(t`Enable Concurrent Jobs`)}
/>
</FormCheckboxLayout>
{hasWebhooks && (
<FormColumnLayout>
<Field name="webhook_service">
{({ form, field }) => (
<FormGroup
name="webhook_service"
fieldId="webhook_service"
helperTextInvalid={form.errors.webhook_service}
isValid={
!(
form.touched.webhook_service ||
form.errors.webhook_service
)
}
label={i18n._(t`Webhook Service`)}
>
<FieldTooltip
content={i18n._(t`Select a webhook service`)}
/>
<AnsibleSelect
id="webhook_service"
data={webhookServiceOptions}
value={field.value}
onChange={(event, val) => {
setWebHookService(val);
storeWebhookValues(form, val);
form.setFieldValue('webhook_service', val);
}}
/>
</FormGroup>
)}
</Field>
{!wfjtAddMatch && (
<>
<FormGroup
type="text"
fieldId="wfjt-webhookURL"
label={i18n._(t`Webhook URL`)}
id="wfjt-webhook-url"
name="webhook_url"
>
<FieldTooltip
content={i18n._(
t`Webhook services can launch jobs with this workflow job template by making a POST request to this URL.`
)}
/>
<TextInput
aria-label={i18n._(t`Webhook URL`)}
value={webhook_url}
isReadOnly
/>
</FormGroup>
<Field>
{({ form }) => (
<FormGroup
fieldId="wfjt-webhook-key"
type="text"
id="wfjt-webhook-key"
name="webhook_key"
isValid={
!(form.touched.webhook_key || form.errors.webhook_key)
}
helperTextInvalid={form.errors.webhook_service}
label={i18n._(t`Webhook Key`)}
>
<FieldTooltip
content={i18n._(
t`Webhook services can use this as a shared secret.`
)}
/>
<InputGroup>
<TextInput
isReadOnly
aria-label="wfjt-webhook-key"
value={webhookKey}
/>
<Button variant="tertiary" onClick={changeWebhookKey}>
<SyncAltIcon />
</Button>
</InputGroup>
</FormGroup>
)}
</Field>
</>
)}
{credTypeId && (
// TODO: Consider how to handle the situation where the results returns
// an empty array, or any of the other values is undefined or null
// (data, results, id)
<Field name="webhook_credential">
{({ form }) => (
<CredentialLookup
label={i18n._(t`Webhook Credential`)}
tooltip={i18n._(
t`Optionally select the credential to use to send status updates back to the webhook service.`
)}
credentialTypeId={credTypeId}
onChange={value => {
form.setFieldValue(
'webhook_credential',
value?.id || null
);
setWebhookCredential(value);
}}
isValid={!form.errors.webhook_credential}
helperTextInvalid={form.errors.webhook_credential}
value={webhookCredential}
/>
)}
</Field>
)}
</FormColumnLayout>
)}
{submitError && <FormSubmitError error={submitError} />}
<FormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}
/> />
</Form> <LabelSelect
value={labelsField.value}
onChange={labels => labelsHelpers.setValue(labels)}
onError={setContentError}
/>
</FormGroup>
</FormFullWidthLayout>
<FormFullWidthLayout>
<VariablesField
id="wfjt-variables"
name="extra_vars"
label={i18n._(t`Variables`)}
tooltip={i18n._(
t`Pass extra command line variables to the playbook. This is the -e or --extra-vars command line parameter for ansible-playbook. Provide key/value pairs using either YAML or JSON. Refer to the Ansible Tower documentation for example syntax.`
)}
/>
</FormFullWidthLayout>
<FormCheckboxLayout fieldId="options" isInline label={i18n._(t`Options`)}>
<Checkbox
aria-label={i18n._(t`Enable Webhook`)}
label={
<span>
{i18n._(t`Enable Webhook`)}
&nbsp;
<FieldTooltip
content={i18n._(
t`Enable webhook for this workflow job template.`
)}
/>
</span>
}
id="wfjt-enabled-webhooks"
isChecked={Boolean(webhookServiceField.value) || hasWebhooks}
onChange={checked => {
setHasWebhooks(checked);
handleWebhookEnablement(checked, webhookServiceField.value);
}}
/>
<CheckboxField
name="allow_simultaneous"
id="allow_simultaneous"
tooltip={i18n._(
t`If enabled, simultaneous runs of this workflow job template will be allowed.`
)}
label={i18n._(t`Enable Concurrent Jobs`)}
/>
</FormCheckboxLayout>
{hasWebhooks && (
<FormColumnLayout>
<FormGroup
name="webhook_service"
fieldId="webhook_service"
helperTextInvalid={webhookServiceMeta.error}
isValid={!(webhookServiceMeta.touched || webhookServiceMeta.error)}
label={i18n._(t`Webhook Service`)}
>
<FieldTooltip content={i18n._(t`Select a webhook service`)} />
<AnsibleSelect
id="webhook_service"
data={webhookServiceOptions}
value={webhookServiceField.value}
onChange={(event, val) => {
storeWebhookValues(val);
webhookServiceHelpers.setValue(val);
}}
/>
</FormGroup>
{!wfjtAddMatch && (
<>
<FormGroup
type="text"
fieldId="wfjt-webhookURL"
label={i18n._(t`Webhook URL`)}
id="wfjt-webhook-url"
name="webhook_url"
>
<FieldTooltip
content={i18n._(
t`Webhook services can launch jobs with this workflow job template by making a POST request to this URL.`
)}
/>
<TextInput
aria-label={i18n._(t`Webhook URL`)}
value={webhookUrlField.value}
isReadOnly
/>
</FormGroup>
<FormGroup
fieldId="wfjt-webhook-key"
type="text"
id="wfjt-webhook-key"
name="webhook_key"
label={i18n._(t`Webhook Key`)}
>
<FieldTooltip
content={i18n._(
t`Webhook services can use this as a shared secret.`
)}
/>
<InputGroup>
<TextInput
isReadOnly
aria-label="wfjt-webhook-key"
value={webhookKeyField.value}
/>
<Button variant="tertiary" onClick={changeWebhookKey}>
<SyncAltIcon />
</Button>
</InputGroup>
</FormGroup>
</>
)}
{credTypeId && (
// TODO: Consider how to handle the situation where the results returns
// an empty array, or any of the other values is undefined or null
// (data, results, id)
<CredentialLookup
label={i18n._(t`Webhook Credential`)}
tooltip={i18n._(
t`Optionally select the credential to use to send status updates back to the webhook service.`
)}
credentialTypeId={credTypeId}
onChange={value => {
webhookCredentialHelpers.setValue(value || null);
}}
isValid={!webhookCredentialMeta.error}
helperTextInvalid={webhookCredentialMeta.error}
value={webhookCredentialField.value}
/>
)}
</FormColumnLayout>
)} )}
</Formik> {submitError && <FormSubmitError error={submitError} />}
<FormActionGroup onCancel={handleCancel} onSubmit={handleSubmit} />
</Form>
); );
} }
WorkflowJobTemplateForm.propTypes = { WorkflowJobTemplateForm.propTypes = {
handleSubmit: func.isRequired, handleSubmit: PropTypes.func.isRequired,
handleCancel: func.isRequired, handleCancel: PropTypes.func.isRequired,
submitError: shape({}), submitError: shape({}),
}; };
@@ -506,4 +396,38 @@ WorkflowJobTemplateForm.defaultProps = {
submitError: null, submitError: null,
}; };
export default withI18n()(WorkflowJobTemplateForm); const FormikApp = withFormik({
mapPropsToValues({ template = {} }) {
return {
name: template.name || '',
description: template.description || '',
inventory: template?.summary_fields?.inventory || null,
organization: template?.summary_fields?.organization || null,
labels: template.summary_fields?.labels?.results || [],
extra_vars: template.extra_vars || '---',
limit: template.limit || '',
scm_branch: template.scm_branch || '',
allow_simultaneous: template.allow_simultaneous || false,
webhook_credential: template?.summary_fields?.webhook_credential || null,
webhook_service: template.webhook_service || '',
ask_limit_on_launch: template.ask_limit_on_launch || false,
ask_inventory_on_launch: template.ask_inventory_on_launch || false,
ask_variables_on_launch: template.ask_variables_on_launch || false,
ask_scm_branch_on_launch: template.ask_scm_branch_on_launch || false,
webhook_url: template?.related?.webhook_receiver
? `${urlOrigin}${template.related.webhook_receiver}`
: '',
webhook_key: template.webhook_key || '',
};
},
handleSubmit: async (values, { props, setErrors }) => {
try {
await props.handleSubmit(values);
} catch (errors) {
setErrors(errors);
}
},
})(WorkflowJobTemplateForm);
export { WorkflowJobTemplateForm as _WorkflowJobTemplateForm };
export default withI18n()(FormikApp);

View File

@@ -40,6 +40,7 @@ describe('<WorkflowJobTemplateForm/>', () => {
related: { related: {
webhook_receiver: '/api/v2/workflow_job_templates/57/gitlab/', webhook_receiver: '/api/v2/workflow_job_templates/57/gitlab/',
}, },
webhook_key: 'sdfghjklmnbvcdsew435678iokjhgfd',
}; };
beforeEach(async () => { beforeEach(async () => {
@@ -74,7 +75,6 @@ describe('<WorkflowJobTemplateForm/>', () => {
template={mockTemplate} template={mockTemplate}
handleCancel={handleCancel} handleCancel={handleCancel}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
webhook_key="sdfghjklmnbvcdsew435678iokjhgfd"
/> />
)} )}
/>, />,
@@ -106,13 +106,14 @@ describe('<WorkflowJobTemplateForm/>', () => {
const fields = [ const fields = [
'FormField[name="name"]', 'FormField[name="name"]',
'FormField[name="description"]', 'FormField[name="description"]',
'Field[name="organization"]', 'FormGroup[label="Organization"]',
'Field[name="inventory"]', 'FormGroup[label="Inventory"]',
'FormField[name="limit"]', 'FormField[name="limit"]',
'FormField[name="scm_branch"]', 'FormField[name="scm_branch"]',
'Field[name="labels"]', 'FormGroup[label="Labels"]',
'VariablesField', 'VariablesField',
]; ];
const assertField = field => { const assertField = field => {
expect(wrapper.find(`${field}`).length).toBe(1); expect(wrapper.find(`${field}`).length).toBe(1);
}; };
@@ -171,7 +172,7 @@ describe('<WorkflowJobTemplateForm/>', () => {
test('webhooks and enable concurrent jobs functions properly', async () => { test('webhooks and enable concurrent jobs functions properly', async () => {
act(() => { act(() => {
wrapper.find('Checkbox[aria-label="Enable Webhooks"]').invoke('onChange')( wrapper.find('Checkbox[aria-label="Enable Webhook"]').invoke('onChange')(
true, true,
{ {
currentTarget: { value: true, type: 'change', checked: true }, currentTarget: { value: true, type: 'change', checked: true },
@@ -180,7 +181,7 @@ describe('<WorkflowJobTemplateForm/>', () => {
}); });
wrapper.update(); wrapper.update();
expect( expect(
wrapper.find('Checkbox[aria-label="Enable Webhooks"]').prop('isChecked') wrapper.find('Checkbox[aria-label="Enable Webhook"]').prop('isChecked')
).toBe(true); ).toBe(true);
expect( expect(
@@ -201,8 +202,7 @@ describe('<WorkflowJobTemplateForm/>', () => {
).toContain('/api/v2/workflow_job_templates/57/gitlab/'); ).toContain('/api/v2/workflow_job_templates/57/gitlab/');
wrapper.update(); wrapper.update();
expect(wrapper.find('FormGroup[name="webhook_service"]').length).toBe(1);
expect(wrapper.find('Field[name="webhook_service"]').length).toBe(1);
act(() => wrapper.find('AnsibleSelect').prop('onChange')({}, 'gitlab')); act(() => wrapper.find('AnsibleSelect').prop('onChange')({}, 'gitlab'));
wrapper.update(); wrapper.update();