Adds Webhook form fields and tooltip to VariablesField component

This commit is contained in:
Alex Corey 2020-02-14 16:01:58 -05:00
parent d97f516c3a
commit d4ba32d0c5
7 changed files with 254 additions and 91 deletions

View File

@ -69,6 +69,22 @@ describe('VariablesField', () => {
expect(field.prop('hasErrors')).toEqual(true);
expect(wrapper.find('.pf-m-error')).toHaveLength(1);
});
it('should render tooltip', () => {
const value = '---\n';
const wrapper = mount(
<Formik initialValues={{ variables: value }}>
{() => (
<VariablesField
id="the-field"
name="variables"
label="Variables"
tooltip="This is a tooltip"
/>
)}
</Formik>
);
expect(wrapper.find('Tooltip').length).toBe(1);
});
it('should submit value through Formik', async () => {
const value = '---\nfoo: bar\n';

View File

@ -22,15 +22,18 @@ function WorkflowJobTemplateAdd() {
setFormSubmitError(err);
}
};
const submitLabels = (templateId, labels = [], organizationId) => {
const associatePromises = labels.map(label =>
WorkflowJobTemplatesAPI.associateLabel(templateId, label, organizationId)
);
return Promise.all([...associatePromises]);
};
const handleCancel = () => {
history.push(`/templates`);
};
return (
<PageSection>
<Card>
@ -38,9 +41,9 @@ function WorkflowJobTemplateAdd() {
<WorkflowJobTemplateForm
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={formSubmitError}
/>
</CardBody>
{formSubmitError ? <div>formSubmitError</div> : ''}
</Card>
</PageSection>
);

View File

@ -61,4 +61,27 @@ describe('<WorkflowJobTemplateAdd/>', () => {
});
expect(history.location.pathname).toBe('/templates');
});
test('throwing error renders FormSubmitError component', async () => {
const error = new Error('oops');
WorkflowJobTemplatesAPI.create.mockImplementation(() =>
Promise.reject(error)
);
await act(async () => {
await wrapper.find('WorkflowJobTemplateForm').prop('handleSubmit')({
id: 6,
name: 'Alex',
description: 'Apollo and Athena',
inventory: { id: 1, name: 'Inventory 1' },
organization: 2,
scm_branch: 'master',
limit: '5000',
variables: '---',
});
});
wrapper.update();
expect(WorkflowJobTemplatesAPI.create).toBeCalled();
expect(wrapper.find('WorkflowJobTemplateForm').prop('submitError')).toEqual(
error
);
});
});

View File

@ -25,6 +25,7 @@ function WorkflowJobTemplateEdit({
setFormSubmitError(err);
}
};
const submitLabels = async (labels = [], orgId) => {
const { added, removed } = getAddedAndRemoved(
template.summary_fields.labels.results,
@ -40,9 +41,11 @@ function WorkflowJobTemplateEdit({
setFormSubmitError(err);
}
}
const disassociationPromises = removed.map(label =>
WorkflowJobTemplatesAPI.disassociateLabel(template.id, label)
);
const associationPromises = added.map(label => {
return WorkflowJobTemplatesAPI.associateLabel(
template.id,
@ -61,6 +64,7 @@ function WorkflowJobTemplateEdit({
const handleCancel = () => {
history.push(`/templates`);
};
if (hasTemplateLoading) {
return <ContentLoading />;
}
@ -72,9 +76,9 @@ function WorkflowJobTemplateEdit({
handleCancel={handleCancel}
template={template}
webhook_key={webhook_key}
submitError={formSubmitError}
/>
</CardBody>
{formSubmitError ? <div>formSubmitError</div> : ''}
</>
);
}

View File

@ -94,4 +94,27 @@ describe('<WorkflowJobTemplateEdit/>', () => {
});
expect(history.location.pathname).toBe('/templates');
});
test('throwing error renders FormSubmitError component', async () => {
const error = new Error('oops');
WorkflowJobTemplatesAPI.update.mockImplementation(() =>
Promise.reject(error)
);
await act(async () => {
wrapper.find('WorkflowJobTemplateForm').prop('handleSubmit')({
id: 6,
name: 'Alex',
description: 'Apollo and Athena',
inventory: { id: 1, name: 'Inventory 1' },
organization: 2,
scm_branch: 'master',
limit: '5000',
variables: '---',
});
});
wrapper.update();
expect(WorkflowJobTemplatesAPI.update).toHaveBeenCalled();
expect(wrapper.find('WorkflowJobTemplateForm').prop('submitError')).toEqual(
error
);
});
});

View File

@ -1,5 +1,7 @@
import React, { useState, useEffect } from 'react';
import { t } from '@lingui/macro';
import { func, shape } from 'prop-types';
import { withI18n } from '@lingui/react';
import { Formik, Field } from 'formik';
import {
@ -11,14 +13,17 @@ import {
TextInput,
} from '@patternfly/react-core';
import { required } from '@util/validators';
import PropTypes from 'prop-types';
import { SyncAltIcon } from '@patternfly/react-icons';
import { useParams } from 'react-router-dom';
import AnsibleSelect from '@components/AnsibleSelect';
import { WorkflowJobTemplatesAPI, CredentialTypesAPI } from '@api';
import FormRow from '@components/FormRow';
import FormField, { FieldTooltip, CheckboxField } from '@components/FormField';
import FormField, {
FieldTooltip,
FormSubmitError,
} from '@components/FormField';
import OrganizationLookup from '@components/Lookup/OrganizationLookup';
import CredentialLookup from '@components/Lookup/CredentialLookup';
import { InventoryLookup } from '@components/Lookup';
@ -44,11 +49,11 @@ function WorkflowJobTemplateForm({
handleCancel,
i18n,
template = {},
className,
webhook_key,
submitError,
}) {
const urlOrigin = window.location.origin;
const { id } = useParams();
const urlOrigin = window.location.origin;
const [contentError, setContentError] = useState(null);
const [inventory, setInventory] = useState(
template?.summary_fields?.inventory || null
@ -139,9 +144,10 @@ function WorkflowJobTemplateForm({
variables: template.variables || '---',
limit: template.limit || '',
scmBranch: template.scm_branch || '',
allow_simultaneous: template?.allow_simultaneous || false,
webhook_url: `${urlOrigin}${template?.related?.webhook_receiver}` || '',
webhook_key: webHookKey,
allow_simultaneous: template.allow_simultaneous || false,
webhook_url:
template?.related?.webhook_receiver &&
`${urlOrigin}${template?.related?.webhook_receiver}`,
webhook_credential:
template?.summary_fields?.webhook_credential?.id || null,
webhook_service: webhookService,
@ -269,16 +275,12 @@ function WorkflowJobTemplateForm({
</FormRow>
<FormRow>
<VariablesField
id="host-variables"
id="wfjt-variables"
name="variables"
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.`
)}
/>
</FormRow>
<GridFormGroup
className={className}
fieldId="options"
isInline
label={i18n._(t`Options`)}
@ -302,21 +304,34 @@ function WorkflowJobTemplateForm({
setHasWebhooks(checked);
}}
/>
<CheckboxField
id="wfjt-allow_simultaneous"
name="allow_simultaneous"
label={i18n._(t`Enable Concurrent Jobs`)}
tooltip={i18n._(
t`If enabled, simultaneous runs of this workflow job template will be allowed.`
<Field name="allow_simultaneous">
{({ field, form }) => (
<Checkbox
name="allow_simultaneous"
id="wfjt-allow_simultaneous"
label={
<span>
{i18n._(t`Enable Concurrent Jobs`)} &nbsp;
<FieldTooltip
content={i18n._(
t`If enabled, simultaneous runs of this workflow job template will be allowed.`
)}
/>
</span>
}
isChecked={field.value}
onChange={value => {
form.setFieldValue('allow_simultaneous', value);
}}
/>
)}
/>
</Field>
</GridFormGroup>
{hasWebhooks && (
<>
<FormRow>
{template.related && (
<FormGroup
className={className}
fieldId="wfjt-webhook-url"
id="wfjt-webhook-url"
name="webhook_url"
@ -362,69 +377,60 @@ function WorkflowJobTemplateForm({
</FormGroup>
)}
<Field name="webhook_service">
{({ form }) => {
const isValid =
!form.errors.webhook_service;
return (
<FormGroup
name="webhook_service"
fieldId="webhook_service"
helperTextInvalid={form.errors.webhook_service}
isValid={isValid}
label={i18n._(t`Webhook Service`)}
>
<FieldTooltip
content={i18n._(t`Select a webhook service`)}
/>
<AnsibleSelect
isValid={isValid}
id="webhook_service"
data={webhookServiceOptions}
value={webhookService}
onChange={(event, val) => {
setWebHookService(val);
setWebHookCredential(null);
form.setFieldValue('webhook_credential', null);
}}
/>
</FormGroup>
);
}}
{({ form }) => (
<FormGroup
name="webhook_service"
fieldId="webhook_service"
helperTextInvalid={form.errors.webhook_service}
label={i18n._(t`Webhook Service`)}
>
<FieldTooltip
content={i18n._(t`Select a webhook service`)}
/>
<AnsibleSelect
id="webhook_service"
data={webhookServiceOptions}
value={webhookService}
onChange={(event, val) => {
setWebHookService(val);
setWebHookCredential(null);
form.setFieldValue('webhook_service', val);
form.setFieldValue('webhook_credential', null);
}}
/>
</FormGroup>
)}
</Field>
</FormRow>
{credTypeId && (
<FormRow>
<Field name="webhook_credential">
{({ form }) => {
return (
<FormGroup
fieldId="webhook_credential"
id="webhook_credential"
name="webhook_credential"
>
<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 || null}
onChange={value => {
setWebHookCredential(value);
form.setFieldValue(
'webhook_credential',
value.id
);
}}
value={webhookCredential}
/>
</FormGroup>
);
}}
{({ form }) => (
<FormGroup
fieldId="webhook_credential"
id="webhook_credential"
name="webhook_credential"
>
<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 || null}
onChange={value => {
setWebHookCredential(value);
form.setFieldValue('webhook_credential', value.id);
}}
value={webhookCredential}
/>
</FormGroup>
)}
</Field>
</FormRow>
)}
</>
)}
<FormSubmitError error={submitError} />
<FormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}
@ -436,7 +442,13 @@ function WorkflowJobTemplateForm({
}
WorkflowJobTemplateForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
handleCancel: PropTypes.func.isRequired,
handleSubmit: func.isRequired,
handleCancel: func.isRequired,
submitError: shape({}),
};
WorkflowJobTemplateForm.defaultProps = {
submitError: null,
};
export default withI18n()(WorkflowJobTemplateForm);

View File

@ -1,10 +1,20 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { sleep } from '@testUtils/testUtils';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import WorkflowJobTemplateForm from './WorkflowJobTemplateForm';
import { WorkflowJobTemplatesAPI } from '../../../api';
jest.mock('@api/models/WorkflowJobTemplates');
WorkflowJobTemplatesAPI.updateWebhookKey.mockResolvedValue({
data: { webhook_key: 'sdafdghjkl2345678ionbvcxz' },
});
describe('<WorkflowJobTemplateForm/>', () => {
let wrapper;
let history;
const handleSubmit = jest.fn();
const handleCancel = jest.fn();
const mockTemplate = {
@ -21,15 +31,38 @@ describe('<WorkflowJobTemplateForm/>', () => {
scm_branch: 'devel',
limit: '5000',
variables: '---',
related: {
webhook_receiver: '/api/v2/workflow_job_templates/57/gitlab/',
},
};
beforeEach(() => {
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/6/edit'],
});
act(() => {
wrapper = mountWithContexts(
<WorkflowJobTemplateForm
template={mockTemplate}
handleCancel={handleCancel}
handleSubmit={handleSubmit}
/>
<Route
path="/templates/workflow_job_template/:id/edit"
component={() => (
<WorkflowJobTemplateForm
template={mockTemplate}
handleCancel={handleCancel}
handleSubmit={handleSubmit}
webhook_key="sdfghjklmnbvcdsew435678iokjhgfd"
/>
)}
/>,
{
context: {
router: {
history,
route: {
location: history.location,
match: { params: { id: 6 } },
},
},
},
}
);
});
});
@ -63,7 +96,10 @@ describe('<WorkflowJobTemplateForm/>', () => {
});
test('changing inputs should update values', async () => {
const inputsToChange = [
{ element: 'wfjt-name', value: { value: 'new foo', name: 'name' } },
{
element: 'wfjt-name',
value: { value: 'new foo', name: 'name' },
},
{
element: 'wfjt-description',
value: { value: 'new bar', name: 'description' },
@ -76,12 +112,13 @@ describe('<WorkflowJobTemplateForm/>', () => {
];
const changeInputs = async ({ element, value }) => {
wrapper.find(`input#${element}`).simulate('change', {
target: value,
target: { value: `${value.value}`, name: `${value.name}` },
});
};
await act(async () => {
inputsToChange.map(input => changeInputs(input));
wrapper.find('LabelSelect').invoke('onChange')([
{ name: 'new label', id: 5 },
{ name: 'Label 1', id: 1 },
@ -97,13 +134,17 @@ describe('<WorkflowJobTemplateForm/>', () => {
});
});
wrapper.update();
expect(wrapper.find('input#wfjt-name').prop('value')).toEqual('new foo');
const assertChanges = ({ element, value }) => {
expect(wrapper.find(`input#${element}`).prop('value')).toEqual(
typeof value.value === 'string' ? `${value.value}` : value.value
`${value.value}`
);
};
inputsToChange.map(input => assertChanges(input));
expect(wrapper.find('input#wfjt-name').prop('value')).toEqual('new foo');
expect(wrapper.find('InventoryLookup').prop('value')).toEqual({
id: 3,
name: 'inventory',
@ -118,16 +159,57 @@ describe('<WorkflowJobTemplateForm/>', () => {
{ name: 'Label 2', id: 2 },
]);
});
test('handleSubmit is called on submit button click', async () => {
await act(async () => {
await wrapper.find('button[aria-label="Save"]').simulate('click');
});
test('webhooks and enable concurrent jobs functions properly', async () => {
act(() => {
expect(handleSubmit).toBeCalled();
wrapper.find('Checkbox[name="has_webhooks"]').invoke('onChange')(true);
wrapper.find('Checkbox[name="allow_simultaneous"]').invoke('onChange')(
true
);
});
wrapper.update();
expect(
wrapper.find('input[aria-label="wfjt-webhook-key"]').prop('readOnly')
).toBe(true);
expect(
wrapper.find('input[aria-label="wfjt-webhook-key"]').prop('value')
).toBe('sdfghjklmnbvcdsew435678iokjhgfd');
await act(() =>
wrapper
.find('FormGroup[name="webhook_key"]')
.find('Button[variant="tertiary"]')
.prop('onClick')()
);
expect(WorkflowJobTemplatesAPI.updateWebhookKey).toBeCalledWith('6');
expect(
wrapper.find('TextInputBase[name="webhook_url"]').prop('value')
).toBe('http://127.0.0.1:3001/api/v2/workflow_job_templates/57/gitlab/');
wrapper.update();
expect(
wrapper.find('Checkbox[name="has_webhooks"]').prop('isChecked')
).toBe(true);
expect(
wrapper.find('Checkbox[name="allow_simultaneous"]').prop('isChecked')
).toBe(true);
expect(wrapper.find('Field[name="webhook_service"]').length).toBe(1);
act(() => wrapper.find('AnsibleSelect').prop('onChange')({}, 'gitlab'));
wrapper.update();
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('gitlab');
});
test('handleCancel is called on cancel button click', () => {
test('handleSubmit is called on submit button click', async () => {
act(() => {
wrapper.find('Formik').prop('onSubmit')();
});
wrapper.update();
sleep(0);
expect(handleSubmit).toBeCalled();
});
test('handleCancel is called on cancel button click', async () => {
act(() => {
wrapper.find('button[aria-label="Cancel"]').simulate('click');
});