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(field.prop('hasErrors')).toEqual(true);
expect(wrapper.find('.pf-m-error')).toHaveLength(1); 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 () => { it('should submit value through Formik', async () => {
const value = '---\nfoo: bar\n'; const value = '---\nfoo: bar\n';

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,20 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; 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 { mountWithContexts } from '@testUtils/enzymeHelpers';
import WorkflowJobTemplateForm from './WorkflowJobTemplateForm'; import WorkflowJobTemplateForm from './WorkflowJobTemplateForm';
import { WorkflowJobTemplatesAPI } from '../../../api';
jest.mock('@api/models/WorkflowJobTemplates');
WorkflowJobTemplatesAPI.updateWebhookKey.mockResolvedValue({
data: { webhook_key: 'sdafdghjkl2345678ionbvcxz' },
});
describe('<WorkflowJobTemplateForm/>', () => { describe('<WorkflowJobTemplateForm/>', () => {
let wrapper; let wrapper;
let history;
const handleSubmit = jest.fn(); const handleSubmit = jest.fn();
const handleCancel = jest.fn(); const handleCancel = jest.fn();
const mockTemplate = { const mockTemplate = {
@@ -21,15 +31,38 @@ describe('<WorkflowJobTemplateForm/>', () => {
scm_branch: 'devel', scm_branch: 'devel',
limit: '5000', limit: '5000',
variables: '---', variables: '---',
related: {
webhook_receiver: '/api/v2/workflow_job_templates/57/gitlab/',
},
}; };
beforeEach(() => { beforeEach(() => {
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/6/edit'],
});
act(() => { act(() => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<WorkflowJobTemplateForm <Route
template={mockTemplate} path="/templates/workflow_job_template/:id/edit"
handleCancel={handleCancel} component={() => (
handleSubmit={handleSubmit} <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 () => { test('changing inputs should update values', async () => {
const inputsToChange = [ const inputsToChange = [
{ element: 'wfjt-name', value: { value: 'new foo', name: 'name' } }, {
element: 'wfjt-name',
value: { value: 'new foo', name: 'name' },
},
{ {
element: 'wfjt-description', element: 'wfjt-description',
value: { value: 'new bar', name: 'description' }, value: { value: 'new bar', name: 'description' },
@@ -76,12 +112,13 @@ describe('<WorkflowJobTemplateForm/>', () => {
]; ];
const changeInputs = async ({ element, value }) => { const changeInputs = async ({ element, value }) => {
wrapper.find(`input#${element}`).simulate('change', { wrapper.find(`input#${element}`).simulate('change', {
target: value, target: { value: `${value.value}`, name: `${value.name}` },
}); });
}; };
await act(async () => { await act(async () => {
inputsToChange.map(input => changeInputs(input)); inputsToChange.map(input => changeInputs(input));
wrapper.find('LabelSelect').invoke('onChange')([ wrapper.find('LabelSelect').invoke('onChange')([
{ name: 'new label', id: 5 }, { name: 'new label', id: 5 },
{ name: 'Label 1', id: 1 }, { name: 'Label 1', id: 1 },
@@ -97,13 +134,17 @@ describe('<WorkflowJobTemplateForm/>', () => {
}); });
}); });
wrapper.update(); wrapper.update();
expect(wrapper.find('input#wfjt-name').prop('value')).toEqual('new foo');
const assertChanges = ({ element, value }) => { const assertChanges = ({ element, value }) => {
expect(wrapper.find(`input#${element}`).prop('value')).toEqual( expect(wrapper.find(`input#${element}`).prop('value')).toEqual(
typeof value.value === 'string' ? `${value.value}` : value.value `${value.value}`
); );
}; };
inputsToChange.map(input => assertChanges(input)); inputsToChange.map(input => assertChanges(input));
expect(wrapper.find('input#wfjt-name').prop('value')).toEqual('new foo');
expect(wrapper.find('InventoryLookup').prop('value')).toEqual({ expect(wrapper.find('InventoryLookup').prop('value')).toEqual({
id: 3, id: 3,
name: 'inventory', name: 'inventory',
@@ -118,16 +159,57 @@ describe('<WorkflowJobTemplateForm/>', () => {
{ name: 'Label 2', id: 2 }, { 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(() => { 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(() => { act(() => {
wrapper.find('button[aria-label="Cancel"]').simulate('click'); wrapper.find('button[aria-label="Cancel"]').simulate('click');
}); });