From 6e648cf72f851891ad848fa065638d20642b418d Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Wed, 8 Apr 2020 10:30:42 -0400 Subject: [PATCH 1/2] Adds webhooks to jt form --- awx/ui_next/src/api/models/JobTemplates.js | 4 + awx/ui_next/src/components/Lookup/index.js | 1 + .../JobTemplateAdd/JobTemplateAdd.jsx | 14 +- .../JobTemplateAdd/JobTemplateAdd.test.jsx | 4 + .../JobTemplateEdit/JobTemplateEdit.jsx | 2 + .../JobTemplateEdit/JobTemplateEdit.test.jsx | 14 ++ awx/ui_next/src/screens/Template/Template.jsx | 6 + .../Template/shared/JobTemplateForm.jsx | 64 +++++- .../Template/shared/JobTemplateForm.test.jsx | 83 +++++++ .../Template/shared/WebhooksSubForm.jsx | 207 ++++++++++++++++++ 10 files changed, 395 insertions(+), 4 deletions(-) create mode 100644 awx/ui_next/src/screens/Template/shared/WebhooksSubForm.jsx diff --git a/awx/ui_next/src/api/models/JobTemplates.js b/awx/ui_next/src/api/models/JobTemplates.js index 522240c733..0e2eba8079 100644 --- a/awx/ui_next/src/api/models/JobTemplates.js +++ b/awx/ui_next/src/api/models/JobTemplates.js @@ -87,6 +87,10 @@ class JobTemplates extends SchedulesMixin( readWebhookKey(id) { return this.http.get(`${this.baseUrl}${id}/webhook_key/`); } + + updateWebhookKey(id) { + return this.http.post(`${this.baseUrl}${id}/webhook_key/`); + } } export default JobTemplates; diff --git a/awx/ui_next/src/components/Lookup/index.js b/awx/ui_next/src/components/Lookup/index.js index cde48e2bcd..9321fb08e9 100644 --- a/awx/ui_next/src/components/Lookup/index.js +++ b/awx/ui_next/src/components/Lookup/index.js @@ -3,3 +3,4 @@ export { default as InstanceGroupsLookup } from './InstanceGroupsLookup'; export { default as InventoryLookup } from './InventoryLookup'; export { default as ProjectLookup } from './ProjectLookup'; export { default as MultiCredentialsLookup } from './MultiCredentialsLookup'; +export { default as CredentialLookup } from './CredentialLookup'; diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx index 201fdb2b3c..0b959320fa 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx @@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom'; import { Card, PageSection } from '@patternfly/react-core'; import { CardBody } from '@components/Card'; import JobTemplateForm from '../shared/JobTemplateForm'; -import { JobTemplatesAPI } from '@api'; +import { JobTemplatesAPI, OrganizationsAPI } from '@api'; function JobTemplateAdd() { const [formSubmitError, setFormSubmitError] = useState(null); @@ -15,11 +15,13 @@ function JobTemplateAdd() { instanceGroups, initialInstanceGroups, credentials, + webhook_credential, ...remainingValues } = values; setFormSubmitError(null); remainingValues.project = remainingValues.project.id; + remainingValues.webhook_credential = webhook_credential?.id; try { const { data: { id, type }, @@ -36,6 +38,16 @@ function JobTemplateAdd() { } async function submitLabels(templateId, labels = [], orgId) { + if (!orgId) { + try { + const { + data: { results }, + } = await OrganizationsAPI.read(); + orgId = results[0].id; + } catch (err) { + throw err; + } + } const associationPromises = labels.map(label => JobTemplatesAPI.associateLabel(templateId, label, orgId) ); diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx index 504b815d87..dbc55e8580 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx @@ -152,6 +152,10 @@ describe('', () => { project: 2, playbook: 'Baz', inventory: 2, + webhook_credential: undefined, + webhook_key: '', + webhook_service: '', + webhook_url: '', }); }); diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx index 5b5c36cbcc..9848f53006 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx @@ -100,11 +100,13 @@ class JobTemplateEdit extends Component { instanceGroups, initialInstanceGroups, credentials, + webhook_credential, ...remainingValues } = values; this.setState({ formSubmitError: null }); remainingValues.project = values.project.id; + remainingValues.webhook_credential = webhook_credential?.id || null; try { await JobTemplatesAPI.update(template.id, remainingValues); await Promise.all([ diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx index 8c296b0630..8a8d7131fc 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx @@ -62,6 +62,12 @@ const mockJobTemplate = { type: 'job_template', use_fact_cache: false, verbosity: '0', + webhook_credential: null, + webhook_key: 'webhook Key', + webhook_service: 'gitlab', + related: { + webhook_receiver: '/api/v2/workflow_job_templates/57/gitlab/', + }, }; const mockRelatedCredentials = { @@ -245,6 +251,8 @@ describe('', () => { delete expected.summary_fields; delete expected.id; delete expected.type; + delete expected.related; + expected.webhook_url = `${window.location.origin}${mockJobTemplate.related.webhook_receiver}`; expect(JobTemplatesAPI.update).toHaveBeenCalledWith(1, expected); expect(JobTemplatesAPI.disassociateLabel).toHaveBeenCalledTimes(2); expect(JobTemplatesAPI.associateLabel).toHaveBeenCalledTimes(4); @@ -308,6 +316,12 @@ describe('', () => { { id: 1, kind: 'cloud', name: 'Foo' }, { id: 2, kind: 'ssh', name: 'Bar' }, ], + webhook_credential: { + id: 7, + name: 'webhook credential', + kind: 'github_token', + credential_type_id: 12, + }, }, }; await act(async () => diff --git a/awx/ui_next/src/screens/Template/Template.jsx b/awx/ui_next/src/screens/Template/Template.jsx index c916500685..b95cf64b88 100644 --- a/awx/ui_next/src/screens/Template/Template.jsx +++ b/awx/ui_next/src/screens/Template/Template.jsx @@ -45,6 +45,12 @@ function Template({ i18n, me, setBreadcrumb }) { role_level: 'notification_admin_role', }), ]); + if (data?.related?.webhook_key) { + const { + data: { webhook_key }, + } = await JobTemplatesAPI.readWebhookKey(templateId); + data.webhook_key = webhook_key; + } setBreadcrumb(data); return { diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index 37e40bee9a..621b5d9e64 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -40,6 +40,9 @@ import { import { JobTemplatesAPI, ProjectsAPI } from '@api'; import LabelSelect from './LabelSelect'; import PlaybookSelect from './PlaybookSelect'; +import WebhookSubForm from './WebhooksSubForm'; + +const { origin } = document.location; function JobTemplateForm({ template, @@ -59,6 +62,10 @@ function JobTemplateForm({ Boolean(template?.host_config_key) ); + const [enableWebhooks, setEnableWebhooks] = useState( + Boolean(template.webhook_service) + ); + const { values: formikValues } = useFormikContext(); const [jobTypeField, jobTypeMeta, jobTypeHelpers] = useField({ name: 'job_type', @@ -87,6 +94,10 @@ function JobTemplateForm({ ); const [jobTagsField, , jobTagsHelpers] = useField('job_tags'); const [skipTagsField, , skipTagsHelpers] = useField('skip_tags'); + const webhookService = useField('webhook_service'); + const webhookUrl = useField('webhook_url'); + const webhookKey = useField('webhook_key'); + const webhookCredential = useField('webhook_credential'); const { request: fetchProject, @@ -174,16 +185,23 @@ function JobTemplateForm({ ]; let callbackUrl; if (template?.related) { - const { origin } = document.location; const path = template.related.callback || `${template.url}callback`; callbackUrl = `${origin}${path}`; } - if (instanceGroupLoading || hasProjectLoading) { + if ( + instanceGroupLoading || + hasProjectLoading + // credentialContentLoading + ) { return ; } - if (instanceGroupError || projectContentError) { + if ( + instanceGroupError || + projectContentError + // credentialContentError + ) { return ; } @@ -498,6 +516,39 @@ function JobTemplateForm({ setAllowCallbacks(checked); }} /> + + {i18n._(t`Enable Webhook`)} +   + + + } + id="wfjt-enabled-webhooks" + isChecked={ + Boolean(webhookService[0].value) || enableWebhooks + } + onChange={checked => { + setEnableWebhooks(checked); + webhookService[2].setValue( + !checked ? '' : webhookService[1].initialValue + ); + webhookUrl[2].setValue( + !checked ? '' : webhookUrl[1].initialValue + ); + webhookKey[2].setValue( + !checked ? '' : webhookKey[1].initialValue + ); + webhookCredential[2].setValue( + !checked ? null : webhookCredential[1].initialValue + ); + }} + /> + {allowCallbacks && ( <> {callbackUrl && ( @@ -616,6 +668,12 @@ const FormikApp = withFormik({ instanceGroups: [], credentials: summary_fields.credentials || [], extra_vars: template.extra_vars || '---\n', + webhook_service: template.webhook_service || '', + webhook_url: template?.related?.webhook_receiver + ? `${origin}${template.related.webhook_receiver}` + : '', + webhook_key: template.webhook_key || '', + webhook_credential: template?.summary_fields?.webhook_credential || null, }; }, handleSubmit: async (values, { props, setErrors }) => { diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx index 383bef39f0..7a52aa16e2 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx @@ -2,6 +2,8 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { sleep } from '@testUtils/testUtils'; +import { Route } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; import JobTemplateForm from './JobTemplateForm'; import { LabelsAPI, JobTemplatesAPI, ProjectsAPI, CredentialsAPI } from '@api'; @@ -34,6 +36,10 @@ describe('', () => { { id: 2, kind: 'ssh', name: 'Bar' }, ], }, + related: { webhook_receiver: '/api/v2/workflow_job_templates/57/gitlab/' }, + webhook_key: 'webhook key', + webhook_service: 'github', + webhook_credential: 7, }; const mockInstanceGroups = [ { @@ -86,6 +92,9 @@ describe('', () => { JobTemplatesAPI.readInstanceGroups.mockReturnValue({ data: { results: mockInstanceGroups }, }); + JobTemplatesAPI.updateWebhookKey.mockReturnValue({ + data: { webhook_key: 'webhook key' }, + }); ProjectsAPI.readPlaybooks.mockReturnValue({ data: ['debug.yml'], }); @@ -209,6 +218,80 @@ describe('', () => { ]); }); + test('webhooks and enable concurrent jobs functions properly', async () => { + let wrapper; + const history = createMemoryHistory({ + initialEntries: ['/templates/job_template/1/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + ( + + )} + />, + { + context: { + router: { + history, + route: { + location: history.location, + match: { params: { id: 1 } }, + }, + }, + }, + } + ); + }); + act(() => { + wrapper.find('Checkbox[aria-label="Enable Webhook"]').invoke('onChange')( + true, + { + currentTarget: { value: true, type: 'change', checked: true }, + } + ); + }); + wrapper.update(); + expect( + wrapper.find('Checkbox[aria-label="Enable Webhook"]').prop('isChecked') + ).toBe(true); + + 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('webhook key'); + await act(() => + wrapper.find('Button[aria-label="Update webhook key"]').prop('onClick')() + ); + expect(JobTemplatesAPI.updateWebhookKey).toBeCalledWith('1'); + expect( + wrapper.find('TextInputBase[aria-label="Webhook URL"]').prop('value') + ).toContain('/api/v2/workflow_job_templates/57/gitlab/'); + + wrapper.update(); + + expect(wrapper.find('FormGroup[name="webhook_service"]').length).toBe(1); + + await act(async () => + wrapper.find('AnsibleSelect#webhook_service').prop('onChange')( + {}, + 'gitlab' + ) + ); + wrapper.update(); + + expect(wrapper.find('AnsibleSelect#webhook_service').prop('value')).toBe( + 'gitlab' + ); + }); + test('should call handleSubmit when Submit button is clicked', async () => { const handleSubmit = jest.fn(); let wrapper; diff --git a/awx/ui_next/src/screens/Template/shared/WebhooksSubForm.jsx b/awx/ui_next/src/screens/Template/shared/WebhooksSubForm.jsx new file mode 100644 index 0000000000..6430fc1f6a --- /dev/null +++ b/awx/ui_next/src/screens/Template/shared/WebhooksSubForm.jsx @@ -0,0 +1,207 @@ +import React, { useEffect, useCallback, useState } from 'react'; +import { SyncAltIcon } from '@patternfly/react-icons'; +import { useParams, useRouteMatch } from 'react-router-dom'; +import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; +import { + FormGroup, + TextInput, + InputGroup, + Button, +} from '@patternfly/react-core'; +import ContentError from '@components/ContentError'; +import ContentLoading from '@components/ContentLoading'; +import useRequest from '@util/useRequest'; +import { useField } from 'formik'; +import { FormColumnLayout } from '@components/FormLayout'; +import { CredentialLookup } from '@components/Lookup'; +import AnsibleSelect from '@components/AnsibleSelect'; +import { FieldTooltip } from '@components/FormField'; +import { JobTemplatesAPI, CredentialTypesAPI } from '@api'; + +function WebhookSubForm({ i18n, enableWebhooks }) { + const [contentError, setContentError] = useState(null); + const jtAddMatch = useRouteMatch('/templates/job_template/add'); + const { id } = useParams(); + + const { origin } = document.location; + + const [ + webhookServiceField, + webhookServiceMeta, + webhookServiceHelpers, + ] = useField('webhook_service'); + + const [webhookUrlField, , webhookUrlHelpers] = useField('webhook_url'); + const [webhookKeyField, webhookKeyMeta, webhookKeyHelpers] = useField( + 'webhook_key' + ); + const [ + webhookCredentialField, + webhookCredentialMeta, + webhookCredentialHelpers, + ] = useField('webhook_credential'); + + const { + request: loadCredentialType, + error, + isLoading, + result: credTypeId, + } = useRequest( + useCallback(async () => { + let results; + if (webhookServiceField.value) { + results = await CredentialTypesAPI.read({ + namespace: `${webhookServiceField.value}_token`, + }); + // 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; + }, [webhookServiceField.value]) + ); + + useEffect(() => { + loadCredentialType(); + }, [loadCredentialType]); + + const changeWebhookKey = async () => { + try { + const { + data: { webhook_key: key }, + } = await JobTemplatesAPI.updateWebhookKey(id); + webhookKeyHelpers.setValue(key); + } catch (err) { + setContentError(err); + } + }; + + const webhookServiceOptions = [ + { + value: '', + key: '', + label: i18n._(t`Choose a Webhook Service`), + isDisabled: true, + }, + { + value: 'github', + key: 'github', + label: i18n._(t`GitHub`), + isDisabled: false, + }, + { + value: 'gitlab', + key: 'gitlab', + label: i18n._(t`GitLab`), + isDisabled: false, + }, + ]; + + if (error || contentError) { + return ; + } + if (isLoading) { + return ; + } + return ( + enableWebhooks && ( + + + + { + webhookServiceHelpers.setValue(val); + webhookUrlHelpers.setValue( + `${origin}/api/v2/job_templates/${id}/${val}/` + ); + if (val === webhookServiceMeta.initialValue || val === '') { + webhookKeyHelpers.setValue(webhookKeyMeta.initialValue); + webhookCredentialHelpers.setValue( + webhookCredentialMeta.initialValue + ); + } else { + webhookKeyHelpers.setValue( + i18n + ._(t`a new webhook key will be generated on save.`) + .toUpperCase() + ); + webhookCredentialHelpers.setValue(null); + } + }} + /> + + {!jtAddMatch && ( + <> + + + + + + + + + + + + + )} + {credTypeId && ( + { + webhookCredentialHelpers.setValue(value || null); + }} + isValid={!webhookCredentialMeta.error} + helperTextInvalid={webhookCredentialMeta.error} + value={webhookCredentialField.value} + /> + )} + + ) + ); +} +export default withI18n()(WebhookSubForm); From 222fecc5f686d29de90b06e309eaeec048fbe371 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Mon, 13 Apr 2020 15:08:06 -0400 Subject: [PATCH 2/2] adds test for new webhook component --- awx/ui_next/src/screens/Template/Template.jsx | 2 +- .../Template/shared/JobTemplateForm.jsx | 42 ++---- .../Template/shared/JobTemplateForm.test.jsx | 43 ++++++ ...WebhooksSubForm.jsx => WebhookSubForm.jsx} | 141 +++++++++++------- .../Template/shared/WebhookSubForm.test.jsx | 124 +++++++++++++++ 5 files changed, 260 insertions(+), 92 deletions(-) rename awx/ui_next/src/screens/Template/shared/{WebhooksSubForm.jsx => WebhookSubForm.jsx} (60%) create mode 100644 awx/ui_next/src/screens/Template/shared/WebhookSubForm.test.jsx diff --git a/awx/ui_next/src/screens/Template/Template.jsx b/awx/ui_next/src/screens/Template/Template.jsx index b95cf64b88..80e4df8539 100644 --- a/awx/ui_next/src/screens/Template/Template.jsx +++ b/awx/ui_next/src/screens/Template/Template.jsx @@ -45,7 +45,7 @@ function Template({ i18n, me, setBreadcrumb }) { role_level: 'notification_admin_role', }), ]); - if (data?.related?.webhook_key) { + if (data.webhook_service && data?.related?.webhook_key) { const { data: { webhook_key }, } = await JobTemplatesAPI.readWebhookKey(templateId); diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index 621b5d9e64..0e96b8995d 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -40,7 +40,7 @@ import { import { JobTemplatesAPI, ProjectsAPI } from '@api'; import LabelSelect from './LabelSelect'; import PlaybookSelect from './PlaybookSelect'; -import WebhookSubForm from './WebhooksSubForm'; +import WebhookSubForm from './WebhookSubForm'; const { origin } = document.location; @@ -94,10 +94,6 @@ function JobTemplateForm({ ); const [jobTagsField, , jobTagsHelpers] = useField('job_tags'); const [skipTagsField, , skipTagsHelpers] = useField('skip_tags'); - const webhookService = useField('webhook_service'); - const webhookUrl = useField('webhook_url'); - const webhookKey = useField('webhook_key'); - const webhookCredential = useField('webhook_credential'); const { request: fetchProject, @@ -189,19 +185,11 @@ function JobTemplateForm({ callbackUrl = `${origin}${path}`; } - if ( - instanceGroupLoading || - hasProjectLoading - // credentialContentLoading - ) { + if (instanceGroupLoading || hasProjectLoading) { return ; } - if ( - instanceGroupError || - projectContentError - // credentialContentError - ) { + if (instanceGroupError || projectContentError) { return ; } @@ -530,23 +518,9 @@ function JobTemplateForm({ } id="wfjt-enabled-webhooks" - isChecked={ - Boolean(webhookService[0].value) || enableWebhooks - } + isChecked={enableWebhooks} onChange={checked => { setEnableWebhooks(checked); - webhookService[2].setValue( - !checked ? '' : webhookService[1].initialValue - ); - webhookUrl[2].setValue( - !checked ? '' : webhookUrl[1].initialValue - ); - webhookKey[2].setValue( - !checked ? '' : webhookKey[1].initialValue - ); - webhookCredential[2].setValue( - !checked ? null : webhookCredential[1].initialValue - ); }} /> ', () => { ); }); + test('webhooks should render properly, without data', async () => { + let wrapper; + const history = createMemoryHistory({ + initialEntries: ['/templates/job_template/1/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + ( + + )} + />, + { + context: { + router: { + history, + route: { + location: history.location, + match: { params: { id: 1 } }, + }, + }, + }, + } + ); + }); + expect( + wrapper.find('TextInputBase#template-webhook_key').prop('value') + ).toBe('A NEW WEBHOOK KEY WILL BE GENERATED ON SAVE.'); + expect( + wrapper.find('Button[aria-label="Update webhook key"]').prop('isDisabled') + ).toBe(true); + }); test('should call handleSubmit when Submit button is clicked', async () => { const handleSubmit = jest.fn(); let wrapper; diff --git a/awx/ui_next/src/screens/Template/shared/WebhooksSubForm.jsx b/awx/ui_next/src/screens/Template/shared/WebhookSubForm.jsx similarity index 60% rename from awx/ui_next/src/screens/Template/shared/WebhooksSubForm.jsx rename to awx/ui_next/src/screens/Template/shared/WebhookSubForm.jsx index 6430fc1f6a..7211990c90 100644 --- a/awx/ui_next/src/screens/Template/shared/WebhooksSubForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/WebhookSubForm.jsx @@ -1,6 +1,6 @@ -import React, { useEffect, useCallback, useState } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { SyncAltIcon } from '@patternfly/react-icons'; -import { useParams, useRouteMatch } from 'react-router-dom'; +import { useParams, useLocation } from 'react-router-dom'; import { t } from '@lingui/macro'; import { withI18n } from '@lingui/react'; import { @@ -20,9 +20,8 @@ import { FieldTooltip } from '@components/FormField'; import { JobTemplatesAPI, CredentialTypesAPI } from '@api'; function WebhookSubForm({ i18n, enableWebhooks }) { - const [contentError, setContentError] = useState(null); - const jtAddMatch = useRouteMatch('/templates/job_template/add'); - const { id } = useParams(); + const { id, templateType } = useParams(); + const { pathname } = useLocation(); const { origin } = document.location; @@ -32,7 +31,9 @@ function WebhookSubForm({ i18n, enableWebhooks }) { webhookServiceHelpers, ] = useField('webhook_service'); - const [webhookUrlField, , webhookUrlHelpers] = useField('webhook_url'); + const [webhookUrlField, webhookUrlMeta, webhookUrlHelpers] = useField( + 'webhook_url' + ); const [webhookKeyField, webhookKeyMeta, webhookKeyHelpers] = useField( 'webhook_key' ); @@ -65,17 +66,37 @@ function WebhookSubForm({ i18n, enableWebhooks }) { loadCredentialType(); }, [loadCredentialType]); - const changeWebhookKey = async () => { - try { + useEffect(() => { + if (enableWebhooks) { + webhookServiceHelpers.setValue(webhookServiceMeta.initialValue); + webhookUrlHelpers.setValue(webhookUrlMeta.initialValue); + webhookKeyHelpers.setValue(webhookKeyMeta.initialValue); + webhookCredentialHelpers.setValue(webhookCredentialMeta.initialValue); + } else { + webhookServiceHelpers.setValue(''); + webhookUrlHelpers.setValue(''); + webhookKeyHelpers.setValue(''); + webhookCredentialHelpers.setValue(null); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [enableWebhooks]); + + const { request: fetchWebhookKey, error: webhookKeyError } = useRequest( + useCallback(async () => { const { data: { webhook_key: key }, } = await JobTemplatesAPI.updateWebhookKey(id); webhookKeyHelpers.setValue(key); - } catch (err) { - setContentError(err); - } - }; + }, [webhookKeyHelpers, id]) + ); + const changeWebhookKey = async () => { + await fetchWebhookKey(); + }; + const isUpdateKeyDisabled = + pathname.endsWith('/add') || + webhookKeyMeta.initialValue === + 'A NEW WEBHOOK KEY WILL BE GENERATED ON SAVE.'; const webhookServiceOptions = [ { value: '', @@ -97,7 +118,7 @@ function WebhookSubForm({ i18n, enableWebhooks }) { }, ]; - if (error || contentError) { + if (error || webhookKeyError) { return ; } if (isLoading) { @@ -120,7 +141,11 @@ function WebhookSubForm({ i18n, enableWebhooks }) { onChange={(event, val) => { webhookServiceHelpers.setValue(val); webhookUrlHelpers.setValue( - `${origin}/api/v2/job_templates/${id}/${val}/` + pathname.endsWith('/add') + ? i18n + ._(t`a new webhook url will be generated on save.`) + .toUpperCase() + : `${origin}/api/v2/${templateType}s/${id}/${val}/` ); if (val === webhookServiceMeta.initialValue || val === '') { webhookKeyHelpers.setValue(webhookKeyMeta.initialValue); @@ -138,53 +163,53 @@ function WebhookSubForm({ i18n, enableWebhooks }) { }} /> - {!jtAddMatch && ( - <> - - + <> + + + + + + + - - - - - - - - - - )} + + + + + {credTypeId && ( ', () => { + let wrapper; + let history; + const initialValues = { + webhook_url: '/api/v2/job_templates/51/github/', + webhook_credential: { id: 1, name: 'Github credential' }, + webhook_service: 'github', + webhook_key: 'webhook key', + }; + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['templates/job_template/51/edit'], + }); + CredentialsAPI.read.mockResolvedValue({ + data: { results: [{ id: 12, name: 'Github credential' }] }, + }); + await act(async () => { + wrapper = mountWithContexts( + + + + + , + { + context: { + router: { + history, + route: { + location: { pathname: 'templates/job_template/51/edit' }, + match: { params: { id: 51, templateType: 'job_template' } }, + }, + }, + }, + } + ); + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + test('mounts properly', () => { + expect(wrapper.length).toBe(1); + }); + test('should render initial values properly', () => { + waitForElement(wrapper, 'Lookup__ChipHolder', el => el.lenth > 0); + expect(wrapper.find('AnsibleSelect').prop('value')).toBe('github'); + expect( + wrapper.find('TextInputBase[aria-label="Webhook URL"]').prop('value') + ).toContain('/api/v2/job_templates/51/github/'); + expect( + wrapper.find('TextInputBase[aria-label="wfjt-webhook-key"]').prop('value') + ).toBe('webhook key'); + expect( + wrapper + .find('Chip') + .find('span') + .text() + ).toBe('Github credential'); + }); + test('should make other credential type available', async () => { + CredentialsAPI.read.mockResolvedValue({ + data: { results: [{ id: 13, name: 'GitLab credential' }] }, + }); + await act(async () => + wrapper.find('AnsibleSelect').prop('onChange')({}, 'gitlab') + ); + expect(CredentialsAPI.read).toHaveBeenCalledWith({ + namespace: 'gitlab_token', + }); + wrapper.update(); + expect( + wrapper.find('TextInputBase[aria-label="Webhook URL"]').prop('value') + ).toContain('/api/v2/job_templates/51/gitlab/'); + expect( + wrapper.find('TextInputBase[aria-label="wfjt-webhook-key"]').prop('value') + ).toBe('A NEW WEBHOOK KEY WILL BE GENERATED ON SAVE.'); + }); + test('should have disabled button to update webhook key', async () => { + let newWrapper; + await act(async () => { + newWrapper = mountWithContexts( + + + + + , + { + context: { + router: { + history, + route: { + location: { pathname: 'templates/job_template/51/edit' }, + match: { params: { id: 51, templateType: 'job_template' } }, + }, + }, + }, + } + ); + }); + expect( + newWrapper + .find("Button[aria-label='Update webhook key']") + .prop('isDisabled') + ).toBe(true); + }); +});