mirror of
https://github.com/ansible/awx.git
synced 2026-03-13 15:09:32 -02:30
add custom notification messages subform
This commit is contained in:
@@ -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 (
|
||||||
|
<FormGroup
|
||||||
|
fieldId={id}
|
||||||
|
helperText={helperText}
|
||||||
|
helperTextInvalid={meta.error}
|
||||||
|
isRequired={isRequired}
|
||||||
|
validated={isValid ? 'default' : 'error'}
|
||||||
|
label={label}
|
||||||
|
labelIcon={<FieldTooltip content={tooltip} />}
|
||||||
|
>
|
||||||
|
<CodeMirrorInput
|
||||||
|
id="webhook-headers"
|
||||||
|
{...rest}
|
||||||
|
{...field}
|
||||||
|
onChange={value => {
|
||||||
|
helpers.setValue(value);
|
||||||
|
}}
|
||||||
|
mode={mode}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import CodeMirrorInput from './CodeMirrorInput';
|
import CodeMirrorInput from './CodeMirrorInput';
|
||||||
|
|
||||||
export default CodeMirrorInput;
|
export default CodeMirrorInput;
|
||||||
|
export { default as CodeMirrorField } from './CodeMirrorField';
|
||||||
export { default as VariablesDetail } from './VariablesDetail';
|
export { default as VariablesDetail } from './VariablesDetail';
|
||||||
export { default as VariablesInput } from './VariablesInput';
|
export { default as VariablesInput } from './VariablesInput';
|
||||||
export { default as VariablesField } from './VariablesField';
|
export { default as VariablesField } from './VariablesField';
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { CardBody } from '../../../components/Card';
|
import { CardBody } from '../../../components/Card';
|
||||||
import { OrganizationsAPI } from '../../../api';
|
import { OrganizationsAPI } from '../../../api';
|
||||||
import { Config } from '../../../contexts/Config';
|
|
||||||
|
|
||||||
import NotificationTemplateForm from '../shared/NotificationTemplateForm';
|
import NotificationTemplateForm from '../shared/NotificationTemplateForm';
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<Switch
|
||||||
|
id="toggle-custom-messages"
|
||||||
|
label={i18n._(t`Customize messages…`)}
|
||||||
|
isChecked={!!useCustomField.value}
|
||||||
|
onChange={() => useCustomHelpers.setValue(!useCustomField.value)}
|
||||||
|
/>
|
||||||
|
{useCustomField.value && (
|
||||||
|
<SubFormLayout>
|
||||||
|
<Text
|
||||||
|
className="pf-c-content"
|
||||||
|
css="margin-bottom: var(--pf-c-content--MarginBottom)"
|
||||||
|
>
|
||||||
|
<small>
|
||||||
|
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:{' '}
|
||||||
|
<code>
|
||||||
|
{'{{'} job_friendly_name {'}}'}
|
||||||
|
</code>
|
||||||
|
,{' '}
|
||||||
|
<code>
|
||||||
|
{'{{'} url {'}}'}
|
||||||
|
</code>
|
||||||
|
, or attributes of the job such as{' '}
|
||||||
|
<code>
|
||||||
|
{'{{'} job.status {'}}'}
|
||||||
|
</code>
|
||||||
|
. You may apply a number of possible variables in the message.
|
||||||
|
Refer to the{' '}
|
||||||
|
<a
|
||||||
|
href="https://docs.ansible.com/ansible-tower/latest/html/userguide/notifications.html#create-custom-notifications"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Ansible Tower documentation
|
||||||
|
</a>{' '}
|
||||||
|
for more details.
|
||||||
|
</small>
|
||||||
|
</Text>
|
||||||
|
<FormFullWidthLayout>
|
||||||
|
{showMessages && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="start-message"
|
||||||
|
name="messages.started.message"
|
||||||
|
label={i18n._(t`Start message`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showBodies && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="start-body"
|
||||||
|
name="messages.started.body"
|
||||||
|
label={i18n._(t`Start message body`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={6}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showMessages && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="success-message"
|
||||||
|
name="messages.success.message"
|
||||||
|
label={i18n._(t`Success message`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showBodies && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="success-body"
|
||||||
|
name="messages.success.body"
|
||||||
|
label={i18n._(t`Success message body`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={6}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showMessages && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="error-message"
|
||||||
|
name="messages.error.message"
|
||||||
|
label={i18n._(t`Error message`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showBodies && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="error-body"
|
||||||
|
name="messages.error.body"
|
||||||
|
label={i18n._(t`Error message body`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={6}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showMessages && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="wf-approved-message"
|
||||||
|
name="messages.workflow_approval.approved.message"
|
||||||
|
label={i18n._(t`Workflow approved message`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showBodies && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="wf-approved-body"
|
||||||
|
name="messages.workflow_approval.approved.body"
|
||||||
|
label={i18n._(t`Workflow approved message body`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={6}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showMessages && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="wf-denied-message"
|
||||||
|
name="messages.workflow_approval.denied.message"
|
||||||
|
label={i18n._(t`Workflow denied message`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showBodies && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="wf-denied-body"
|
||||||
|
name="messages.workflow_approval.denied.body"
|
||||||
|
label={i18n._(t`Workflow denied message body`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={6}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showMessages && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="wf-running-message"
|
||||||
|
name="messages.workflow_approval.running.message"
|
||||||
|
label={i18n._(t`Workflow pending message`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showBodies && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="wf-running-body"
|
||||||
|
name="messages.workflow_approval.running.body"
|
||||||
|
label={i18n._(t`Workflow pending message body`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={6}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showMessages && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="wf-timed-out-message"
|
||||||
|
name="messages.workflow_approval.timed_out.message"
|
||||||
|
label={i18n._(t`Workflow timed out message`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showBodies && (
|
||||||
|
<CodeMirrorField
|
||||||
|
id="wf-timed-out-body"
|
||||||
|
name="messages.workflow_approval.timed_out.body"
|
||||||
|
label={i18n._(t`Workflow timed out message body`)}
|
||||||
|
mode="jinja2"
|
||||||
|
rows={6}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormFullWidthLayout>
|
||||||
|
</SubFormLayout>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(CustomMessagesSubForm);
|
||||||
@@ -1,22 +1,18 @@
|
|||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { shape, func } from 'prop-types';
|
import { shape, func } from 'prop-types';
|
||||||
import { Formik, useField } from 'formik';
|
import { Formik, useField } from 'formik';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Form, FormGroup } from '@patternfly/react-core';
|
import { Form, FormGroup } from '@patternfly/react-core';
|
||||||
|
|
||||||
import { OrganizationsAPI } from '../../../api';
|
|
||||||
import { ConfigContext } from '../../../contexts/Config';
|
|
||||||
import AnsibleSelect from '../../../components/AnsibleSelect';
|
import AnsibleSelect from '../../../components/AnsibleSelect';
|
||||||
import ContentError from '../../../components/ContentError';
|
|
||||||
import ContentLoading from '../../../components/ContentLoading';
|
|
||||||
import FormField, { FormSubmitError } from '../../../components/FormField';
|
import FormField, { FormSubmitError } from '../../../components/FormField';
|
||||||
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
||||||
import { OrganizationLookup } from '../../../components/Lookup';
|
import { OrganizationLookup } from '../../../components/Lookup';
|
||||||
import { getAddedAndRemoved } from '../../../util/lists';
|
import { required } from '../../../util/validators';
|
||||||
import { required, minMaxValue } from '../../../util/validators';
|
|
||||||
import { FormColumnLayout } from '../../../components/FormLayout';
|
import { FormColumnLayout } from '../../../components/FormLayout';
|
||||||
import TypeInputsSubForm from './TypeInputsSubForm';
|
import TypeInputsSubForm from './TypeInputsSubForm';
|
||||||
|
import CustomMessagesSubForm from './CustomMessagesSubForm';
|
||||||
import typeFieldNames, { initialConfigValues } from './typeFieldNames';
|
import typeFieldNames, { initialConfigValues } from './typeFieldNames';
|
||||||
import { NotificationTemplate } from '../../../types';
|
import { NotificationTemplate } from '../../../types';
|
||||||
|
|
||||||
@@ -85,6 +81,10 @@ function NotificationTemplateFormFields({ i18n, defaultMessages }) {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
{typeField.value && <TypeInputsSubForm type={typeField.value} />}
|
{typeField.value && <TypeInputsSubForm type={typeField.value} />}
|
||||||
|
<CustomMessagesSubForm
|
||||||
|
defaultMessages={defaultMessages}
|
||||||
|
type={typeField.value}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -105,6 +105,14 @@ function NotificationTemplateForm({
|
|||||||
if (template.notification_type === 'email') {
|
if (template.notification_type === 'email') {
|
||||||
emailOptions = template.notification_configuration.use_ssl ? 'ssl' : 'tls';
|
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 (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
@@ -117,6 +125,38 @@ function NotificationTemplateForm({
|
|||||||
...template.notification_configuration,
|
...template.notification_configuration,
|
||||||
},
|
},
|
||||||
emailOptions,
|
emailOptions,
|
||||||
|
messages: {
|
||||||
|
started: { ...mergeDefaultMessages(messages.started, defs.started) },
|
||||||
|
success: { ...mergeDefaultMessages(messages.success, defs.success) },
|
||||||
|
error: { ...mergeDefaultMessages(messages.error, defs.error) },
|
||||||
|
workflow_approval: {
|
||||||
|
approved: {
|
||||||
|
...mergeDefaultMessages(
|
||||||
|
messages.workflow_approval.approved,
|
||||||
|
defs.workflow_approval.approved
|
||||||
|
),
|
||||||
|
},
|
||||||
|
denied: {
|
||||||
|
...mergeDefaultMessages(
|
||||||
|
messages.workflow_approval.denied,
|
||||||
|
defs.workflow_approval.denied
|
||||||
|
),
|
||||||
|
},
|
||||||
|
running: {
|
||||||
|
...mergeDefaultMessages(
|
||||||
|
messages.workflow_approval.running,
|
||||||
|
defs.workflow_approval.running
|
||||||
|
),
|
||||||
|
},
|
||||||
|
timed_out: {
|
||||||
|
...mergeDefaultMessages(
|
||||||
|
messages.workflow_approval.timed_out,
|
||||||
|
defs.workflow_approval.timed_out
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
useCustomMessages: hasCustomMessages(messages, defs),
|
||||||
}}
|
}}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
@@ -155,6 +195,42 @@ NotificationTemplateForm.defaultProps = {
|
|||||||
|
|
||||||
export default withI18n()(NotificationTemplateForm);
|
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
|
/* 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
|
* to a different notification type, unecessary fields may be set in the
|
||||||
* notification_configuration — this function strips them off */
|
* notification_configuration — this function strips them off */
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { useField, useFormikContext } from 'formik';
|
import { useField } from 'formik';
|
||||||
import { FormGroup, Title } from '@patternfly/react-core';
|
import { FormGroup, Title } from '@patternfly/react-core';
|
||||||
import {
|
import {
|
||||||
FormColumnLayout,
|
FormColumnLayout,
|
||||||
|
|||||||
Reference in New Issue
Block a user