mirror of
https://github.com/ansible/awx.git
synced 2026-03-09 05:29:26 -02:30
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:
@@ -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';
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user