Adds subform restore functionality and addresses PR issues

This commit is contained in:
Alex Corey
2020-02-22 17:20:23 -05:00
parent 4ccca08cda
commit e90ee5113d
4 changed files with 184 additions and 174 deletions

View File

@@ -1,10 +1,9 @@
const InstanceGroupsMixin = parent => const InstanceGroupsMixin = parent =>
class extends parent { class extends parent {
readInstanceGroups(resourceId, params) { readInstanceGroups(resourceId, params) {
return this.http.get( return this.http.get(`${this.baseUrl}${resourceId}/instance_groups/`, {
`${this.baseUrl}${resourceId}/instance_groups/`, params,
params });
);
} }
associateInstanceGroup(resourceId, instanceGroupId) { associateInstanceGroup(resourceId, instanceGroupId) {

View File

@@ -25,6 +25,7 @@ class WorkflowJobTemplate extends Component {
contentError: null, contentError: null,
hasContentLoading: true, hasContentLoading: true,
template: null, template: null,
webhook_key: null,
}; };
this.loadTemplate = this.loadTemplate.bind(this); this.loadTemplate = this.loadTemplate.bind(this);
this.loadSchedules = this.loadSchedules.bind(this); this.loadSchedules = this.loadSchedules.bind(this);
@@ -66,7 +67,6 @@ class WorkflowJobTemplate extends Component {
); );
data.summary_fields.webhook_credential.kind = name; data.summary_fields.webhook_credential.kind = name;
} }
setBreadcrumb(data);
this.setState({ template: data }); this.setState({ template: data });
setBreadcrumb(data); setBreadcrumb(data);
} catch (err) { } catch (err) {
@@ -141,7 +141,7 @@ class WorkflowJobTemplate extends Component {
return ( return (
<PageSection> <PageSection>
<Card className="awx-c-card"> <Card>
{cardHeader} {cardHeader}
<Switch> <Switch>
<Redirect <Redirect

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 { useRouteMatch, useParams } from 'react-router-dom';
import { func, shape } from 'prop-types'; import { func, shape } from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
@@ -7,42 +9,34 @@ import { Formik, Field } from 'formik';
import { import {
Form, Form,
FormGroup, FormGroup,
Checkbox,
InputGroup, InputGroup,
Button, Button,
TextInput, TextInput,
Checkbox,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { required } from '@util/validators'; import { required } from '@util/validators';
import { SyncAltIcon } from '@patternfly/react-icons'; import { SyncAltIcon } from '@patternfly/react-icons';
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 FormField, { import FormField, {
FieldTooltip, FieldTooltip,
FormSubmitError, FormSubmitError,
} from '@components/FormField'; } from '@components/FormField';
import {
FormColumnLayout,
FormFullWidthLayout,
FormCheckboxLayout,
} from '@components/FormLayout';
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';
import { VariablesField } from '@components/CodeMirrorInput'; import { VariablesField } from '@components/CodeMirrorInput';
import FormActionGroup from '@components/FormActionGroup'; import FormActionGroup from '@components/FormActionGroup';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import styled from 'styled-components';
import LabelSelect from './LabelSelect'; import LabelSelect from './LabelSelect';
import CheckboxField from '../../../components/FormField/CheckboxField';
const GridFormGroup = styled(FormGroup)`
& > label {
grid-column: 1 / -1;
}
&& {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
`;
function WorkflowJobTemplateForm({ function WorkflowJobTemplateForm({
handleSubmit, handleSubmit,
@@ -53,25 +47,17 @@ function WorkflowJobTemplateForm({
submitError, submitError,
}) { }) {
const { id } = useParams(); const { id } = useParams();
const wfjtAddMatch = useRouteMatch('/templates/workflow_job_template/add');
const urlOrigin = window.location.origin; const urlOrigin = window.location.origin;
const [contentError, setContentError] = useState(null); const [contentError, setContentError] = useState(null);
const [inventory, setInventory] = useState( const [webhookKey, setWebHookKey] = useState(webhook_key);
template?.summary_fields?.inventory || null
);
const [organization, setOrganization] = useState(
template?.summary_fields?.organization || null
);
const [webHookKey, setWebHookKey] = useState(webhook_key);
const [credTypeId, setCredentialTypeId] = useState(); const [credTypeId, setCredentialTypeId] = useState();
const [webhookService, setWebHookService] = useState( const [webhookService, setWebHookService] = useState(
template.webhook_service || '' template.webhook_service || ''
); );
const [hasWebhooks, setHasWebhooks] = useState( const [hasWebhooks, setHasWebhooks] = useState(Boolean(webhookService));
webhookService !== '' || false
);
const [webhookCredential, setWebHookCredential] = useState(
template?.summary_fields?.webhook_credential || null
);
const webhookServiceOptions = [ const webhookServiceOptions = [
{ {
@@ -83,14 +69,12 @@ function WorkflowJobTemplateForm({
{ {
value: 'github', value: 'github',
key: 'github', key: 'github',
namespace: 'git_hub',
label: i18n._(t`GitHub`), label: i18n._(t`GitHub`),
isDisabled: false, isDisabled: false,
}, },
{ {
value: 'gitlab', value: 'gitlab',
key: 'gitlab', key: 'gitlab',
namespace: 'git_lab',
label: i18n._(t`Git Lab`), label: i18n._(t`Git Lab`),
isDisabled: false, isDisabled: false,
}, },
@@ -105,9 +89,7 @@ function WorkflowJobTemplateForm({
const { const {
data: { results }, data: { results },
} = await CredentialTypesAPI.read({ } = await CredentialTypesAPI.read({
namespace: webhookService.includes('hub') namespace: `${webhookService}_token`,
? 'github_token'
: 'gitlab_token',
}); });
setCredentialTypeId(results[0].id); setCredentialTypeId(results[0].id);
} catch (err) { } catch (err) {
@@ -128,20 +110,67 @@ function WorkflowJobTemplateForm({
} }
}; };
let initialWebhookKey = webhook_key;
const setWebhookValues = (form, webhookServiceValue) => {
if (
webhookServiceValue === form.initialValues.webhook_service ||
webhookServiceValue === ''
) {
form.setFieldValue(
'webhook_credential',
form.initialValues.webhook_credential
);
form.setFieldValue('webhook_url', form.initialValues.webhook_url);
form.setFieldValue('webhook_service', form.initialValues.webhook_service);
setWebHookKey(initialWebhookKey);
} else {
form.setFieldValue('webhook_credential', null);
form.setFieldValue(
'webhook_url',
`${urlOrigin}/api/v2/workflow_job_templates/${template.id}/${webhookServiceValue}/`
);
setWebHookKey('A NEW WEBHOOK KEY WILL BE GENERATED ON SAVE');
}
};
const handleWebhookEnablement = (
form,
enabledWebhooks,
webhookServiceValue
) => {
if (!enabledWebhooks) {
initialWebhookKey = webhookKey;
form.setFieldValue('webhook_credential', null);
form.setFieldValue('webhook_service', '');
form.setFieldValue('webhook_url', '');
setWebHookService('');
setWebHookKey('');
} else {
setWebhookValues(form, webhookServiceValue);
}
};
if (contentError) { if (contentError) {
return <ContentError error={contentError} />; return <ContentError error={contentError} />;
} }
return ( return (
<Formik <Formik
onSubmit={handleSubmit} onSubmit={values => {
values.webhook_credential = values?.webhook_credential?.id;
values.organization = values?.organization?.id;
values.inventory = values?.inventory?.id;
return handleSubmit(values);
}}
initialValues={{ initialValues={{
name: template.name || '', name: template.name || '',
description: template.description || '', description: template.description || '',
inventory: template.summary_fields?.inventory?.id || '', inventory: template?.summary_fields?.inventory || null,
organization: template.summary_fields?.organization?.id || '', organization: template?.summary_fields?.organization || null,
labels: template.summary_fields?.labels?.results || [], labels: template.summary_fields?.labels?.results || [],
variables: template.variables || '---', extra_vars: 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,
@@ -149,8 +178,8 @@ function WorkflowJobTemplateForm({
template?.related?.webhook_receiver && template?.related?.webhook_receiver &&
`${urlOrigin}${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 || null,
webhook_service: webhookService, webhook_service: template.webhook_service || '',
ask_limit_on_launch: template.ask_limit_on_launch || false, ask_limit_on_launch: template.ask_limit_on_launch || false,
ask_inventory_on_launch: template.ask_inventory_on_launch || false, ask_inventory_on_launch: template.ask_inventory_on_launch || false,
ask_variables_on_launch: template.ask_variables_on_launch || false, ask_variables_on_launch: template.ask_variables_on_launch || false,
@@ -159,7 +188,7 @@ function WorkflowJobTemplateForm({
> >
{formik => ( {formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}> <Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormRow> <FormColumnLayout>
<FormField <FormField
id="wfjt-name" id="wfjt-name"
name="name" name="name"
@@ -179,7 +208,7 @@ function WorkflowJobTemplateForm({
label={i18n._(t`Organization`)} label={i18n._(t`Organization`)}
name="organization" name="organization"
> >
{({ form }) => ( {({ form, field }) => (
<OrganizationLookup <OrganizationLookup
helperTextInvalid={form.errors.organization} helperTextInvalid={form.errors.organization}
isValid={ isValid={
@@ -187,33 +216,26 @@ function WorkflowJobTemplateForm({
} }
onBlur={() => form.setFieldTouched('organization')} onBlur={() => form.setFieldTouched('organization')}
onChange={value => { onChange={value => {
form.setFieldValue('organization', value?.id || null); form.setFieldValue('organization', value);
setOrganization(value);
}} }}
value={organization} value={field.value}
touched={form.touched.organization} touched={form.touched.organization}
error={form.errors.organization} error={form.errors.organization}
/> />
)} )}
</Field> </Field>
</FormRow>
<FormRow>
<Field name="inventory"> <Field name="inventory">
{({ form }) => ( {({ form, field }) => (
<InventoryLookup <InventoryLookup
value={inventory} value={field.value}
tooltip={i18n._( tooltip={i18n._(
t`Select an inventory for the workflow. This inventory is applied to all job template nodes that prompt for an inventory.` t`Select an inventory for the workflow. This inventory is applied to all job template nodes that prompt for an inventory.`
)} )}
isValid={!form.touched.inventory || !form.errors.inventory} isValid={!form.touched.inventory || !form.errors.inventory}
helperTextInvalid={form.errors.inventory} helperTextInvalid={form.errors.inventory}
onChange={value => { onChange={value => {
form.setFieldValue('inventory', value?.id || null); form.setFieldValue('inventory', value);
form.setFieldValue( form.setFieldValue('organizationId', value.organization);
'organizationId',
value?.organization || null
);
setInventory(value);
}} }}
error={form.errors.inventory} error={form.errors.inventory}
/> />
@@ -249,9 +271,8 @@ function WorkflowJobTemplateForm({
name="scmBranch" name="scmBranch"
/> />
</FormGroup> </FormGroup>
</FormRow> </FormColumnLayout>
<FormFullWidthLayout>
<FormRow>
<Field name="labels"> <Field name="labels">
{({ form, field }) => ( {({ form, field }) => (
<FormGroup <FormGroup
@@ -272,65 +293,92 @@ function WorkflowJobTemplateForm({
</FormGroup> </FormGroup>
)} )}
</Field> </Field>
</FormRow> </FormFullWidthLayout>
<FormRow> <FormFullWidthLayout>
<VariablesField <VariablesField
id="wfjt-variables" id="wfjt-variables"
name="variables" name="extra_vars"
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> </FormFullWidthLayout>
<GridFormGroup <FormCheckboxLayout
fieldId="options" fieldId="options"
isInline isInline
label={i18n._(t`Options`)} label={i18n._(t`Options`)}
css="margin-top: 20px" css="margin-top: 20px"
> >
<Checkbox <Field
id="wfjt-webhooks" id="wfjt-webhooks"
name="has_webhooks" name="hasWebhooks"
label={ label={i18n._(t`Webhooks`)}
<span> >
{i18n._(t`Webhooks`)} &nbsp; {({ form }) => (
<FieldTooltip
content={i18n._(
t`Enable webhook for this workflow job template.`
)}
/>
</span>
}
isChecked={hasWebhooks}
onChange={checked => {
setHasWebhooks(checked);
}}
/>
<Field name="allow_simultaneous">
{({ field, form }) => (
<Checkbox <Checkbox
name="allow_simultaneous" aria-label={i18n._(t`Webhooks`)}
id="wfjt-allow_simultaneous"
label={ label={
<span> <span>
{i18n._(t`Enable Concurrent Jobs`)} &nbsp; {i18n._(t`Webhooks`)}
&nbsp;
<FieldTooltip <FieldTooltip
content={i18n._( content={i18n._(
t`If enabled, simultaneous runs of this workflow job template will be allowed.` t`Enable webhook for this workflow job template.`
)} )}
/> />
</span> </span>
} }
isChecked={field.value} id="wfjt-enabled-webhooks"
onChange={value => { isChecked={
form.setFieldValue('allow_simultaneous', value); Boolean(form.values.webhook_service) || hasWebhooks
}
onChange={checked => {
setHasWebhooks(checked);
handleWebhookEnablement(form, checked, webhookService);
}} }}
/> />
)} )}
</Field> </Field>
</GridFormGroup>
<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 && ( {hasWebhooks && (
<> <FormColumnLayout>
<FormRow> <Field name="webhook_service">
{template.related && ( {({ form, field }) => (
<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={field.value}
onChange={(event, val) => {
setWebHookService(val);
setWebhookValues(form, val);
form.setFieldValue('webhook_service', val);
}}
/>
</FormGroup>
)}
</Field>
{!wfjtAddMatch && (
<>
<FormGroup <FormGroup
fieldId="wfjt-webhook-url" fieldId="wfjt-webhook-url"
id="wfjt-webhook-url" id="wfjt-webhook-url"
@@ -348,10 +396,9 @@ function WorkflowJobTemplateForm({
name="webhook_url" name="webhook_url"
label="" label=""
isReadOnly isReadOnly
value="asdfgh"
/> />
</FormGroup> </FormGroup>
)}
{template.related && (
<FormGroup <FormGroup
fieldId="wfjt-webhook-key" fieldId="wfjt-webhook-key"
type="text" type="text"
@@ -368,67 +415,39 @@ function WorkflowJobTemplateForm({
<TextInput <TextInput
isReadOnly isReadOnly
aria-label="wfjt-webhook-key" aria-label="wfjt-webhook-key"
value={webHookKey} value={webhookKey}
/> />
<Button variant="tertiary" onClick={changeWebhookKey}> <Button variant="tertiary" onClick={changeWebhookKey}>
<SyncAltIcon /> <SyncAltIcon />
</Button> </Button>
</InputGroup> </InputGroup>
</FormGroup> </FormGroup>
)} </>
<Field name="webhook_service"> )}
{({ form }) => ( {credTypeId && (
<Field name="webhook_credential">
{({ form, field }) => (
<FormGroup <FormGroup
name="webhook_service" fieldId="webhook_credential"
fieldId="webhook_service" id="webhook_credential"
helperTextInvalid={form.errors.webhook_service} name="webhook_credential"
label={i18n._(t`Webhook Service`)}
> >
<FieldTooltip <CredentialLookup
content={i18n._(t`Select a webhook service`)} label={i18n._(t`Webhook Credential`)}
/> tooltip={i18n._(
<AnsibleSelect t`Optionally select the credential to use to send status updates back to the webhook service.`
id="webhook_service" )}
data={webhookServiceOptions} credentialTypeId={credTypeId || null}
value={webhookService} onChange={value => {
onChange={(event, val) => { form.setFieldValue('webhook_credential', value);
setWebHookService(val);
setWebHookCredential(null);
form.setFieldValue('webhook_service', val);
form.setFieldValue('webhook_credential', null);
}} }}
value={field.value}
/> />
</FormGroup> </FormGroup>
)} )}
</Field> </Field>
</FormRow>
{credTypeId && (
<FormRow>
<Field name="webhook_credential">
{({ 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>
)} )}
</> </FormColumnLayout>
)} )}
<FormSubmitError error={submitError} /> <FormSubmitError error={submitError} />
<FormActionGroup <FormActionGroup

View File

@@ -75,22 +75,17 @@ describe('<WorkflowJobTemplateForm/>', () => {
}); });
test('all the fields render successfully', () => { test('all the fields render successfully', () => {
const fields = [ const fields = [
'name', 'FormField[name="name"]',
'description', 'FormField[name="description"]',
'organization', 'Field[name="organization"]',
'inventory', 'Field[name="inventory"]',
'limit', 'FormField[name="limit"]',
'scmBranch', 'FormField[name="scmBranch"]',
'labels', 'Field[name="labels"]',
'variables', 'VariablesField',
]; ];
const assertField = (field, index) => { const assertField = field => {
expect( expect(wrapper.find(`${field}`).length).toBe(1);
wrapper
.find('Field')
.at(index)
.prop('name')
).toBe(`${field}`);
}; };
fields.map((field, index) => assertField(field, index)); fields.map((field, index) => assertField(field, index));
}); });
@@ -162,12 +157,15 @@ describe('<WorkflowJobTemplateForm/>', () => {
test('webhooks and enable concurrent jobs functions properly', async () => { test('webhooks and enable concurrent jobs functions properly', async () => {
act(() => { act(() => {
wrapper.find('Checkbox[name="has_webhooks"]').invoke('onChange')(true); wrapper.find('Checkbox[aria-label="Webhooks"]').invoke('onChange')(true, {
wrapper.find('Checkbox[name="allow_simultaneous"]').invoke('onChange')( currentTarget: { value: true, type: 'change', checked: true },
true });
);
}); });
wrapper.update(); wrapper.update();
expect(
wrapper.find('Checkbox[aria-label="Webhooks"]').prop('isChecked')
).toBe(true);
expect( expect(
wrapper.find('input[aria-label="wfjt-webhook-key"]').prop('readOnly') wrapper.find('input[aria-label="wfjt-webhook-key"]').prop('readOnly')
).toBe(true); ).toBe(true);
@@ -188,12 +186,6 @@ describe('<WorkflowJobTemplateForm/>', () => {
wrapper.update(); 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); expect(wrapper.find('Field[name="webhook_service"]').length).toBe(1);
act(() => wrapper.find('AnsibleSelect').prop('onChange')({}, 'gitlab')); act(() => wrapper.find('AnsibleSelect').prop('onChange')({}, 'gitlab'));
@@ -203,7 +195,7 @@ describe('<WorkflowJobTemplateForm/>', () => {
}); });
test('handleSubmit is called on submit button click', async () => { test('handleSubmit is called on submit button click', async () => {
act(() => { act(() => {
wrapper.find('Formik').prop('onSubmit')(); wrapper.find('Formik').prop('onSubmit')({});
}); });
wrapper.update(); wrapper.update();
sleep(0); sleep(0);