Adds error handling to submit labels, prevents uncessary api call

The unecessary api call is for the webhook credential id.  If there is no
webhook service we do not want the api to make a call for get the webhook credential
type id.
This commit is contained in:
Alex Corey
2020-02-28 09:43:02 -05:00
parent 51a069fcc4
commit acfa6d056f
5 changed files with 100 additions and 89 deletions

View File

@@ -5,9 +5,8 @@ import { t } from '@lingui/macro';
import { useField } from 'formik'; import { useField } from 'formik';
import styled from 'styled-components'; import styled from 'styled-components';
import { Split, SplitItem } from '@patternfly/react-core'; import { Split, SplitItem } from '@patternfly/react-core';
import { CheckboxField } from '@components/FormField'; import { CheckboxField, FieldTooltip } from '@components/FormField';
import { yamlToJson, jsonToYaml, isJson } from '@util/yaml'; import { yamlToJson, jsonToYaml, isJson } from '@util/yaml';
import { FieldTooltip } from '@components/FormField';
import CodeMirrorInput from './CodeMirrorInput'; import CodeMirrorInput from './CodeMirrorInput';
import YamlJsonToggle from './YamlJsonToggle'; import YamlJsonToggle from './YamlJsonToggle';
import { JSON_MODE, YAML_MODE } from './constants'; import { JSON_MODE, YAML_MODE } from './constants';

View File

@@ -17,33 +17,28 @@ function WorkflowJobTemplateAdd() {
const { const {
data: { id }, data: { id },
} = await WorkflowJobTemplatesAPI.create(remainingValues); } = await WorkflowJobTemplatesAPI.create(remainingValues);
await Promise.all([submitLabels(id, labels, organizationId, values)]); 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) {
setFormSubmitError(err); setFormSubmitError(err);
} }
}; };
const submitLabels = async ( const submitLabels = async (templateId, labels = [], organizationId) => {
templateId, if (!organizationId) {
labels = [],
organizationId,
values
) => {
if (!organizationId && !values.organization) {
try { try {
const { const {
data: { results }, data: { results },
} = await OrganizationsAPI.read(); } = await OrganizationsAPI.read();
organizationId = results[0].id; organizationId = results[0].id;
} catch (err) { } catch (err) {
setFormSubmitError(err); throw err;
} }
} }
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 [...associatePromises];
}; };
const handleCancel = () => { const handleCancel = () => {

View File

@@ -13,46 +13,38 @@ function WorkflowJobTemplateEdit({ template, webhook_key }) {
const handleSubmit = async values => { const handleSubmit = async values => {
const { labels, ...remainingValues } = values; const { labels, ...remainingValues } = values;
try { try {
await submitLabels(labels, values.organization, template.organization);
await WorkflowJobTemplatesAPI.update(template.id, remainingValues); await WorkflowJobTemplatesAPI.update(template.id, remainingValues);
await Promise.all([submitLabels(labels, values.organization)]);
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);
} }
}; };
const submitLabels = async (labels = [], orgId) => { const submitLabels = async (labels = [], formOrgId, templateOrgId) => {
const { added, removed } = getAddedAndRemoved( const { added, removed } = getAddedAndRemoved(
template.summary_fields.labels.results, template.summary_fields.labels.results,
labels labels
); );
if (!orgId && !template.organization) { let orgId = formOrgId || templateOrgId;
if (!orgId) {
try { try {
const { const {
data: { results }, data: { results },
} = await OrganizationsAPI.read(); } = await OrganizationsAPI.read();
orgId = results[0].id; orgId = results[0].id;
} catch (err) { } catch (err) {
setFormSubmitError(err); throw err;
} }
} }
const disassociationPromises = removed.map(label => const disassociationPromises = await removed.map(label =>
WorkflowJobTemplatesAPI.disassociateLabel(template.id, label) WorkflowJobTemplatesAPI.disassociateLabel(template.id, label)
); );
const associationPromises = await added.map(label =>
const associationPromises = added.map(label => { WorkflowJobTemplatesAPI.associateLabel(template.id, label, orgId)
return WorkflowJobTemplatesAPI.associateLabel( );
template.id, const results = [...disassociationPromises, ...associationPromises];
label,
orgId || template.organization
);
});
const results = await Promise.all([
...disassociationPromises,
...associationPromises,
]);
return results; return results;
}; };

View File

@@ -1,12 +1,14 @@
import React from 'react'; import React from 'react';
import { Route } from 'react-router-dom'; import { Route } from 'react-router-dom';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { WorkflowJobTemplatesAPI } from '@api'; import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '@api';
import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import WorkflowJobTemplateEdit from './WorkflowJobTemplateEdit'; import WorkflowJobTemplateEdit from './WorkflowJobTemplateEdit';
jest.mock('@api'); jest.mock('@api/models/WorkflowJobTemplates');
jest.mock('@api/models/Organizations');
const mockTemplate = { const mockTemplate = {
id: 6, id: 6,
name: 'Foo', name: 'Foo',
@@ -53,6 +55,7 @@ describe('<WorkflowJobTemplateEdit/>', () => {
afterEach(async () => { afterEach(async () => {
wrapper.unmount(); wrapper.unmount();
jest.clearAllMocks();
}); });
test('renders successfully', () => { test('renders successfully', () => {
@@ -65,8 +68,8 @@ describe('<WorkflowJobTemplateEdit/>', () => {
id: 6, id: 6,
name: 'Alex', name: 'Alex',
description: 'Apollo and Athena', description: 'Apollo and Athena',
inventory: { id: 1, name: 'Inventory 1' }, inventory: 1,
organization: 2, organization: 1,
labels: [{ name: 'Label 2', id: 2 }, { name: 'Generated Label' }], labels: [{ name: 'Label 2', id: 2 }, { name: 'Generated Label' }],
scm_branch: 'master', scm_branch: 'master',
limit: '5000', limit: '5000',
@@ -78,8 +81,8 @@ describe('<WorkflowJobTemplateEdit/>', () => {
id: 6, id: 6,
name: 'Alex', name: 'Alex',
description: 'Apollo and Athena', description: 'Apollo and Athena',
inventory: { id: 1, name: 'Inventory 1' }, inventory: 1,
organization: 2, organization: 1,
scm_branch: 'master', scm_branch: 'master',
limit: '5000', limit: '5000',
variables: '---', variables: '---',
@@ -94,9 +97,9 @@ describe('<WorkflowJobTemplateEdit/>', () => {
await expect(WorkflowJobTemplatesAPI.associateLabel).toBeCalledTimes(1); await expect(WorkflowJobTemplatesAPI.associateLabel).toBeCalledTimes(1);
}); });
test('handleCancel navigates the user to the /templates', async () => { test('handleCancel navigates the user to the /templates', () => {
await act(async () => { act(() => {
await wrapper.find('WorkflowJobTemplateForm').invoke('handleCancel')(); wrapper.find('WorkflowJobTemplateForm').invoke('handleCancel')();
}); });
expect(history.location.pathname).toBe( expect(history.location.pathname).toBe(
'/templates/workflow_job_template/6/details' '/templates/workflow_job_template/6/details'
@@ -105,25 +108,41 @@ describe('<WorkflowJobTemplateEdit/>', () => {
test('throwing error renders FormSubmitError component', async () => { test('throwing error renders FormSubmitError component', async () => {
const error = new Error('oops'); const error = new Error('oops');
WorkflowJobTemplatesAPI.update.mockImplementation(() => OrganizationsAPI.read.mockResolvedValue({ results: [{ id: 1 }] });
Promise.reject(error) WorkflowJobTemplatesAPI.update.mockRejectedValue(error);
);
await act(async () => { await act(async () => {
wrapper.find('WorkflowJobTemplateForm').prop('handleSubmit')({ wrapper.find('Button[aria-label="Save"]').simulate('click');
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(WorkflowJobTemplatesAPI.update).toHaveBeenCalled();
wrapper.update();
expect(wrapper.find('WorkflowJobTemplateForm').prop('submitError')).toEqual( expect(wrapper.find('WorkflowJobTemplateForm').prop('submitError')).toEqual(
error error
); );
}); });
test('throwing error prevents form submission', () => {
OrganizationsAPI.read.mockRejectedValue(new Error('An error occurred'));
WorkflowJobTemplatesAPI.update.mockResolvedValue();
act(() => {
wrapper = mountWithContexts(
<WorkflowJobTemplateEdit template={mockTemplate} />,
{
context: {
router: {
history,
},
},
}
);
});
wrapper.find('Button[aria-label="Save"]').simulate('click');
expect(wrapper.find('WorkflowJobTemplateForm').length).toBe(1);
expect(OrganizationsAPI.read).toBeCalled();
expect(WorkflowJobTemplatesAPI.update).not.toBeCalled();
expect(history.location.pathname).toBe(
'/templates/workflow_job_template/6/edit'
);
});
}); });

View File

@@ -97,11 +97,14 @@ function WorkflowJobTemplateForm({
result: credTypeId, result: credTypeId,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const results = await CredentialTypesAPI.read({ let results;
namespace: `${webhookService}_token`, if (webhookService) {
}); results = await CredentialTypesAPI.read({
// TODO: Consider how to handle the situation where the results returns namespace: `${webhookService}_token`,
// and empty array, or any of the other values is undefined or null (data, results, id) });
// 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; return results?.data?.results[0]?.id;
}, [webhookService]) }, [webhookService])
); );
@@ -109,10 +112,10 @@ function WorkflowJobTemplateForm({
useEffect(() => { useEffect(() => {
loadCredentialType(); loadCredentialType();
}, [loadCredentialType]); }, [loadCredentialType]);
// TODO: Convert this function below to useRequest. Create a new webhookkey component
// that handles all of that api calls. Will also need to move this api call out of // TODO: Convert this function below to useRequest. Might want to create a new
// WorkflowJobTemplate.jsx and add it to workflowJobTemplateDetai.jsx // webhookkey component that handles all of that api calls. Will also need
let initialWebhookKey = webhook_key; // to move this api call out of WorkflowJobTemplate.jsx and add it to workflowJobTemplateDetai.jsx
const changeWebhookKey = async () => { const changeWebhookKey = async () => {
try { try {
const { const {
@@ -124,7 +127,9 @@ function WorkflowJobTemplateForm({
} }
}; };
let initialWebhookKey = webhook_key;
const initialWebhookCredential = template?.summary_fields?.webhook_credential; const initialWebhookCredential = template?.summary_fields?.webhook_credential;
const storeWebhookValues = (form, webhookServiceValue) => { const storeWebhookValues = (form, webhookServiceValue) => {
if ( if (
webhookServiceValue === form.initialValues.webhook_service || webhookServiceValue === form.initialValues.webhook_service ||
@@ -135,12 +140,17 @@ function WorkflowJobTemplateForm({
form.initialValues.webhook_credential form.initialValues.webhook_credential
); );
setWebhookCredential(initialWebhookCredential); setWebhookCredential(initialWebhookCredential);
form.setFieldValue('webhook_url', form.initialValues.webhook_url); form.setFieldValue('webhook_url', form.initialValues.webhook_url);
form.setFieldValue('webhook_service', form.initialValues.webhook_service); form.setFieldValue('webhook_service', form.initialValues.webhook_service);
setWebHookService(form.initialValues.webhook_service);
setWebHookKey(initialWebhookKey); setWebHookKey(initialWebhookKey);
} else { } else {
form.setFieldValue('webhook_credential', null); form.setFieldValue('webhook_credential', null);
setWebhookCredential(null); setWebhookCredential(null);
form.setFieldValue( form.setFieldValue(
'webhook_url', 'webhook_url',
`${urlOrigin}/api/v2/workflow_job_templates/${template.id}/${webhookServiceValue}/` `${urlOrigin}/api/v2/workflow_job_templates/${template.id}/${webhookServiceValue}/`
@@ -232,36 +242,37 @@ function WorkflowJobTemplateForm({
{({ form }) => ( {({ form }) => (
<OrganizationLookup <OrganizationLookup
helperTextInvalid={form.errors.organization} helperTextInvalid={form.errors.organization}
onBlur={() => form.setFieldTouched('organization')}
onChange={value => { onChange={value => {
form.setFieldValue('organization', value?.id || null); form.setFieldValue('organization', value?.id || null);
setOrganization(value); setOrganization(value);
}} }}
value={organization} value={organization}
isValid={ isValid={!form.errors.organization}
!(form.touched.organization || form.errors.organization)
}
touched={form.touched.organization}
error={form.errors.organization}
/> />
)} )}
</Field> </Field>
<Field name="inventory"> <Field name="inventory">
{({ form }) => ( {({ form }) => (
<InventoryLookup <FormGroup
value={inventory} label={i18n._(t`Inventory`)}
tooltip={i18n._( fieldId="wfjt-inventory"
t`Select an inventory for the workflow. This inventory is applied to all job template nodes that prompt for an inventory.` >
)} <FieldTooltip
isValid={!(form.touched.inventory || form.errors.inventory)} content={i18n._(
helperTextInvalid={form.errors.inventory} t`Select an inventory for the workflow. This inventory is applied to all job template nodes that prompt for an inventory.`
onChange={value => { )}
form.setFieldValue('inventory', value?.id || null); />
setInventory(value); <InventoryLookup
form.setFieldValue('organizationId', value?.organization); value={inventory}
}} isValid={!form.errors.inventory}
error={form.errors.inventory} helperTextInvalid={form.errors.inventory}
/> onChange={value => {
form.setFieldValue('inventory', value?.id || null);
setInventory(value);
form.setFieldValue('organizationId', value?.organization);
}}
/>
</FormGroup>
)} )}
</Field> </Field>
<FormField <FormField
@@ -459,12 +470,7 @@ function WorkflowJobTemplateForm({
); );
setWebhookCredential(value); setWebhookCredential(value);
}} }}
isValid={ isValid={!form.errors.webhook_credential}
!(
form.touched.webhook_credential ||
form.errors.webhook_credential
)
}
helperTextInvalid={form.errors.webhook_credential} helperTextInvalid={form.errors.webhook_credential}
value={webhookCredential} value={webhookCredential}
/> />
@@ -473,7 +479,7 @@ function WorkflowJobTemplateForm({
)} )}
</FormColumnLayout> </FormColumnLayout>
)} )}
<FormSubmitError error={submitError} /> {submitError && <FormSubmitError error={submitError} />}
<FormActionGroup <FormActionGroup
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}