From c993c8b3b95e969b9f38c3d550d7e6bc9b3b3a5e Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 16 Jun 2021 16:50:02 -0400 Subject: [PATCH] Fixes bug where email notification ssl and tls were a dropdown instead of checkboxes --- .../AnsibleSelect/AnsibleSelect.jsx | 1 - .../src/components/DetailList/ArrayDetail.jsx | 2 +- .../NotificationTemplateDetail.jsx | 34 +- .../NotificationTemplateDetail.test.jsx | 102 ++++++ .../shared/NotificationTemplateForm.jsx | 9 - .../shared/NotificationTemplateForm.test.jsx | 112 ++++--- .../shared/TypeInputsSubForm.jsx | 41 +-- ...otification-template-default-messages.json | 302 ++++++++++++++++++ .../shared/typeFieldNames.js | 2 + 9 files changed, 518 insertions(+), 87 deletions(-) create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.test.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/shared/notification-template-default-messages.json diff --git a/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx b/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx index 2c63abf9e8..7fe6865f44 100644 --- a/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx +++ b/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx @@ -15,7 +15,6 @@ import { FormSelect, FormSelectOption } from '@patternfly/react-core'; function AnsibleSelect({ id, data, - isValid, onBlur, value, diff --git a/awx/ui_next/src/components/DetailList/ArrayDetail.jsx b/awx/ui_next/src/components/DetailList/ArrayDetail.jsx index a946a5976f..0b7df17d6a 100644 --- a/awx/ui_next/src/components/DetailList/ArrayDetail.jsx +++ b/awx/ui_next/src/components/DetailList/ArrayDetail.jsx @@ -25,7 +25,7 @@ function ArrayDetail({ label, value, dataCy }) { {vals.map(v => ( -
{v}
+
{v}
))}
diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx index 236ec0b656..2d6164a79c 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx @@ -1,7 +1,12 @@ import React, { useCallback } from 'react'; import { Link, useHistory } from 'react-router-dom'; - -import { Button } from '@patternfly/react-core'; +import { + Button, + TextList, + TextListItem, + TextListItemVariants, + TextListVariants, +} from '@patternfly/react-core'; import { t } from '@lingui/macro'; import AlertModal from '../../../components/AlertModal'; import { CardBody, CardActionsRow } from '../../../components/Card'; @@ -31,6 +36,23 @@ function NotificationTemplateDetail({ template, defaultMessages }) { messages, } = template; + const renderOptionsField = configuration.use_ssl || configuration.use_tls; + + const renderOptions = ( + + {configuration.use_ssl && ( + + {t`Use SSL`} + + )} + {configuration.use_tls && ( + + {t`Use TLS`} + + )} + + ); + const { request: deleteTemplate, isLoading, error: deleteError } = useRequest( useCallback(async () => { await NotificationTemplatesAPI.destroy(template.id); @@ -104,11 +126,9 @@ function NotificationTemplateDetail({ template, defaultMessages }) { value={configuration.timeout} dataCy="nt-detail-timeout" /> - + {renderOptionsField && ( + + )} )} {template.notification_type === 'grafana' && ( diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.test.jsx new file mode 100644 index 0000000000..3f4bd34fb3 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.test.jsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; +import NotificationTemplateDetail from './NotificationTemplateDetail'; +import defaultMessages from '../shared/notification-template-default-messages.json'; + +jest.mock('../../../api'); + +const mockTemplate = { + id: 1, + type: 'notification_template', + url: '/api/v2/notification_templates/1/', + related: { + named_url: '/api/v2/notification_templates/abc++Default/', + created_by: '/api/v2/users/2/', + modified_by: '/api/v2/users/2/', + test: '/api/v2/notification_templates/1/test/', + notifications: '/api/v2/notification_templates/1/notifications/', + copy: '/api/v2/notification_templates/1/copy/', + organization: '/api/v2/organizations/1/', + }, + summary_fields: { + organization: { + id: 1, + name: 'Default', + description: '', + }, + created_by: { + id: 2, + username: 'test', + first_name: '', + last_name: '', + }, + modified_by: { + id: 2, + username: 'test', + first_name: '', + last_name: '', + }, + user_capabilities: { + edit: true, + delete: true, + copy: true, + }, + recent_notifications: [], + }, + created: '2021-06-16T18:52:23.811374Z', + modified: '2021-06-16T18:53:37.631371Z', + name: 'abc', + description: 'foo description', + organization: 1, + notification_type: 'email', + notification_configuration: { + username: '', + password: '', + host: 'https://localhost', + recipients: ['foo@ansible.com'], + sender: 'bar@ansible.com', + port: 324, + timeout: 11, + use_ssl: true, + use_tls: true, + }, + messages: null, +}; + +describe('', () => { + let wrapper; + + beforeEach(async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should render Details', () => { + function assertDetail(label, value) { + expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label); + expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value); + } + assertDetail('Name', mockTemplate.name); + assertDetail('Description', mockTemplate.description); + expect( + wrapper + .find('Detail[label="Email Options"]') + .containsAllMatchingElements([
  • Use SSL
  • ,
  • Use TLS
  • ]) + ).toEqual(true); + }); +}); diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx index 6edaa4e97d..dc3535526f 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx @@ -118,10 +118,6 @@ function NotificationTemplateForm({ ); }; - let emailOptions = ''; - if (template.notification_type === 'email') { - emailOptions = template.notification_configuration?.use_ssl ? 'ssl' : 'tls'; - } const messages = template.messages || { workflow_approval: {} }; const defs = defaultMessages[template.notification_type || 'email']; const mergeDefaultMessages = (templ = {}, def) => { @@ -144,7 +140,6 @@ function NotificationTemplateForm({ ...template.notification_configuration, headers: headers ? JSON.stringify(headers, null, 2) : null, }, - emailOptions, organization: template.summary_fields?.organization, messages: { started: { ...mergeDefaultMessages(messages.started, defs.started) }, @@ -235,10 +230,6 @@ function normalizeTypeFields(values) { stripped[fieldName] = values.notification_configuration[fieldName]; } }); - if (values.notification_type === 'email') { - stripped.use_ssl = values.emailOptions === 'ssl'; - stripped.use_tls = !stripped.use_ssl; - } if (values.notification_type === 'webhook') { stripped.headers = stripped.headers ? JSON.parse(stripped.headers) : {}; } diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.test.jsx index 5b90fd5e8d..5d5825a110 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.test.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.test.jsx @@ -4,6 +4,7 @@ import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; import NotificationTemplateForm from './NotificationTemplateForm'; jest.mock('../../../api/models/NotificationTemplates'); +jest.mock('../../../api/models/Organizations'); const template = { id: 3, @@ -50,16 +51,19 @@ const defaultMessages = { }; describe('', () => { - test('should render form fields', () => { - const wrapper = mountWithContexts( - - ); + let wrapper; + test('should render form fields', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); expect(wrapper.find('input#notification-name').prop('value')).toEqual( 'Test Notification' @@ -77,26 +81,48 @@ describe('', () => { expect( wrapper.find('CustomMessagesSubForm').prop('defaultMessages') ).toEqual(defaultMessages); + + expect(wrapper.find('input#option-use-ssl').length).toBe(0); + expect(wrapper.find('input#option-use-tls').length).toBe(0); + + await act(async () => { + wrapper.find('AnsibleSelect#notification-type').invoke('onChange')( + { + target: { + name: 'notification_type', + value: 'email', + }, + }, + 'email' + ); + }); + + wrapper.update(); + + expect(wrapper.find('input#option-use-ssl').length).toBe(1); + expect(wrapper.find('input#option-use-tls').length).toBe(1); }); - test('should render custom messages fields', () => { - const wrapper = mountWithContexts( - { + await act(async () => { + wrapper = mountWithContexts( + - ); + }} + defaultMessages={defaultMessages} + detailUrl="/notification_templates/3/detail" + onSubmit={jest.fn()} + onCancel={jest.fn()} + /> + ); + }); expect( wrapper @@ -108,21 +134,23 @@ describe('', () => { test('should submit', async () => { const handleSubmit = jest.fn(); - const wrapper = mountWithContexts( - - ); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); await act(async () => { wrapper.find('FormActionGroup').invoke('onSubmit')(); diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx index b7f8b4a1be..e6f7393139 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx @@ -4,6 +4,7 @@ import { t } from '@lingui/macro'; import { useField } from 'formik'; import { FormGroup, Title } from '@patternfly/react-core'; import { + FormCheckboxLayout, FormColumnLayout, FormFullWidthLayout, SubFormLayout, @@ -56,10 +57,6 @@ TypeInputsSubForm.propTypes = { export default TypeInputsSubForm; function EmailFields() { - const [optionsField, optionsMeta] = useField({ - name: 'emailOptions', - validate: required(t`Select a value for this field`), - }); return ( <> - - + + + + + ); diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/notification-template-default-messages.json b/awx/ui_next/src/screens/NotificationTemplate/shared/notification-template-default-messages.json new file mode 100644 index 0000000000..e2bd0ccaed --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/notification-template-default-messages.json @@ -0,0 +1,302 @@ +{ + "type": "json", + "required": false, + "label": "Messages", + "help_text": "Optional custom messages for notification template.", + "filterable": true, + "default": { + "started": null, + "success": null, + "error": null, + "workflow_approval": null + }, + "email": { + "started": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": "{{ job_friendly_name }} #{{ job.id }} had status {{ job.status }}, view details at {{ url }}\n\n{{ job_metadata }}" + }, + "success": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": "{{ job_friendly_name }} #{{ job.id }} had status {{ job.status }}, view details at {{ url }}\n\n{{ job_metadata }}" + }, + "error": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": "{{ job_friendly_name }} #{{ job.id }} had status {{ job.status }}, view details at {{ url }}\n\n{{ job_metadata }}" + }, + "workflow_approval": { + "running": { + "message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}", + "body": "The approval node \"{{ approval_node_name }}\" needs review. This approval node can be viewed at: {{ workflow_url }}\n\n{{ job_metadata }}" + }, + "approved": { + "message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}", + "body": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}\n\n{{ job_metadata }}" + }, + "timed_out": { + "message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}", + "body": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}\n\n{{ job_metadata }}" + }, + "denied": { + "message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}", + "body": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}\n\n{{ job_metadata }}" + } + } + }, + "slack": { + "started": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "success": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "error": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "workflow_approval": { + "running": { + "message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}", + "body": null + }, + "approved": { + "message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}", + "body": null + }, + "timed_out": { + "message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}", + "body": null + }, + "denied": { + "message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}", + "body": null + } + } + }, + "twilio": { + "started": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "success": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "error": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "workflow_approval": { + "running": { + "message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}", + "body": null + }, + "approved": { + "message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}", + "body": null + }, + "timed_out": { + "message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}", + "body": null + }, + "denied": { + "message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}", + "body": null + } + } + }, + "pagerduty": { + "started": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": "{{ job_metadata }}" + }, + "success": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": "{{ job_metadata }}" + }, + "error": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": "{{ job_metadata }}" + }, + "workflow_approval": { + "running": { + "message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}", + "body": "The approval node \"{{ approval_node_name }}\" needs review. This approval node can be viewed at: {{ workflow_url }}\n\n{{ job_metadata }}" + }, + "approved": { + "message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}", + "body": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}\n\n{{ job_metadata }}" + }, + "timed_out": { + "message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}", + "body": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}\n\n{{ job_metadata }}" + }, + "denied": { + "message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}", + "body": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}\n\n{{ job_metadata }}" + } + } + }, + "grafana": { + "started": { + "body": "{{ job_metadata }}", + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}" + }, + "success": { + "body": "{{ job_metadata }}", + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}" + }, + "error": { + "body": "{{ job_metadata }}", + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}" + }, + "workflow_approval": { + "running": { + "message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}", + "body": "The approval node \"{{ approval_node_name }}\" needs review. This approval node can be viewed at: {{ workflow_url }}\n\n{{ job_metadata }}" + }, + "approved": { + "message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}", + "body": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}\n\n{{ job_metadata }}" + }, + "timed_out": { + "message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}", + "body": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}\n\n{{ job_metadata }}" + }, + "denied": { + "message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}", + "body": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}\n\n{{ job_metadata }}" + } + } + }, + "webhook": { + "started": { + "body": "{{ job_metadata }}" + }, + "success": { + "body": "{{ job_metadata }}" + }, + "error": { + "body": "{{ job_metadata }}" + }, + "workflow_approval": { + "running": { + "body": { + "body": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}" + } + }, + "approved": { + "body": { + "body": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}" + } + }, + "timed_out": { + "body": { + "body": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}" + } + }, + "denied": { + "body": { + "body": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}" + } + } + } + }, + "mattermost": { + "started": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "success": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "error": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "workflow_approval": { + "running": { + "message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}", + "body": null + }, + "approved": { + "message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}", + "body": null + }, + "timed_out": { + "message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}", + "body": null + }, + "denied": { + "message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}", + "body": null + } + } + }, + "rocketchat": { + "started": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "success": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "error": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "workflow_approval": { + "running": { + "message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}", + "body": null + }, + "approved": { + "message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}", + "body": null + }, + "timed_out": { + "message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}", + "body": null + }, + "denied": { + "message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}", + "body": null + } + } + }, + "irc": { + "started": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "success": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "error": { + "message": "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}", + "body": null + }, + "workflow_approval": { + "running": { + "message": "The approval node \"{{ approval_node_name }}\" needs review. This node can be viewed at: {{ workflow_url }}", + "body": null + }, + "approved": { + "message": "The approval node \"{{ approval_node_name }}\" was approved. {{ workflow_url }}", + "body": null + }, + "timed_out": { + "message": "The approval node \"{{ approval_node_name }}\" has timed out. {{ workflow_url }}", + "body": null + }, + "denied": { + "message": "The approval node \"{{ approval_node_name }}\" was denied. {{ workflow_url }}", + "body": null + } + } + } +} diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/typeFieldNames.js b/awx/ui_next/src/screens/NotificationTemplate/shared/typeFieldNames.js index 4dd991cfa9..6d4d3046a6 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/shared/typeFieldNames.js +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/typeFieldNames.js @@ -7,6 +7,8 @@ const typeFieldNames = { 'sender', 'port', 'timeout', + 'use_ssl', + 'use_tls', ], grafana: [ 'grafana_url',