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: '',
name: 'Foo',
playbook: 'Baz',
project: { id: 3, summary_fields: { organization: { id: 1 } } },
project: 3,
scm_branch: '',
skip_tags: '',
summary_fields: {

View File

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

View File

@@ -12,11 +12,23 @@ function WorkflowJobTemplateAdd() {
const [formSubmitError, setFormSubmitError] = useState(null);
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 {
const {
data: { id },
} = await WorkflowJobTemplatesAPI.create(remainingValues);
} = await WorkflowJobTemplatesAPI.create(templatePayload);
await Promise.all(await submitLabels(id, labels, organizationId));
history.push(`/templates/workflow_job_template/${id}/details`);
} catch (err) {

View File

@@ -15,9 +15,11 @@ jest.mock('@api/models/Inventories');
describe('<WorkflowJobTemplateAdd/>', () => {
let wrapper;
let history;
const handleSubmit = jest.fn();
const handleCancel = jest.fn();
beforeEach(async () => {
WorkflowJobTemplatesAPI.create.mockResolvedValue({ data: { id: 1 } });
OrganizationsAPI.read.mockResolvedValue({ results: [{ id: 1 }] });
OrganizationsAPI.read.mockResolvedValue({ data: { results: [{ id: 1 }] } });
LabelsAPI.read.mockResolvedValue({
data: {
results: [
@@ -36,7 +38,12 @@ describe('<WorkflowJobTemplateAdd/>', () => {
wrapper = mountWithContexts(
<Route
path="/templates/workflow_job_template/add"
component={() => <WorkflowJobTemplateAdd />}
component={() => (
<WorkflowJobTemplateAdd
handleSubmit={handleSubmit}
handleCancel={handleCancel}
/>
)}
/>,
{
context: {
@@ -63,16 +70,48 @@ describe('<WorkflowJobTemplateAdd/>', () => {
test('calls workflowJobTemplatesAPI with correct information on submit', async () => {
await act(async () => {
await wrapper.find('WorkflowJobTemplateForm').invoke('handleSubmit')({
name: 'Alex',
labels: [{ name: 'Foo', id: 1 }, { name: 'bar', id: 2 }],
organizationId: 1,
wrapper.find('input#wfjt-name').simulate('change', {
target: { value: 'Alex', name: 'name' },
});
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',
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 () => {
@@ -95,10 +134,16 @@ describe('<WorkflowJobTemplateAdd/>', () => {
WorkflowJobTemplatesAPI.create.mockRejectedValue(error);
await act(async () => {
wrapper.find('WorkflowJobTemplateForm').invoke('handleSubmit')({
name: 'Foo',
wrapper.find('input#wfjt-name').simulate('change', {
target: { value: 'Alex', name: 'name' },
});
});
wrapper.update();
await act(async () => {
wrapper.find('form').simulate('submit');
});
expect(WorkflowJobTemplatesAPI.create).toHaveBeenCalled();
wrapper.update();
expect(wrapper.find('WorkflowJobTemplateForm').prop('submitError')).toEqual(

View File

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

View File

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

View File

@@ -6,17 +6,30 @@ import { getAddedAndRemoved } from '@util/lists';
import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '@api';
import { WorkflowJobTemplateForm } from '../shared';
function WorkflowJobTemplateEdit({ template, webhook_key }) {
function WorkflowJobTemplateEdit({ template }) {
const history = useHistory();
const [formSubmitError, setFormSubmitError] = useState(null);
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 {
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`);
} catch (err) {
setFormSubmitError(err);
@@ -60,7 +73,6 @@ function WorkflowJobTemplateEdit({ template, webhook_key }) {
handleSubmit={handleSubmit}
handleCancel={handleCancel}
template={template}
webhook_key={webhook_key}
submitError={formSubmitError}
/>
</CardBody>

View File

@@ -29,10 +29,15 @@ const mockTemplate = {
describe('<WorkflowJobTemplateEdit/>', () => {
let wrapper;
let history;
beforeEach(async () => {
LabelsAPI.read.mockResolvedValue({
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 }] });
@@ -71,29 +76,65 @@ describe('<WorkflowJobTemplateEdit/>', () => {
});
test('api is called to properly to save the updated template.', async () => {
await act(async () => {
await wrapper.find('WorkflowJobTemplateForm').invoke('handleSubmit')({
id: 6,
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: '---',
act(() => {
wrapper.find('input#wfjt-name').simulate('change', {
target: { value: 'Alex', name: 'name' },
});
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, {
id: 6,
name: 'Alex',
description: 'Apollo and Athena',
inventory: 1,
organization: 1,
scm_branch: 'master',
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();
await expect(WorkflowJobTemplatesAPI.disassociateLabel).toBeCalledWith(6, {

View File

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

View File

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

View File

@@ -2,10 +2,10 @@ import React, { useState, useEffect, useCallback } from 'react';
import { t } from '@lingui/macro';
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 { Formik, Field } from 'formik';
import { useField, withFormik } from 'formik';
import {
Form,
FormGroup,
@@ -40,38 +40,49 @@ import ContentError from '@components/ContentError';
import CheckboxField from '@components/FormField/CheckboxField';
import LabelSelect from './LabelSelect';
const urlOrigin = window.location.origin;
function WorkflowJobTemplateForm({
handleSubmit,
handleCancel,
i18n,
template = {},
webhook_key,
submitError,
}) {
const urlOrigin = window.location.origin;
const { id } = useParams();
const wfjtAddMatch = useRouteMatch('/templates/workflow_job_template/add');
const [hasContentError, setContentError] = useState(null);
const [webhook_url, setWebhookUrl] = useState(
template?.related?.webhook_receiver
? `${urlOrigin}${template.related.webhook_receiver}`
: ''
const [organizationField, organizationMeta, organizationHelpers] = useField(
'organization'
);
const [inventory, setInventory] = useState(
template?.summary_fields?.inventory || null
const [inventoryField, inventoryMeta, inventoryHelpers] = useField(
'inventory'
);
const [organization, setOrganization] = useState(
template?.summary_fields?.organization || null
const [labelsField, , labelsHelpers] = useField('labels');
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(
template.webhook_service || ''
const [
webhookCredentialField,
webhookCredentialMeta,
webhookCredentialHelpers,
] = useField('webhook_credential');
const [webhookUrlField, webhookUrlMeta, webhookUrlHelpers] = useField(
'webhook_url'
);
const [hasWebhooks, setHasWebhooks] = useState(Boolean(webhookService));
const webhookServiceOptions = [
{
@@ -93,6 +104,38 @@ function WorkflowJobTemplateForm({
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 {
request: loadCredentialType,
error: contentError,
@@ -101,15 +144,15 @@ function WorkflowJobTemplateForm({
} = useRequest(
useCallback(async () => {
let results;
if (webhookService) {
if (webhookServiceField.value) {
results = await CredentialTypesAPI.read({
namespace: `${webhookService}_token`,
namespace: `${webhookServiceField.value}_token`,
});
// 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)
}
return results?.data?.results[0]?.id;
}, [webhookService])
}, [webhookServiceField.value])
);
useEffect(() => {
@@ -124,66 +167,12 @@ function WorkflowJobTemplateForm({
const {
data: { webhook_key: key },
} = await WorkflowJobTemplatesAPI.updateWebhookKey(id);
setWebHookKey(key);
webhookKeyHelpers.setValue(key);
} catch (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) {
return <ContentError error={contentError || hasContentError} />;
}
@@ -193,312 +182,213 @@ function WorkflowJobTemplateForm({
}
return (
<Formik
onSubmit={values => {
if (values.webhook_service === '') {
values.webhook_credential = '';
}
return handleSubmit(values);
}}
initialValues={{
name: template.name || '',
description: template.description || '',
inventory: template?.summary_fields?.inventory?.id || null,
organization: template?.summary_fields?.organization?.id || 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?.id || 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,
}}
>
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormColumnLayout>
<FormField
id="wfjt-name"
name="name"
type="text"
label={i18n._(t`Name`)}
validate={required(null, i18n)}
isRequired
/>
<FormField
id="wfjt-description"
name="description"
type="text"
label={i18n._(t`Description`)}
/>
<Field
id="wfjt-organization"
label={i18n._(t`Organization`)}
name="organization"
>
{({ form }) => (
<OrganizationLookup
helperTextInvalid={form.errors.organization}
onChange={value => {
form.setFieldValue('organization', value?.id || null);
setOrganization(value);
}}
value={organization}
isValid={!form.errors.organization}
/>
)}
</Field>
<Field name="inventory">
{({ form }) => (
<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,
<Form autoComplete="off" onSubmit={handleSubmit}>
<FormColumnLayout>
<FormField
id="wfjt-name"
name="name"
type="text"
label={i18n._(t`Name`)}
validate={required(null, i18n)}
isRequired
/>
<FormField
id="wfjt-description"
name="description"
type="text"
label={i18n._(t`Description`)}
/>
<OrganizationLookup
helperTextInvalid={organizationMeta.error}
onChange={value => {
organizationHelpers.setValue(value || null);
}}
value={organizationField.value}
isValid={!organizationMeta.error}
/>
<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={inventoryField.value}
isValid={!inventoryMeta.error}
helperTextInvalid={inventoryMeta.error}
onChange={value => {
inventoryHelpers.setValue(value || null);
}}
/>
</FormGroup>
<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`SCM 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>
<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 => 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 = {
handleSubmit: func.isRequired,
handleCancel: func.isRequired,
handleSubmit: PropTypes.func.isRequired,
handleCancel: PropTypes.func.isRequired,
submitError: shape({}),
};
@@ -506,4 +396,38 @@ WorkflowJobTemplateForm.defaultProps = {
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: {
webhook_receiver: '/api/v2/workflow_job_templates/57/gitlab/',
},
webhook_key: 'sdfghjklmnbvcdsew435678iokjhgfd',
};
beforeEach(async () => {
@@ -74,7 +75,6 @@ describe('<WorkflowJobTemplateForm/>', () => {
template={mockTemplate}
handleCancel={handleCancel}
handleSubmit={handleSubmit}
webhook_key="sdfghjklmnbvcdsew435678iokjhgfd"
/>
)}
/>,
@@ -106,13 +106,14 @@ describe('<WorkflowJobTemplateForm/>', () => {
const fields = [
'FormField[name="name"]',
'FormField[name="description"]',
'Field[name="organization"]',
'Field[name="inventory"]',
'FormGroup[label="Organization"]',
'FormGroup[label="Inventory"]',
'FormField[name="limit"]',
'FormField[name="scm_branch"]',
'Field[name="labels"]',
'FormGroup[label="Labels"]',
'VariablesField',
];
const assertField = field => {
expect(wrapper.find(`${field}`).length).toBe(1);
};
@@ -171,7 +172,7 @@ describe('<WorkflowJobTemplateForm/>', () => {
test('webhooks and enable concurrent jobs functions properly', async () => {
act(() => {
wrapper.find('Checkbox[aria-label="Enable Webhooks"]').invoke('onChange')(
wrapper.find('Checkbox[aria-label="Enable Webhook"]').invoke('onChange')(
true,
{
currentTarget: { value: true, type: 'change', checked: true },
@@ -180,7 +181,7 @@ describe('<WorkflowJobTemplateForm/>', () => {
});
wrapper.update();
expect(
wrapper.find('Checkbox[aria-label="Enable Webhooks"]').prop('isChecked')
wrapper.find('Checkbox[aria-label="Enable Webhook"]').prop('isChecked')
).toBe(true);
expect(
@@ -201,8 +202,7 @@ describe('<WorkflowJobTemplateForm/>', () => {
).toContain('/api/v2/workflow_job_templates/57/gitlab/');
wrapper.update();
expect(wrapper.find('Field[name="webhook_service"]').length).toBe(1);
expect(wrapper.find('FormGroup[name="webhook_service"]').length).toBe(1);
act(() => wrapper.find('AnsibleSelect').prop('onChange')({}, 'gitlab'));
wrapper.update();