mirror of
https://github.com/ansible/awx.git
synced 2026-05-14 12:57:40 -02:30
Merge pull request #7967 from keithjgrant/7876-notifications-form
Notifications form Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
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
|
||||||
|
id={`${id}-field`}
|
||||||
|
fieldId={id}
|
||||||
|
helperText={helperText}
|
||||||
|
helperTextInvalid={meta.error}
|
||||||
|
isRequired={isRequired}
|
||||||
|
validated={isValid ? 'default' : 'error'}
|
||||||
|
label={label}
|
||||||
|
labelIcon={<FieldTooltip content={tooltip} />}
|
||||||
|
>
|
||||||
|
<CodeMirrorInput
|
||||||
|
id={id}
|
||||||
|
{...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;
|
||||||
@@ -107,6 +107,7 @@ CodeMirrorInput.propTypes = {
|
|||||||
hasErrors: bool,
|
hasErrors: bool,
|
||||||
fullHeight: bool,
|
fullHeight: bool,
|
||||||
rows: number,
|
rows: number,
|
||||||
|
className: string,
|
||||||
};
|
};
|
||||||
CodeMirrorInput.defaultProps = {
|
CodeMirrorInput.defaultProps = {
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
@@ -114,6 +115,7 @@ CodeMirrorInput.defaultProps = {
|
|||||||
rows: 6,
|
rows: 6,
|
||||||
fullHeight: false,
|
fullHeight: false,
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
|
className: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CodeMirrorInput;
|
export default CodeMirrorInput;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
70
awx/ui_next/src/components/FormField/ArrayTextField.jsx
Normal file
70
awx/ui_next/src/components/FormField/ArrayTextField.jsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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);
|
||||||
|
const value = field.value || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
fieldId={id}
|
||||||
|
helperText={helperText}
|
||||||
|
helperTextInvalid={meta.error}
|
||||||
|
isRequired={isRequired}
|
||||||
|
validated={isValid ? 'default' : 'error'}
|
||||||
|
label={label}
|
||||||
|
labelIcon={<FieldTooltip content={tooltip} maxWidth={tooltipMaxWidth} />}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
id={id}
|
||||||
|
isRequired={isRequired}
|
||||||
|
validated={isValid ? 'default' : 'error'}
|
||||||
|
resizeOrientation="vertical"
|
||||||
|
{...rest}
|
||||||
|
{...field}
|
||||||
|
value={value.join('\n')}
|
||||||
|
onChange={val => {
|
||||||
|
helpers.setValue(val.split('\n').map(v => v.trim()));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayTextField.propTypes = {
|
||||||
|
helperText: PropTypes.string,
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
|
||||||
|
validate: PropTypes.func,
|
||||||
|
isRequired: PropTypes.bool,
|
||||||
|
tooltip: PropTypes.node,
|
||||||
|
tooltipMaxWidth: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
ArrayTextField.defaultProps = {
|
||||||
|
helperText: '',
|
||||||
|
validate: () => {},
|
||||||
|
isRequired: false,
|
||||||
|
tooltip: null,
|
||||||
|
tooltipMaxWidth: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArrayTextField;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { Alert } from '@patternfly/react-core';
|
import { Alert } from '@patternfly/react-core';
|
||||||
|
import { FormFullWidthLayout } from '../FormLayout';
|
||||||
|
|
||||||
const findErrorStrings = (obj, messages = []) => {
|
const findErrorStrings = (obj, messages = []) => {
|
||||||
if (typeof obj === 'string') {
|
if (typeof obj === 'string') {
|
||||||
@@ -35,7 +36,18 @@ function FormSubmitError({ error }) {
|
|||||||
typeof error.response.data === 'object' &&
|
typeof error.response.data === 'object' &&
|
||||||
Object.keys(error.response.data).length > 0
|
Object.keys(error.response.data).length > 0
|
||||||
) {
|
) {
|
||||||
const errorMessages = error.response.data;
|
const errorMessages = {};
|
||||||
|
Object.keys(error.response.data).forEach(fieldName => {
|
||||||
|
const errors = error.response.data[fieldName];
|
||||||
|
if (!errors) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(errors.length)) {
|
||||||
|
errorMessages[fieldName] = errors.join(' ');
|
||||||
|
} else {
|
||||||
|
errorMessages[fieldName] = errors;
|
||||||
|
}
|
||||||
|
});
|
||||||
setErrors(errorMessages);
|
setErrors(errorMessages);
|
||||||
|
|
||||||
const messages = findErrorStrings(error.response.data);
|
const messages = findErrorStrings(error.response.data);
|
||||||
@@ -52,15 +64,17 @@ function FormSubmitError({ error }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert
|
<FormFullWidthLayout>
|
||||||
variant="danger"
|
<Alert
|
||||||
isInline
|
variant="danger"
|
||||||
title={
|
isInline
|
||||||
Array.isArray(errorMessage)
|
title={
|
||||||
? errorMessage.map(msg => <div key={msg}>{msg}</div>)
|
Array.isArray(errorMessage)
|
||||||
: errorMessage
|
? errorMessage.map(msg => <div key={msg}>{msg}</div>)
|
||||||
}
|
: errorMessage
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
</FormFullWidthLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export { default as FieldTooltip } from './FieldTooltip';
|
|||||||
export { default as PasswordField } from './PasswordField';
|
export { default as PasswordField } from './PasswordField';
|
||||||
export { default as PasswordInput } from './PasswordInput';
|
export { default as PasswordInput } from './PasswordInput';
|
||||||
export { default as FormSubmitError } from './FormSubmitError';
|
export { default as FormSubmitError } from './FormSubmitError';
|
||||||
|
export { default as ArrayTextField } from './ArrayTextField';
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export { default as MultiCredentialsLookup } from './MultiCredentialsLookup';
|
|||||||
export { default as CredentialLookup } from './CredentialLookup';
|
export { default as CredentialLookup } from './CredentialLookup';
|
||||||
export { default as ApplicationLookup } from './ApplicationLookup';
|
export { default as ApplicationLookup } from './ApplicationLookup';
|
||||||
export { default as HostFilterLookup } from './HostFilterLookup';
|
export { default as HostFilterLookup } from './HostFilterLookup';
|
||||||
|
export { default as OrganizationLookup } from './OrganizationLookup';
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ function CredentialFormFields({
|
|||||||
error={orgMeta.error}
|
error={orgMeta.error}
|
||||||
/>
|
/>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
fieldId="credential-credentialType"
|
fieldId="credential-Type"
|
||||||
helperTextInvalid={credTypeMeta.error}
|
helperTextInvalid={credTypeMeta.error}
|
||||||
isRequired
|
isRequired
|
||||||
validated={
|
validated={
|
||||||
@@ -114,7 +114,7 @@ function CredentialFormFields({
|
|||||||
>
|
>
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
{...credTypeField}
|
{...credTypeField}
|
||||||
id="credential_type"
|
id="credential-type"
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
value: '',
|
value: '',
|
||||||
|
|||||||
@@ -136,23 +136,24 @@ describe('<CredentialForm />', () => {
|
|||||||
test('should display cred type subform when scm type select has a value', async () => {
|
test('should display cred type subform when scm type select has a value', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await wrapper
|
await wrapper
|
||||||
.find('AnsibleSelect[id="credential_type"]')
|
.find('AnsibleSelect[id="credential-type"]')
|
||||||
.invoke('onChange')(null, 1);
|
.invoke('onChange')(null, 1);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
machineFieldExpects();
|
machineFieldExpects();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await wrapper
|
await wrapper
|
||||||
.find('AnsibleSelect[id="credential_type"]')
|
.find('AnsibleSelect[id="credential-type"]')
|
||||||
.invoke('onChange')(null, 2);
|
.invoke('onChange')(null, 2);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
sourceFieldExpects();
|
sourceFieldExpects();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should update expected fields when gce service account json file uploaded', async () => {
|
test('should update expected fields when gce service account json file uploaded', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await wrapper
|
await wrapper
|
||||||
.find('AnsibleSelect[id="credential_type"]')
|
.find('AnsibleSelect[id="credential-type"]')
|
||||||
.invoke('onChange')(null, 10);
|
.invoke('onChange')(null, 10);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@@ -182,6 +183,7 @@ describe('<CredentialForm />', () => {
|
|||||||
'-----BEGIN PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END PRIVATE KEY-----\n'
|
'-----BEGIN PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END PRIVATE KEY-----\n'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should clear expected fields when file clear button clicked', async () => {
|
test('should clear expected fields when file clear button clicked', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper
|
wrapper
|
||||||
@@ -212,7 +214,7 @@ describe('<CredentialForm />', () => {
|
|||||||
test('should show error when error thrown parsing JSON', async () => {
|
test('should show error when error thrown parsing JSON', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await wrapper
|
await wrapper
|
||||||
.find('AnsibleSelect[id="credential_type"]')
|
.find('AnsibleSelect[id="credential-type"]')
|
||||||
.invoke('onChange')(null, 10);
|
.invoke('onChange')(null, 10);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@@ -243,7 +245,7 @@ describe('<CredentialForm />', () => {
|
|||||||
test('should show Test button when external credential type is selected', async () => {
|
test('should show Test button when external credential type is selected', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await wrapper
|
await wrapper
|
||||||
.find('AnsibleSelect[id="credential_type"]')
|
.find('AnsibleSelect[id="credential-type"]')
|
||||||
.invoke('onChange')(null, 21);
|
.invoke('onChange')(null, 21);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|||||||
@@ -24,24 +24,30 @@ function NotificationTemplate({ setBreadcrumb, i18n }) {
|
|||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const {
|
const {
|
||||||
result: template,
|
result: { template, defaultMessages },
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
request: fetchTemplate,
|
request: fetchTemplate,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const { data } = await NotificationTemplatesAPI.readDetail(templateId);
|
const [detail, options] = await Promise.all([
|
||||||
setBreadcrumb(data);
|
NotificationTemplatesAPI.readDetail(templateId),
|
||||||
return data;
|
NotificationTemplatesAPI.readOptions(),
|
||||||
|
]);
|
||||||
|
setBreadcrumb(detail.data);
|
||||||
|
return {
|
||||||
|
template: detail.data,
|
||||||
|
defaultMessages: options.data.actions.POST.messages,
|
||||||
|
};
|
||||||
}, [templateId, setBreadcrumb]),
|
}, [templateId, setBreadcrumb]),
|
||||||
null
|
{ template: null, defaultMessages: null }
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTemplate();
|
fetchTemplate();
|
||||||
}, [fetchTemplate]);
|
}, [fetchTemplate, location.pathname]);
|
||||||
|
|
||||||
if (error) {
|
if (!isLoading && error) {
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -60,7 +66,7 @@ function NotificationTemplate({ setBreadcrumb, i18n }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showCardHeader = !isLoading && !location.pathname.endsWith('edit');
|
const showCardHeader = !location.pathname.endsWith('edit');
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
name: (
|
name: (
|
||||||
@@ -93,6 +99,7 @@ function NotificationTemplate({ setBreadcrumb, i18n }) {
|
|||||||
<Route path="/notification_templates/:id/edit">
|
<Route path="/notification_templates/:id/edit">
|
||||||
<NotificationTemplateEdit
|
<NotificationTemplateEdit
|
||||||
template={template}
|
template={template}
|
||||||
|
defaultMessages={defaultMessages}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -1,5 +1,86 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useHistory, Link } from 'react-router-dom';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
import { CardBody } from '../../components/Card';
|
||||||
|
import { NotificationTemplatesAPI } from '../../api';
|
||||||
|
import useRequest from '../../util/useRequest';
|
||||||
|
import ContentError from '../../components/ContentError';
|
||||||
|
import NotificationTemplateForm from './shared/NotificationTemplateForm';
|
||||||
|
|
||||||
export default function NotificationTemplateAdd() {
|
function NotificationTemplateAdd({ i18n }) {
|
||||||
return <div />;
|
const history = useHistory();
|
||||||
|
const [formError, setFormError] = useState(null);
|
||||||
|
const {
|
||||||
|
result: defaultMessages,
|
||||||
|
error,
|
||||||
|
request: fetchDefaultMessages,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await NotificationTemplatesAPI.readOptions();
|
||||||
|
return data.actions.POST.messages;
|
||||||
|
}, [])
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDefaultMessages();
|
||||||
|
}, [fetchDefaultMessages]);
|
||||||
|
|
||||||
|
const handleSubmit = async values => {
|
||||||
|
try {
|
||||||
|
const { data } = await NotificationTemplatesAPI.create(values);
|
||||||
|
history.push(`/notification_templates/${data.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
setFormError(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
history.push('/notification_templates');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<ContentError error={error}>
|
||||||
|
{error.response.status === 404 && (
|
||||||
|
<span>
|
||||||
|
{i18n._(t`Notification Template not found.`)}{' '}
|
||||||
|
<Link to="/notification_templates">
|
||||||
|
{i18n._(t`View all Notification Templates.`)}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ContentError>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
{defaultMessages && (
|
||||||
|
<NotificationTemplateForm
|
||||||
|
defaultMessages={defaultMessages}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
submitError={formError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationTemplateAdd.contextTypes = {
|
||||||
|
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NotificationTemplateAdd as _NotificationTemplateAdd };
|
||||||
|
export default withI18n()(NotificationTemplateAdd);
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ function NotificationTemplateDetail({ i18n, template }) {
|
|||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Username`)}
|
label={i18n._(t`Username`)}
|
||||||
value={configuration.rocketchat_username}
|
value={configuration.rocketchat_username}
|
||||||
dataCy="nt-detail-pagerduty-rocketchat-username"
|
dataCy="nt-detail-rocketchat-username"
|
||||||
/>
|
/>
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Icon URL`)}
|
label={i18n._(t`Icon URL`)}
|
||||||
|
|||||||
@@ -2,33 +2,17 @@ import React, { useState } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
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 { NotificationTemplatesAPI } from '../../../api';
|
||||||
import { Config } from '../../../contexts/Config';
|
|
||||||
|
|
||||||
import NotificationTemplateForm from '../shared/NotificationTemplateForm';
|
import NotificationTemplateForm from '../shared/NotificationTemplateForm';
|
||||||
|
|
||||||
function NotificationTemplateEdit({ template }) {
|
function NotificationTemplateEdit({ template, defaultMessages }) {
|
||||||
const detailsUrl = `/notification_templates/${template.id}/details`;
|
const detailsUrl = `/notification_templates/${template.id}/details`;
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [formError, setFormError] = useState(null);
|
const [formError, setFormError] = useState(null);
|
||||||
|
|
||||||
const handleSubmit = async (
|
const handleSubmit = async values => {
|
||||||
values,
|
|
||||||
groupsToAssociate,
|
|
||||||
groupsToDisassociate
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
await OrganizationsAPI.update(template.id, values);
|
await NotificationTemplatesAPI.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)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
history.push(detailsUrl);
|
history.push(detailsUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFormError(error);
|
setFormError(error);
|
||||||
@@ -41,17 +25,13 @@ function NotificationTemplateEdit({ template }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Config>
|
<NotificationTemplateForm
|
||||||
{({ me }) => (
|
template={template}
|
||||||
<NotificationTemplateForm
|
defaultMessages={defaultMessages}
|
||||||
template={template}
|
onSubmit={handleSubmit}
|
||||||
onSubmit={handleSubmit}
|
onCancel={handleCancel}
|
||||||
onCancel={handleCancel}
|
submitError={formError}
|
||||||
me={me || {}}
|
/>
|
||||||
submitError={formError}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Config>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
import 'styled-components/macro';
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t, Trans } from '@lingui/macro';
|
||||||
|
import { useField, useFormikContext } from 'formik';
|
||||||
|
import { Switch, Text } from '@patternfly/react-core';
|
||||||
|
import {
|
||||||
|
FormFullWidthLayout,
|
||||||
|
SubFormLayout,
|
||||||
|
} from '../../../components/FormLayout';
|
||||||
|
import CodeMirrorField from '../../../components/CodeMirrorInput/CodeMirrorField';
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
<Trans>
|
||||||
|
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.
|
||||||
|
</Trans>
|
||||||
|
</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,3 +1,326 @@
|
|||||||
export default function NotificationTemplateForm() {
|
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 AnsibleSelect from '../../../components/AnsibleSelect';
|
||||||
|
import FormField, { FormSubmitError } from '../../../components/FormField';
|
||||||
|
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
||||||
|
import { OrganizationLookup } from '../../../components/Lookup';
|
||||||
|
import { required } from '../../../util/validators';
|
||||||
|
import { FormColumnLayout } from '../../../components/FormLayout';
|
||||||
|
import TypeInputsSubForm from './TypeInputsSubForm';
|
||||||
|
import CustomMessagesSubForm from './CustomMessagesSubForm';
|
||||||
|
import typeFieldNames, { initialConfigValues } from './typeFieldNames';
|
||||||
|
|
||||||
|
function NotificationTemplateFormFields({ i18n, defaultMessages }) {
|
||||||
|
const [orgField, orgMeta, orgHelpers] = useField('organization');
|
||||||
|
const [typeField, typeMeta] = useField({
|
||||||
|
name: 'notification_type',
|
||||||
|
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
id="notification-name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
label={i18n._(t`Name`)}
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="notification-description"
|
||||||
|
name="description"
|
||||||
|
type="text"
|
||||||
|
label={i18n._(t`Description`)}
|
||||||
|
/>
|
||||||
|
<OrganizationLookup
|
||||||
|
helperTextInvalid={orgMeta.error}
|
||||||
|
isValid={!orgMeta.touched || !orgMeta.error}
|
||||||
|
onBlur={() => orgHelpers.setTouched()}
|
||||||
|
onChange={value => {
|
||||||
|
orgHelpers.setValue(value);
|
||||||
|
}}
|
||||||
|
value={orgField.value}
|
||||||
|
touched={orgMeta.touched}
|
||||||
|
error={orgMeta.error}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormGroup
|
||||||
|
fieldId="notification-type"
|
||||||
|
helperTextInvalid={typeMeta.error}
|
||||||
|
isRequired
|
||||||
|
validated={!typeMeta.touched || !typeMeta.error ? 'default' : 'error'}
|
||||||
|
label={i18n._(t`Type`)}
|
||||||
|
>
|
||||||
|
<AnsibleSelect
|
||||||
|
{...typeField}
|
||||||
|
id="notification-type"
|
||||||
|
isValid={!typeMeta.touched || !typeMeta.error}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
key: 'none',
|
||||||
|
label: i18n._(t`Choose a Notification Type`),
|
||||||
|
isDisabled: true,
|
||||||
|
},
|
||||||
|
{ value: 'email', key: 'email', label: i18n._(t`E-mail`) },
|
||||||
|
{ value: 'grafana', key: 'grafana', label: 'Grafana' },
|
||||||
|
{ value: 'irc', key: 'irc', label: 'IRC' },
|
||||||
|
{ value: 'mattermost', key: 'mattermost', label: 'Mattermost' },
|
||||||
|
{ value: 'pagerduty', key: 'pagerduty', label: 'Pagerduty' },
|
||||||
|
{ value: 'rocketchat', key: 'rocketchat', label: 'Rocket.Chat' },
|
||||||
|
{ value: 'slack', key: 'slack', label: 'Slack' },
|
||||||
|
{ value: 'twilio', key: 'twilio', label: 'Twilio' },
|
||||||
|
{ value: 'webhook', key: 'webhook', label: 'Webhook' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
{typeField.value && <TypeInputsSubForm type={typeField.value} />}
|
||||||
|
<CustomMessagesSubForm
|
||||||
|
defaultMessages={defaultMessages}
|
||||||
|
type={typeField.value}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotificationTemplateForm({
|
||||||
|
template,
|
||||||
|
defaultMessages,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
submitError,
|
||||||
|
i18n,
|
||||||
|
}) {
|
||||||
|
const handleSubmit = values => {
|
||||||
|
onSubmit(
|
||||||
|
normalizeFields(
|
||||||
|
{
|
||||||
|
...values,
|
||||||
|
organization: values.organization?.id,
|
||||||
|
},
|
||||||
|
defaultMessages
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
return {
|
||||||
|
message: templ.message || def.message || '',
|
||||||
|
body: templ.body || def.body || '',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const { headers } = template?.notification_configuration || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
name: template.name,
|
||||||
|
description: template.description,
|
||||||
|
notification_type: template.notification_type,
|
||||||
|
notification_configuration: {
|
||||||
|
...initialConfigValues,
|
||||||
|
...template.notification_configuration,
|
||||||
|
headers: headers ? JSON.stringify(headers, null, 2) : null,
|
||||||
|
},
|
||||||
|
emailOptions,
|
||||||
|
organization: template.summary_fields?.organization,
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
{formik => (
|
||||||
|
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||||
|
<FormColumnLayout>
|
||||||
|
<NotificationTemplateFormFields
|
||||||
|
i18n={i18n}
|
||||||
|
defaultMessages={defaultMessages}
|
||||||
|
/>
|
||||||
|
<FormSubmitError error={submitError} />
|
||||||
|
<FormActionGroup
|
||||||
|
onCancel={onCancel}
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
/>
|
||||||
|
</FormColumnLayout>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationTemplateForm.propTypes = {
|
||||||
|
template: shape(),
|
||||||
|
defaultMessages: shape().isRequired,
|
||||||
|
onSubmit: func.isRequired,
|
||||||
|
onCancel: func.isRequired,
|
||||||
|
submitError: shape(),
|
||||||
|
};
|
||||||
|
|
||||||
|
NotificationTemplateForm.defaultProps = {
|
||||||
|
template: {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
notification_type: '',
|
||||||
|
},
|
||||||
|
submitError: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.notification_configuration[fieldName] !== 'undefined') {
|
||||||
|
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) : {};
|
||||||
|
}
|
||||||
|
const { emailOptions, ...rest } = values;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
notification_configuration: stripped,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMessageFields(values, defaults) {
|
||||||
|
const { useCustomMessages, ...rest } = values;
|
||||||
|
if (!useCustomMessages) {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
messages: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
...rest,
|
||||||
|
messages: nonDefaultMessages,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import NotificationTemplateForm from './NotificationTemplateForm';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/NotificationTemplates');
|
||||||
|
|
||||||
|
const template = {
|
||||||
|
id: 3,
|
||||||
|
notification_type: 'slack',
|
||||||
|
name: 'Test Notification',
|
||||||
|
description: 'a sample notification',
|
||||||
|
url: '/notification_templates/3',
|
||||||
|
organization: 1,
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
recent_notifications: [
|
||||||
|
{
|
||||||
|
status: 'success',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
organization: {
|
||||||
|
id: 1,
|
||||||
|
name: 'The Organization',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const messageDef = {
|
||||||
|
message: 'default message',
|
||||||
|
body: 'default body',
|
||||||
|
};
|
||||||
|
const defaults = {
|
||||||
|
started: messageDef,
|
||||||
|
success: messageDef,
|
||||||
|
error: messageDef,
|
||||||
|
workflow_approval: {
|
||||||
|
approved: messageDef,
|
||||||
|
denied: messageDef,
|
||||||
|
running: messageDef,
|
||||||
|
timed_out: messageDef,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const defaultMessages = {
|
||||||
|
email: defaults,
|
||||||
|
slack: defaults,
|
||||||
|
twilio: defaults,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<NotificationTemplateForm />', () => {
|
||||||
|
test('should render form fields', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<NotificationTemplateForm
|
||||||
|
template={template}
|
||||||
|
defaultMessages={defaultMessages}
|
||||||
|
detailUrl="/notification_templates/3/detail"
|
||||||
|
onSubmit={jest.fn()}
|
||||||
|
onCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('input#notification-name').prop('value')).toEqual(
|
||||||
|
'Test Notification'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper.find('input#notification-description').prop('value')
|
||||||
|
).toEqual('a sample notification');
|
||||||
|
expect(wrapper.find('OrganizationLookup').prop('value')).toEqual({
|
||||||
|
id: 1,
|
||||||
|
name: 'The Organization',
|
||||||
|
});
|
||||||
|
expect(wrapper.find('AnsibleSelect').prop('value')).toEqual('slack');
|
||||||
|
expect(wrapper.find('TypeInputsSubForm').prop('type')).toEqual('slack');
|
||||||
|
expect(wrapper.find('CustomMessagesSubForm').prop('type')).toEqual('slack');
|
||||||
|
expect(
|
||||||
|
wrapper.find('CustomMessagesSubForm').prop('defaultMessages')
|
||||||
|
).toEqual(defaultMessages);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should submit', async () => {
|
||||||
|
const handleSubmit = jest.fn();
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<NotificationTemplateForm
|
||||||
|
template={{
|
||||||
|
...template,
|
||||||
|
notification_configuration: {
|
||||||
|
channels: ['#foo'],
|
||||||
|
token: 'abc123',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
defaultMessages={defaultMessages}
|
||||||
|
detailUrl="/notification_templates/3/detail"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('FormActionGroup').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(handleSubmit).toHaveBeenCalledWith({
|
||||||
|
name: 'Test Notification',
|
||||||
|
description: 'a sample notification',
|
||||||
|
organization: 1,
|
||||||
|
notification_type: 'slack',
|
||||||
|
notification_configuration: {
|
||||||
|
channels: ['#foo'],
|
||||||
|
hex_color: '',
|
||||||
|
token: 'abc123',
|
||||||
|
},
|
||||||
|
messages: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,509 @@
|
|||||||
|
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 {
|
||||||
|
FormColumnLayout,
|
||||||
|
FormFullWidthLayout,
|
||||||
|
SubFormLayout,
|
||||||
|
} from '../../../components/FormLayout';
|
||||||
|
import FormField, {
|
||||||
|
PasswordField,
|
||||||
|
CheckboxField,
|
||||||
|
ArrayTextField,
|
||||||
|
} from '../../../components/FormField';
|
||||||
|
import AnsibleSelect from '../../../components/AnsibleSelect';
|
||||||
|
import { CodeMirrorField } from '../../../components/CodeMirrorInput';
|
||||||
|
import {
|
||||||
|
combine,
|
||||||
|
required,
|
||||||
|
requiredEmail,
|
||||||
|
url,
|
||||||
|
minMaxValue,
|
||||||
|
} 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 (
|
||||||
|
<SubFormLayout>
|
||||||
|
<Title size="md" headingLevel="h4">
|
||||||
|
{i18n._(t`Type Details`)}
|
||||||
|
</Title>
|
||||||
|
<FormColumnLayout>
|
||||||
|
<Fields i18n={i18n} />
|
||||||
|
</FormColumnLayout>
|
||||||
|
</SubFormLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
id="email-username"
|
||||||
|
label={i18n._(t`Username`)}
|
||||||
|
name="notification_configuration.username"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<PasswordField
|
||||||
|
id="email-password"
|
||||||
|
label={i18n._(t`Password`)}
|
||||||
|
name="notification_configuration.password"
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="email-host"
|
||||||
|
label={i18n._(t`Host`)}
|
||||||
|
name="notification_configuration.host"
|
||||||
|
type="text"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<ArrayTextField
|
||||||
|
id="email-recipients"
|
||||||
|
label={i18n._(t`Recipient list`)}
|
||||||
|
name="notification_configuration.recipients"
|
||||||
|
type="textarea"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
rows={3}
|
||||||
|
tooltip={i18n._(t`Enter one email address per line to create a recipient
|
||||||
|
list for this type of notification.`)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="email-sender"
|
||||||
|
label={i18n._(t`Sender e-mail`)}
|
||||||
|
name="notification_configuration.sender"
|
||||||
|
type="text"
|
||||||
|
validate={requiredEmail(i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="email-port"
|
||||||
|
label={i18n._(t`Port`)}
|
||||||
|
name="notification_configuration.port"
|
||||||
|
type="number"
|
||||||
|
validate={combine([required(null, i18n), minMaxValue(1, 65535, i18n)])}
|
||||||
|
isRequired
|
||||||
|
min="0"
|
||||||
|
max="65535"
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="email-timeout"
|
||||||
|
label={i18n._(t`Timeout`)}
|
||||||
|
name="notification_configuration.timeout"
|
||||||
|
type="number"
|
||||||
|
validate={combine([required(null, i18n), minMaxValue(1, 120, i18n)])}
|
||||||
|
isRequired
|
||||||
|
min="1"
|
||||||
|
max="120"
|
||||||
|
tooltip={i18n._(t`The amount of time (in seconds) before the email
|
||||||
|
notification stops trying to reach the host and times out. Ranges
|
||||||
|
from 1 to 120 seconds.`)}
|
||||||
|
/>
|
||||||
|
<FormGroup
|
||||||
|
fieldId="email-options"
|
||||||
|
helperTextInvalid={optionsMeta.error}
|
||||||
|
isRequired
|
||||||
|
validated={
|
||||||
|
!optionsMeta.touched || !optionsMeta.error ? 'default' : 'error'
|
||||||
|
}
|
||||||
|
label={i18n._(t`E-mail options`)}
|
||||||
|
>
|
||||||
|
<AnsibleSelect
|
||||||
|
{...optionsField}
|
||||||
|
id="email-options"
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
key: '',
|
||||||
|
label: i18n._(t`Choose an email option`),
|
||||||
|
isDisabled: true,
|
||||||
|
},
|
||||||
|
{ value: 'tls', key: 'tls', label: i18n._(t`Use TLS`) },
|
||||||
|
{ value: 'ssl', key: 'ssl', label: i18n._(t`Use SSL`) },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GrafanaFields({ i18n }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
id="grafana-url"
|
||||||
|
label={i18n._(t`Grafana URL`)}
|
||||||
|
name="notification_configuration.grafana_url"
|
||||||
|
type="text"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
tooltip={i18n._(t`The base URL of the Grafana server - the
|
||||||
|
/api/annotations endpoint will be added automatically to the base
|
||||||
|
Grafana URL.`)}
|
||||||
|
/>
|
||||||
|
<PasswordField
|
||||||
|
id="grafana-key"
|
||||||
|
label={i18n._(t`Grafana API key`)}
|
||||||
|
name="notification_configuration.grafana_key"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="grafana-dashboard-id"
|
||||||
|
label={i18n._(t`ID of the dashboard (optional)`)}
|
||||||
|
name="notification_configuration.dashboardId"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="grafana-panel-id"
|
||||||
|
label={i18n._(t`ID of the panel (optional)`)}
|
||||||
|
name="notification_configuration.panelId"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<ArrayTextField
|
||||||
|
id="grafana-tags"
|
||||||
|
label={i18n._(t`Tags for the annotation (optional)`)}
|
||||||
|
name="notification_configuration.annotation_tags"
|
||||||
|
type="textarea"
|
||||||
|
rows={3}
|
||||||
|
tooltip={i18n._(t`Enter one Annotation Tag per line, without commas.`)}
|
||||||
|
/>
|
||||||
|
<CheckboxField
|
||||||
|
id="grafana-ssl"
|
||||||
|
label={i18n._(t`Disable SSL verification`)}
|
||||||
|
name="notification_configuration.grafana_no_verify_ssl"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function IRCFields({ i18n }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PasswordField
|
||||||
|
id="irc-password"
|
||||||
|
label={i18n._(t`IRC server password`)}
|
||||||
|
name="notification_configuration.password"
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="irc-port"
|
||||||
|
label={i18n._(t`IRC server port`)}
|
||||||
|
name="notification_configuration.port"
|
||||||
|
type="number"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="irc-server"
|
||||||
|
label={i18n._(t`IRC server address`)}
|
||||||
|
name="notification_configuration.server"
|
||||||
|
type="text"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="irc-nickname"
|
||||||
|
label={i18n._(t`IRC nick`)}
|
||||||
|
name="notification_configuration.nickname"
|
||||||
|
type="text"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<ArrayTextField
|
||||||
|
id="irc-targets"
|
||||||
|
label={i18n._(t`Destination channels or users`)}
|
||||||
|
name="notification_configuration.targets"
|
||||||
|
type="textarea"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
tooltip={i18n._(t`Enter one IRC channel or username per line. The pound
|
||||||
|
symbol (#) for channels, and the at (@) symbol for users, are not
|
||||||
|
required.`)}
|
||||||
|
/>
|
||||||
|
<CheckboxField
|
||||||
|
id="grafana-ssl"
|
||||||
|
label={i18n._(t`Disable SSL verification`)}
|
||||||
|
name="notification_configuration.use_ssl"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MattermostFields({ i18n }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
id="mattermost-url"
|
||||||
|
label={i18n._(t`Target URL`)}
|
||||||
|
name="notification_configuration.mattermost_url"
|
||||||
|
type="text"
|
||||||
|
validate={combine([required(null, i18n), url(i18n)])}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="mattermost-username"
|
||||||
|
label={i18n._(t`Username`)}
|
||||||
|
name="notification_configuration.mattermost_username"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="mattermost-channel"
|
||||||
|
label={i18n._(t`Channel`)}
|
||||||
|
name="notification_configuration.mattermost_channel"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="mattermost-icon"
|
||||||
|
label={i18n._(t`Icon URL`)}
|
||||||
|
name="notification_configuration.mattermost_icon_url"
|
||||||
|
type="text"
|
||||||
|
validate={url(i18n)}
|
||||||
|
/>
|
||||||
|
<CheckboxField
|
||||||
|
id="mattermost-ssl"
|
||||||
|
label={i18n._(t`Disable SSL verification`)}
|
||||||
|
name="notification_configuration.mattermost_no_verify_ssl"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PagerdutyFields({ i18n }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PasswordField
|
||||||
|
id="pagerduty-token"
|
||||||
|
label={i18n._(t`API Token`)}
|
||||||
|
name="notification_configuration.token"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="pagerduty-subdomain"
|
||||||
|
label={i18n._(t`Pagerduty subdomain`)}
|
||||||
|
name="notification_configuration.subdomain"
|
||||||
|
type="text"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="pagerduty-service-key"
|
||||||
|
label={i18n._(t`API service/integration key`)}
|
||||||
|
name="notification_configuration.service_key"
|
||||||
|
type="text"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="pagerduty-identifier"
|
||||||
|
label={i18n._(t`Client identifier`)}
|
||||||
|
name="notification_configuration.client_name"
|
||||||
|
type="text"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RocketChatFields({ i18n }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
id="rocketchat-url"
|
||||||
|
label={i18n._(t`Target URL`)}
|
||||||
|
name="notification_configuration.rocketchat_url"
|
||||||
|
type="text"
|
||||||
|
validate={combine([required(null, i18n), url(i18n)])}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="rocketchat-username"
|
||||||
|
label={i18n._(t`Username`)}
|
||||||
|
name="notification_configuration.rocketchat_username"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="rocketchat-icon-url"
|
||||||
|
label={i18n._(t`Icon URL`)}
|
||||||
|
name="notification_configuration.rocketchat_icon_url"
|
||||||
|
type="text"
|
||||||
|
validate={url(i18n)}
|
||||||
|
/>
|
||||||
|
<CheckboxField
|
||||||
|
id="rocketchat-ssl"
|
||||||
|
label={i18n._(t`Disable SSL verification`)}
|
||||||
|
name="notification_configuration.rocketchat_no_verify_ssl"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SlackFields({ i18n }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ArrayTextField
|
||||||
|
id="slack-channels"
|
||||||
|
label={i18n._(t`Destination channels`)}
|
||||||
|
name="notification_configuration.channels"
|
||||||
|
type="textarea"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
tooltip={i18n._(t`Enter one Slack channel per line. The pound symbol (#)
|
||||||
|
is required for channels.`)}
|
||||||
|
/>
|
||||||
|
<PasswordField
|
||||||
|
id="slack-token"
|
||||||
|
label={i18n._(t`Token`)}
|
||||||
|
name="notification_configuration.token"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="slack-color"
|
||||||
|
label={i18n._(t`Notification color`)}
|
||||||
|
name="notification_configuration.hex_color"
|
||||||
|
type="text"
|
||||||
|
tooltip={i18n._(t`Specify a notification color. Acceptable colors are hex
|
||||||
|
color code (example: #3af or #789abc).`)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TwilioFields({ i18n }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PasswordField
|
||||||
|
id="twilio-token"
|
||||||
|
label={i18n._(t`Account token`)}
|
||||||
|
name="notification_configuration.account_token"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="twilio-from-phone"
|
||||||
|
label={i18n._(t`Source phone number`)}
|
||||||
|
name="notification_configuration.from_number"
|
||||||
|
type="text"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
tooltip={i18n._(t`Enter the number associated with the "Messaging
|
||||||
|
Service" in Twilio in the format +18005550199.`)}
|
||||||
|
/>
|
||||||
|
<ArrayTextField
|
||||||
|
id="twilio-destination-numbers"
|
||||||
|
label={i18n._(t`Destination SMS number(s)`)}
|
||||||
|
name="notification_configuration.to_numbers"
|
||||||
|
type="textarea"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
tooltip={i18n._(t`Enter one phone number per line to specify where to
|
||||||
|
route SMS messages.`)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="twilio-account-sid"
|
||||||
|
label={i18n._(t`Account SID`)}
|
||||||
|
name="notification_configuration.account_sid"
|
||||||
|
type="text"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WebhookFields({ i18n }) {
|
||||||
|
const [methodField, methodMeta] = useField({
|
||||||
|
name: 'notification_configuration.http_method',
|
||||||
|
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
id="webhook-username"
|
||||||
|
label={i18n._(t`Username`)}
|
||||||
|
name="notification_configuration.username"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<PasswordField
|
||||||
|
id="webhook-password"
|
||||||
|
label={i18n._(t`Basic auth password`)}
|
||||||
|
name="notification_configuration.password"
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
id="webhook-url"
|
||||||
|
label={i18n._(t`Target URL`)}
|
||||||
|
name="notification_configuration.url"
|
||||||
|
type="text"
|
||||||
|
validate={combine([required(null, i18n), url(i18n)])}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<CheckboxField
|
||||||
|
id="webhook-ssl"
|
||||||
|
label={i18n._(t`Disable SSL verification`)}
|
||||||
|
name="notification_configuration.disable_ssl_verification"
|
||||||
|
/>
|
||||||
|
<FormFullWidthLayout>
|
||||||
|
<CodeMirrorField
|
||||||
|
id="webhook-headers"
|
||||||
|
name="notification_configuration.headers"
|
||||||
|
label={i18n._(t`HTTP Headers`)}
|
||||||
|
mode="javascript"
|
||||||
|
tooltip={i18n._(t`Specify HTTP Headers in JSON format. Refer to
|
||||||
|
the Ansible Tower documentation for example syntax.`)}
|
||||||
|
rows={5}
|
||||||
|
/>
|
||||||
|
</FormFullWidthLayout>
|
||||||
|
<FormGroup
|
||||||
|
fieldId="webhook-http-method"
|
||||||
|
helperTextInvalid={methodMeta.error}
|
||||||
|
isRequired
|
||||||
|
validated={
|
||||||
|
!methodMeta.touched || !methodMeta.error ? 'default' : 'error'
|
||||||
|
}
|
||||||
|
label={i18n._(t`E-mail options`)}
|
||||||
|
>
|
||||||
|
<AnsibleSelect
|
||||||
|
{...methodField}
|
||||||
|
id="webhook-http-method"
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
key: '',
|
||||||
|
label: i18n._(t`Choose an HTTP method`),
|
||||||
|
isDisabled: true,
|
||||||
|
},
|
||||||
|
{ value: 'POST', key: 'post', label: i18n._(t`POST`) },
|
||||||
|
{ value: 'PUT', key: 'put', label: i18n._(t`PUT`) },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
const typeFieldNames = {
|
||||||
|
email: [
|
||||||
|
'username',
|
||||||
|
'password',
|
||||||
|
'host',
|
||||||
|
'recipients',
|
||||||
|
'sender',
|
||||||
|
'port',
|
||||||
|
'timeout',
|
||||||
|
],
|
||||||
|
grafana: [
|
||||||
|
'grafana_url',
|
||||||
|
'grafana_key',
|
||||||
|
'dashboardId',
|
||||||
|
'panelId',
|
||||||
|
'annotation_tags',
|
||||||
|
'grafana_no_verify_ssl',
|
||||||
|
],
|
||||||
|
irc: ['password', 'port', 'server', 'nickname', 'targets', 'use_ssl'],
|
||||||
|
mattermost: [
|
||||||
|
'mattermost_url',
|
||||||
|
'mattermost_username',
|
||||||
|
'mattermost_channel',
|
||||||
|
'mattermost_icon_url',
|
||||||
|
'mattermost_no_verify_ssl',
|
||||||
|
],
|
||||||
|
pagerduty: ['token', 'subdomain', 'service_key', 'client_name'],
|
||||||
|
rocketchat: [
|
||||||
|
'rocketchat_url',
|
||||||
|
'rocketchat_username',
|
||||||
|
'rocketchat_icon_url',
|
||||||
|
'rocketchat_no_verify_ssl',
|
||||||
|
],
|
||||||
|
slack: ['channels', 'token', 'hex_color'],
|
||||||
|
twilio: ['account_token', 'from_number', 'to_numbers', 'account_sid'],
|
||||||
|
webhook: [
|
||||||
|
'username',
|
||||||
|
'password',
|
||||||
|
'url',
|
||||||
|
'disable_ssl_verification',
|
||||||
|
'headers',
|
||||||
|
'http_method',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default typeFieldNames;
|
||||||
|
|
||||||
|
const initialConfigValues = {};
|
||||||
|
Object.keys(typeFieldNames).forEach(key => {
|
||||||
|
typeFieldNames[key].forEach(fieldName => {
|
||||||
|
const isBoolean = fieldName.includes('_ssl');
|
||||||
|
initialConfigValues[fieldName] = isBoolean ? false : '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export { initialConfigValues };
|
||||||
@@ -367,3 +367,27 @@ export const CredentialType = shape({
|
|||||||
namespace: string,
|
namespace: string,
|
||||||
inputs: shape({}).isRequired,
|
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,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@@ -76,6 +76,24 @@ export function integer(i18n) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function url(i18n) {
|
||||||
|
return value => {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// 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) {
|
export function combine(validators) {
|
||||||
return value => {
|
return value => {
|
||||||
for (let i = 0; i < validators.length; i++) {
|
for (let i = 0; i < validators.length; i++) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
maxLength,
|
maxLength,
|
||||||
noWhiteSpace,
|
noWhiteSpace,
|
||||||
integer,
|
integer,
|
||||||
|
url,
|
||||||
combine,
|
combine,
|
||||||
regExp,
|
regExp,
|
||||||
} from './validators';
|
} 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', () => {
|
test('combine should run all validators', () => {
|
||||||
const validators = [required(null, i18n), noWhiteSpace(i18n)];
|
const validators = [required(null, i18n), noWhiteSpace(i18n)];
|
||||||
expect(combine(validators)('')).toEqual({
|
expect(combine(validators)('')).toEqual({
|
||||||
|
|||||||
Reference in New Issue
Block a user