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 (
+ <>
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+ >
+ );
+}
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({