From 09178dd5f21923999b494d6604d97c552730fb52 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Mon, 17 Aug 2020 14:16:18 -0700 Subject: [PATCH 01/17] flush out notification form and type subform --- awx/ui_next/src/components/Lookup/index.js | 1 + .../Credential/shared/CredentialForm.jsx | 4 +- .../NotificationTemplate.jsx | 23 +- .../NotificationTemplateDetail.jsx | 2 +- .../NotificationTemplateEdit.jsx | 20 +- .../shared/NotificationTemplateForm.jsx | 147 ++++- .../shared/TypeInputsSubForm.jsx | 528 ++++++++++++++++++ awx/ui_next/src/types.js | 24 + awx/ui_next/src/util/validators.jsx | 15 + awx/ui_next/src/util/validators.test.js | 21 + 10 files changed, 763 insertions(+), 22 deletions(-) create mode 100644 awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx diff --git a/awx/ui_next/src/components/Lookup/index.js b/awx/ui_next/src/components/Lookup/index.js index 2b9b147941..a2fcfbe570 100644 --- a/awx/ui_next/src/components/Lookup/index.js +++ b/awx/ui_next/src/components/Lookup/index.js @@ -6,3 +6,4 @@ export { default as MultiCredentialsLookup } from './MultiCredentialsLookup'; export { default as CredentialLookup } from './CredentialLookup'; export { default as ApplicationLookup } from './ApplicationLookup'; export { default as HostFilterLookup } from './HostFilterLookup'; +export { default as OrganizationLookup } from './OrganizationLookup'; diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx index 62193584d3..e8ee83b1dd 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx @@ -104,7 +104,7 @@ function CredentialFormFields({ error={orgMeta.error} /> { - const { data } = await NotificationTemplatesAPI.readDetail(templateId); - setBreadcrumb(data); - return data; + const [detail, options] = await Promise.all([ + NotificationTemplatesAPI.readDetail(templateId), + NotificationTemplatesAPI.readOptions(), + ]); + setBreadcrumb(detail.data); + return { + template: detail.data, + defaultMessages: options.data.actions.POST.messages, + }; }, [templateId, setBreadcrumb]), - null + { template: null, defaultMessages: null } ); useEffect(() => { @@ -88,11 +94,18 @@ function NotificationTemplate({ setBreadcrumb, i18n }) { to="/notification_templates/:id/details" exact /> + {/* + + */} {template && ( <> diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx index 951ba5bd8b..a70f691e02 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx @@ -234,7 +234,7 @@ function NotificationTemplateDetail({ i18n, template }) { - - {({ me }) => ( - - )} - + ); } diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx index c08caaa3e5..078c2aac6b 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx @@ -1,3 +1,146 @@ -export default function NotificationTemplateForm() { - // +import React, { useContext, useEffect, useState } from 'react'; +import { shape, func } from 'prop-types'; +import { Formik, useField } from 'formik'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Form, FormGroup } from '@patternfly/react-core'; + +import { OrganizationsAPI } from '../../../api'; +import { ConfigContext } from '../../../contexts/Config'; +import AnsibleSelect from '../../../components/AnsibleSelect'; +import ContentError from '../../../components/ContentError'; +import ContentLoading from '../../../components/ContentLoading'; +import FormField, { FormSubmitError } from '../../../components/FormField'; +import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup'; +import { OrganizationLookup } from '../../../components/Lookup'; +import { getAddedAndRemoved } from '../../../util/lists'; +import { required, minMaxValue } from '../../../util/validators'; +import { FormColumnLayout } from '../../../components/FormLayout'; +import TypeInputsSubForm from './TypeInputsSubForm'; +import { NotificationTemplate } from '../../../types'; + +function NotificationTemplateFormFields({ i18n, defaultMessages }) { + const [orgField, orgMeta, orgHelpers] = useField('organization'); + const [typeField, typeMeta, typeHelpers] = useField({ + name: 'notification_type', + validate: required(i18n._(t`Select a value for this field`), i18n), + }); + + return ( + <> + + + orgHelpers.setTouched()} + onChange={value => { + orgHelpers.setValue(value); + }} + value={orgField.value} + touched={orgMeta.touched} + error={orgMeta.error} + required + /> + + + + {typeField.value && } + + ); } + +function NotificationTemplateForm({ + template, + defaultMessages, + onSubmit, + onCancel, + submitError, + i18n, +}) { + const handleSubmit = values => { + console.log(values); + // onSubmit(values); + }; + + return ( + + {formik => ( +
+ + + + + +
+ )} +
+ ); +} + +NotificationTemplateForm.propTypes = { + template: NotificationTemplate, + defaultMessages: shape().isRequired, + onSubmit: func.isRequired, + onCancel: func.isRequired, + submitError: shape(), +}; + +NotificationTemplateForm.defaultProps = { + template: { + name: '', + description: '', + notification_type: '', + }, + submitError: null, +}; + +export default withI18n()(NotificationTemplateForm); diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx new file mode 100644 index 0000000000..1ed9eb2724 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx @@ -0,0 +1,528 @@ +import React from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { useField } from 'formik'; +import { FormGroup, Title } from '@patternfly/react-core'; +import { + FormCheckboxLayout, + FormColumnLayout, + FormFullWidthLayout, + SubFormLayout, +} from '../../../components/FormLayout'; +import FormField, { + PasswordField, + CheckboxField, + FieldTooltip, +} from '../../../components/FormField'; +import AnsibleSelect from '../../../components/AnsibleSelect'; +import CodeMirrorInput from '../../../components/CodeMirrorInput'; +import { + combine, + required, + requiredEmail, + url, +} from '../../../util/validators'; +import { NotificationType } from '../../../types'; + +const TypeFields = { + email: EmailFields, + grafana: GrafanaFields, + irc: IRCFields, + mattermost: MattermostFields, + pagerduty: PagerdutyFields, + rocketchat: RocketChatFields, + slack: SlackFields, + twilio: TwilioFields, + webhook: WebhookFields, +}; + +function TypeInputsSubForm({ type, i18n }) { + const Fields = TypeFields[type]; + return ( + + + {i18n._(t`Type Details`)} + + + + + + ); +} +TypeInputsSubForm.propTypes = { + type: NotificationType.isRequired, +}; + +export default withI18n()(TypeInputsSubForm); + +function EmailFields({ i18n }) { + const [optionsField, optionsMeta] = useField({ + name: 'emailOptions', + validate: required(i18n._(t`Select a value for this field`), i18n), + }); + return ( + <> + + + + + + + + + + + + ); +} + +function GrafanaFields({ i18n }) { + return ( + <> + + + + + + + + ); +} + +function IRCFields({ i18n }) { + return ( + <> + + + + + + + + ); +} + +function MattermostFields({ i18n }) { + return ( + <> + + + + + + + ); +} + +function PagerdutyFields({ i18n }) { + return ( + <> + + + + + + ); +} + +function RocketChatFields({ i18n }) { + return ( + <> + + + + + + ); +} + +function SlackFields({ i18n }) { + return ( + <> + + + + + ); +} + +function TwilioFields({ i18n }) { + return ( + <> + + + + + + ); +} + +function WebhookFields({ i18n }) { + const [methodField, methodMeta] = useField({ + name: 'notification_configuration.http_method', + validate: required(i18n._(t`Select a value for this field`), i18n), + }); + const [headersField, headersMeta, headersHelpers] = useField({ + name: 'notification_configuration.headers', + validate: required(i18n._(t`Select enter a value for this field`), i18n), + }); + return ( + <> + + + + + + + } + > + { + headersHelpers.setValue(value); + }} + mode="javascript" + rows="5" + /> + + + + + + + ); +} diff --git a/awx/ui_next/src/types.js b/awx/ui_next/src/types.js index e1fa5a5163..7f83e31490 100644 --- a/awx/ui_next/src/types.js +++ b/awx/ui_next/src/types.js @@ -367,3 +367,27 @@ export const CredentialType = shape({ namespace: string, inputs: shape({}).isRequired, }); + +export const NotificationType = oneOf([ + 'email', + 'grafana', + 'irc', + 'mattermost', + 'pagerduty', + 'rocketchat', + 'slack', + 'twilio', + 'webhook', +]); + +export const NotificationTemplate = shape({ + id: number.isRequired, + name: string.isRequired, + description: string, + url: string.isRequired, + organization: number.isRequired, + notification_type: NotificationType, + summary_fields: shape({ + organization: Organization, + }), +}); diff --git a/awx/ui_next/src/util/validators.jsx b/awx/ui_next/src/util/validators.jsx index 035b4dfb56..959740d317 100644 --- a/awx/ui_next/src/util/validators.jsx +++ b/awx/ui_next/src/util/validators.jsx @@ -76,6 +76,21 @@ export function integer(i18n) { }; } +export function url(i18n) { + return value => { + // URL regex from https://urlregex.com/ + if ( + // eslint-disable-next-line max-len + !/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www\.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w\-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[.!/\\\w]*))?)/.test( + value + ) + ) { + return i18n._(t`Please enter a valid URL`); + } + return undefined; + }; +} + export function combine(validators) { return value => { for (let i = 0; i < validators.length; i++) { diff --git a/awx/ui_next/src/util/validators.test.js b/awx/ui_next/src/util/validators.test.js index 11b1a3bfd9..fcde053dbb 100644 --- a/awx/ui_next/src/util/validators.test.js +++ b/awx/ui_next/src/util/validators.test.js @@ -4,6 +4,7 @@ import { maxLength, noWhiteSpace, integer, + url, combine, regExp, } from './validators'; @@ -111,6 +112,26 @@ describe('validators', () => { }); }); + test('url should reject incomplete url', () => { + expect(url(i18n)('abcd')).toEqual({ + id: 'Please enter a valid URL', + }); + }); + + test('url should accept fully qualified url', () => { + expect(url(i18n)('http://example.com/foo')).toBeUndefined(); + }); + + test('url should accept url with query params', () => { + expect(url(i18n)('https://example.com/foo?bar=baz')).toBeUndefined(); + }); + + test('url should reject short protocol', () => { + expect(url(i18n)('h://example.com/foo')).toEqual({ + id: 'Please enter a valid URL', + }); + }); + test('combine should run all validators', () => { const validators = [required(null, i18n), noWhiteSpace(i18n)]; expect(combine(validators)('')).toEqual({ From bb12e0a3a9e5831dde176141f502bde43f56687a Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Tue, 18 Aug 2020 12:36:28 -0700 Subject: [PATCH 02/17] fix initial values for notification type fields --- .../shared/NotificationTemplateForm.jsx | 38 ++++++++++++- .../shared/TypeInputsSubForm.jsx | 13 ++--- .../shared/typeFieldNames.js | 55 +++++++++++++++++++ 3 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 awx/ui_next/src/screens/NotificationTemplate/shared/typeFieldNames.js diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx index 078c2aac6b..300fea5a59 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx @@ -17,11 +17,12 @@ import { getAddedAndRemoved } from '../../../util/lists'; import { required, minMaxValue } from '../../../util/validators'; import { FormColumnLayout } from '../../../components/FormLayout'; import TypeInputsSubForm from './TypeInputsSubForm'; +import typeFieldNames, { initialConfigValues } from './typeFieldNames'; import { NotificationTemplate } from '../../../types'; function NotificationTemplateFormFields({ i18n, defaultMessages }) { const [orgField, orgMeta, orgHelpers] = useField('organization'); - const [typeField, typeMeta, typeHelpers] = useField({ + const [typeField, typeMeta] = useField({ name: 'notification_type', validate: required(i18n._(t`Select a value for this field`), i18n), }); @@ -97,16 +98,25 @@ function NotificationTemplateForm({ i18n, }) { const handleSubmit = values => { - console.log(values); - // onSubmit(values); + onSubmit(normalizeTypeFields(values)); }; + let emailOptions = ''; + if (template.notification_type === 'email') { + emailOptions = template.notification_configuration.use_ssl ? 'ssl' : 'tls'; + } + return ( @@ -144,3 +154,25 @@ NotificationTemplateForm.defaultProps = { }; export default withI18n()(NotificationTemplateForm); + +/* If the user filled in some of the Type Details fields, then switched + * to a different notification type, unecessary fields may be set in the + * notification_configuration — this function strips them off */ +function normalizeTypeFields(values) { + const stripped = {}; + const fields = typeFieldNames[values.notification_type]; + fields.foreach(fieldName => { + if (typeof values[fieldName] !== 'undefined') { + stripped[fieldName] = values[fieldName]; + } + }); + if (values.notification_type === 'email') { + stripped.use_ssl = values.emailOptions === 'ssl'; + stripped.use_tls = !stripped.use_ssl; + } + + return { + ...values, + notification_configuration: stripped, + }; +} diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx index 1ed9eb2724..253430da8d 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx @@ -1,10 +1,9 @@ import React from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { useField } from 'formik'; +import { useField, useFormikContext } from 'formik'; import { FormGroup, Title } from '@patternfly/react-core'; import { - FormCheckboxLayout, FormColumnLayout, FormFullWidthLayout, SubFormLayout, @@ -418,7 +417,7 @@ function TwilioFields({ i18n }) { { + typeFieldNames[key].forEach(fieldName => { + initialConfigValues[fieldName] = ''; + }); +}); + +export { initialConfigValues }; From ba95775deddb5a8a951e1403dd8d2c14402d594d Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Wed, 19 Aug 2020 16:11:58 -0700 Subject: [PATCH 03/17] add custom notification messages subform --- .../CodeMirrorInput/CodeMirrorField.jsx | 73 ++++++ .../src/components/CodeMirrorInput/index.js | 1 + .../NotificationTemplateEdit.jsx | 1 - .../shared/CustomMessagesSubForm.jsx | 213 ++++++++++++++++++ .../shared/NotificationTemplateForm.jsx | 90 +++++++- .../shared/TypeInputsSubForm.jsx | 2 +- 6 files changed, 371 insertions(+), 9 deletions(-) create mode 100644 awx/ui_next/src/components/CodeMirrorInput/CodeMirrorField.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx diff --git a/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorField.jsx b/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorField.jsx new file mode 100644 index 0000000000..60d7896a38 --- /dev/null +++ b/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorField.jsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { + string, + oneOfType, + object, + func, + bool, + node, + oneOf, + number, +} from 'prop-types'; +import { useField } from 'formik'; +import { FormGroup } from '@patternfly/react-core'; +import CodeMirrorInput from './CodeMirrorInput'; +import { FieldTooltip } from '../FormField'; + +function CodeMirrorField({ + id, + name, + label, + tooltip, + helperText, + validate, + isRequired, + mode, + ...rest +}) { + const [field, meta, helpers] = useField({ name, validate }); + const isValid = !(meta.touched && meta.error); + + return ( + } + > + { + helpers.setValue(value); + }} + mode={mode} + /> + + ); +} +CodeMirrorField.propTypes = { + helperText: string, + id: string.isRequired, + name: string.isRequired, + label: oneOfType([object, string]).isRequired, + validate: func, + isRequired: bool, + tooltip: node, + mode: oneOf(['javascript', 'yaml', 'jinja2']).isRequired, + rows: number, +}; + +CodeMirrorField.defaultProps = { + helperText: '', + validate: () => {}, + isRequired: false, + tooltip: null, + rows: 5, +}; + +export default CodeMirrorField; diff --git a/awx/ui_next/src/components/CodeMirrorInput/index.js b/awx/ui_next/src/components/CodeMirrorInput/index.js index 9cad016228..2c60b806f5 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/index.js +++ b/awx/ui_next/src/components/CodeMirrorInput/index.js @@ -1,6 +1,7 @@ import CodeMirrorInput from './CodeMirrorInput'; export default CodeMirrorInput; +export { default as CodeMirrorField } from './CodeMirrorField'; export { default as VariablesDetail } from './VariablesDetail'; export { default as VariablesInput } from './VariablesInput'; export { default as VariablesField } from './VariablesField'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx index e57a01fd4e..9445f9c7be 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; import { CardBody } from '../../../components/Card'; import { OrganizationsAPI } from '../../../api'; -import { Config } from '../../../contexts/Config'; import NotificationTemplateForm from '../shared/NotificationTemplateForm'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx new file mode 100644 index 0000000000..8bca667b82 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx @@ -0,0 +1,213 @@ +import 'styled-components/macro'; +import React from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { useField } from 'formik'; +import { + FormGroup, + Title, + Switch, + Text, + TextVariants, +} from '@patternfly/react-core'; +import { + FormColumnLayout, + FormFullWidthLayout, + SubFormLayout, +} from '../../../components/FormLayout'; +import FormField, { + PasswordField, + CheckboxField, + FieldTooltip, +} from '../../../components/FormField'; +import AnsibleSelect from '../../../components/AnsibleSelect'; +import { CodeMirrorField } from '../../../components/CodeMirrorInput'; +import { + combine, + required, + requiredEmail, + url, +} from '../../../util/validators'; +import { NotificationType } from '../../../types'; + +function CustomMessagesSubForm({ defaultMessages, type, i18n }) { + const [useCustomField, , useCustomHelpers] = useField('useCustomMessages'); + const showMessages = type !== 'webhook'; + const showBodies = ['email', 'pagerduty', 'webhook'].includes(type); + + return ( + <> + useCustomHelpers.setValue(!useCustomField.value)} + /> + {useCustomField.value && ( + + + + Use custom messages to change the content of notifications sent + when a job starts, succeeds, or fails. Use curly braces to access + information about the job:{' '} + + {'{{'} job_friendly_name {'}}'} + + ,{' '} + + {'{{'} url {'}}'} + + , or attributes of the job such as{' '} + + {'{{'} job.status {'}}'} + + . You may apply a number of possible variables in the message. + Refer to the{' '} + + Ansible Tower documentation + {' '} + for more details. + + + + {showMessages && ( + + )} + {showBodies && ( + + )} + {showMessages && ( + + )} + {showBodies && ( + + )} + {showMessages && ( + + )} + {showBodies && ( + + )} + {showMessages && ( + + )} + {showBodies && ( + + )} + {showMessages && ( + + )} + {showBodies && ( + + )} + {showMessages && ( + + )} + {showBodies && ( + + )} + {showMessages && ( + + )} + {showBodies && ( + + )} + + + )} + + ); +} + +export default withI18n()(CustomMessagesSubForm); diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx index 300fea5a59..0efe8b764b 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx @@ -1,22 +1,18 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React from 'react'; import { shape, func } from 'prop-types'; import { Formik, useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Form, FormGroup } from '@patternfly/react-core'; -import { OrganizationsAPI } from '../../../api'; -import { ConfigContext } from '../../../contexts/Config'; import AnsibleSelect from '../../../components/AnsibleSelect'; -import ContentError from '../../../components/ContentError'; -import ContentLoading from '../../../components/ContentLoading'; import FormField, { FormSubmitError } from '../../../components/FormField'; import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup'; import { OrganizationLookup } from '../../../components/Lookup'; -import { getAddedAndRemoved } from '../../../util/lists'; -import { required, minMaxValue } from '../../../util/validators'; +import { required } from '../../../util/validators'; import { FormColumnLayout } from '../../../components/FormLayout'; import TypeInputsSubForm from './TypeInputsSubForm'; +import CustomMessagesSubForm from './CustomMessagesSubForm'; import typeFieldNames, { initialConfigValues } from './typeFieldNames'; import { NotificationTemplate } from '../../../types'; @@ -85,6 +81,10 @@ function NotificationTemplateFormFields({ i18n, defaultMessages }) { />
{typeField.value && } + ); } @@ -105,6 +105,14 @@ function NotificationTemplateForm({ 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) => { + return { + message: templ.message || def.message || '', + body: templ.body || def.body || '', + }; + }; return ( @@ -155,6 +195,42 @@ NotificationTemplateForm.defaultProps = { export default withI18n()(NotificationTemplateForm); +function hasCustomMessages(messages, defaults) { + return ( + isCustomized(messages.started, defaults.started) || + isCustomized(messages.success, defaults.success) || + isCustomized(messages.error, defaults.error) || + isCustomized( + messages.workflow_approval.approved, + defaults.workflow_approval.approved + ) || + isCustomized( + messages.workflow_approval.denied, + defaults.workflow_approval.denied + ) || + isCustomized( + messages.workflow_approval.running, + defaults.workflow_approval.running + ) || + isCustomized( + messages.workflow_approval.timed_out, + defaults.workflow_approval.timed_out + ) + ); +} +function isCustomized(message, defaultMessage) { + if (!message) { + return false; + } + if (!message.message || message.message !== defaultMessage.message) { + return true; + } + if (!message.body || message.body !== defaultMessage.body) { + return true; + } + return false; +} + /* If the user filled in some of the Type Details fields, then switched * to a different notification type, unecessary fields may be set in the * notification_configuration — this function strips them off */ diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx index 253430da8d..23d21a5dcc 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { useField, useFormikContext } from 'formik'; +import { useField } from 'formik'; import { FormGroup, Title } from '@patternfly/react-core'; import { FormColumnLayout, From 19fc0d9a96609f94b3f22179a63ee7139742992e Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 20 Aug 2020 10:08:03 -0700 Subject: [PATCH 04/17] reset notification messages to defaults when switching to new type --- .../shared/CustomMessagesSubForm.jsx | 65 ++++++++++++------- .../shared/NotificationTemplateForm.jsx | 5 +- .../shared/TypeInputsSubForm.jsx | 6 +- 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx index 8bca667b82..9387a84a39 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx @@ -1,40 +1,59 @@ import 'styled-components/macro'; -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { useField } from 'formik'; +import { useField, useFormikContext } from 'formik'; +import { Switch, Text } from '@patternfly/react-core'; import { - FormGroup, - Title, - Switch, - Text, - TextVariants, -} from '@patternfly/react-core'; -import { - FormColumnLayout, FormFullWidthLayout, SubFormLayout, } from '../../../components/FormLayout'; -import FormField, { - PasswordField, - CheckboxField, - FieldTooltip, -} from '../../../components/FormField'; -import AnsibleSelect from '../../../components/AnsibleSelect'; import { CodeMirrorField } from '../../../components/CodeMirrorInput'; -import { - combine, - required, - requiredEmail, - url, -} from '../../../util/validators'; -import { NotificationType } from '../../../types'; function CustomMessagesSubForm({ defaultMessages, type, i18n }) { const [useCustomField, , useCustomHelpers] = useField('useCustomMessages'); const showMessages = type !== 'webhook'; const showBodies = ['email', 'pagerduty', 'webhook'].includes(type); + const { setFieldValue } = useFormikContext(); + const mountedRef = useRef(null); + useEffect( + function resetToDefaultMessages() { + if (!mountedRef.current) { + mountedRef.current = true; + return; + } + const defs = defaultMessages[type]; + + const resetFields = (name, defaults) => { + setFieldValue(`${name}.message`, defaults.message || ''); + setFieldValue(`${name}.body`, defaults.body || ''); + }; + + resetFields('messages.started', defs.started); + resetFields('messages.success', defs.success); + resetFields('messages.error', defs.error); + resetFields( + 'messages.workflow_approval.approved', + defs.workflow_approval.approved + ); + resetFields( + 'messages.workflow_approval.denied', + defs.workflow_approval.denied + ); + resetFields( + 'messages.workflow_approval.running', + defs.workflow_approval.running + ); + resetFields( + 'messages.workflow_approval.timed_out', + defs.workflow_approval.timed_out + ); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [type, setFieldValue] + ); + return ( <> (
- + @@ -187,7 +187,7 @@ function GrafanaFields({ i18n }) { label={i18n._(t`Tags for the annotation (optional)`)} name="notification_configuration.annotation_tags" type="textarea" - rows="3" + rows={3} tooltip={i18n._(t`Enter one Annotation Tag per line, without commas.`)} /> From 458d29a579443a2a5dc24d5496c8f4de5f70c0b6 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 20 Aug 2020 14:43:30 -0700 Subject: [PATCH 05/17] only submit customized notification messages if changed --- .../NotificationTemplateEdit.jsx | 20 +------ .../shared/NotificationTemplateForm.jsx | 60 +++++++++++++++++-- 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx index 9445f9c7be..91844a477c 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; import { CardBody } from '../../../components/Card'; -import { OrganizationsAPI } from '../../../api'; +import { NotificationTemplatesAPI } from '../../../api'; import NotificationTemplateForm from '../shared/NotificationTemplateForm'; @@ -11,23 +11,9 @@ function NotificationTemplateEdit({ template, defaultMessages }) { const history = useHistory(); const [formError, setFormError] = useState(null); - const handleSubmit = async ( - values, - groupsToAssociate, - groupsToDisassociate - ) => { + const handleSubmit = async values => { try { - await OrganizationsAPI.update(template.id, values); - await Promise.all( - groupsToAssociate.map(id => - OrganizationsAPI.associateInstanceGroup(template.id, id) - ) - ); - await Promise.all( - groupsToDisassociate.map(id => - OrganizationsAPI.disassociateInstanceGroup(template.id, id) - ) - ); + await NotificationTemplatesAPI.update(template.id, values); history.push(detailsUrl); } catch (error) { setFormError(error); diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx index f1ab928ca6..edbb591c4d 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx @@ -98,7 +98,8 @@ function NotificationTemplateForm({ i18n, }) { const handleSubmit = values => { - onSubmit(normalizeTypeFields(values)); + // TODO: convert list values to arrays (do it in the field itself?) + onSubmit(normalizeFields(values, defaultMessages)); }; let emailOptions = ''; @@ -225,24 +226,28 @@ function isCustomized(message, defaultMessage) { if (!message) { return false; } - if (!message.message || message.message !== defaultMessage.message) { + if (message.message && message.message !== defaultMessage.message) { return true; } - if (!message.body || message.body !== defaultMessage.body) { + if (message.body && message.body !== defaultMessage.body) { return true; } return false; } +function normalizeFields(values, defaultMessages) { + return normalizeTypeFields(normalizeMessageFields(values, defaultMessages)); +} + /* If the user filled in some of the Type Details fields, then switched * to a different notification type, unecessary fields may be set in the * notification_configuration — this function strips them off */ function normalizeTypeFields(values) { const stripped = {}; const fields = typeFieldNames[values.notification_type]; - fields.foreach(fieldName => { - if (typeof values[fieldName] !== 'undefined') { - stripped[fieldName] = values[fieldName]; + fields.forEach(fieldName => { + if (typeof values.notification_configuration[fieldName] !== 'undefined') { + stripped[fieldName] = values.notification_configuration[fieldName]; } }); if (values.notification_type === 'email') { @@ -255,3 +260,46 @@ function normalizeTypeFields(values) { notification_configuration: stripped, }; } + +function normalizeMessageFields(values, defaults) { + if (!values.useCustomMessages) { + return values; + } + const { messages } = values; + const defs = defaults[values.notification_type]; + + const nullIfDefault = (m, d) => { + return { + message: m.message === d.message ? null : m.message, + body: m.body === d.body ? null : m.body, + }; + }; + + const nonDefaultMessages = { + started: nullIfDefault(messages.started, defs.started), + success: nullIfDefault(messages.success, defs.success), + error: nullIfDefault(messages.error, defs.error), + workflow_approval: { + approved: nullIfDefault( + messages.workflow_approval.approved, + defs.workflow_approval.approved + ), + denied: nullIfDefault( + messages.workflow_approval.denied, + defs.workflow_approval.denied + ), + running: nullIfDefault( + messages.workflow_approval.running, + defs.workflow_approval.running + ), + timed_out: nullIfDefault( + messages.workflow_approval.timed_out, + defs.workflow_approval.timed_out + ), + }, + }; + return { + ...values, + messages: nonDefaultMessages, + }; +} From 9bb834a422561a23d94b7d9a0582475522da6f88 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Fri, 21 Aug 2020 09:24:32 -0700 Subject: [PATCH 06/17] create ArrayTextField component --- .../components/FormField/ArrayTextField.jsx | 69 +++++++++++++++++++ awx/ui_next/src/components/FormField/index.js | 1 + .../shared/NotificationTemplateForm.jsx | 1 - .../shared/TypeInputsSubForm.jsx | 11 +-- 4 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 awx/ui_next/src/components/FormField/ArrayTextField.jsx diff --git a/awx/ui_next/src/components/FormField/ArrayTextField.jsx b/awx/ui_next/src/components/FormField/ArrayTextField.jsx new file mode 100644 index 0000000000..9b94036e3f --- /dev/null +++ b/awx/ui_next/src/components/FormField/ArrayTextField.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useField } from 'formik'; +import { FormGroup, TextArea } from '@patternfly/react-core'; +import FieldTooltip from './FieldTooltip'; + +function ArrayTextField(props) { + const { + id, + helperText, + name, + label, + tooltip, + tooltipMaxWidth, + validate, + isRequired, + type, + ...rest + } = props; + + const [field, meta, helpers] = useField({ name, validate }); + const isValid = !(meta.touched && meta.error); + + return ( + } + > +