diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx
index c1b277b730..ed7d019072 100644
--- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx
+++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx
@@ -1,8 +1,14 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { useLocation, useRouteMatch } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
-import { Card, PageSection } from '@patternfly/react-core';
+import {
+ Alert,
+ AlertActionCloseButton,
+ AlertGroup,
+ Card,
+ PageSection,
+} from '@patternfly/react-core';
import { NotificationTemplatesAPI } from '../../../api';
import PaginatedTable, {
HeaderRow,
@@ -29,6 +35,7 @@ const QS_CONFIG = getQSConfig('notification-templates', {
function NotificationTemplatesList({ i18n }) {
const location = useLocation();
const match = useRouteMatch();
+ const [testToasts, setTestToasts] = useState([]);
const addUrl = `${match.url}/add`;
@@ -102,6 +109,14 @@ function NotificationTemplatesList({ i18n }) {
setSelected([]);
};
+ const addTestToast = useCallback(notification => {
+ setTestToasts(oldToasts => [...oldToasts, notification]);
+ }, []);
+
+ const removeTestToast = notificationId => {
+ setTestToasts(testToasts.filter(toast => toast.id !== notificationId));
+ };
+
const canAdd = actions && actions.POST;
return (
@@ -185,6 +200,7 @@ function NotificationTemplatesList({ i18n }) {
}
renderRow={(template, index) => (
+
+ {testToasts
+ .filter(notification => notification.status !== 'pending')
+ .map(notification => (
+ removeTestToast(notification.id)}
+ />
+ }
+ onTimeout={() => removeTestToast(notification.id)}
+ timeout={notification.status !== 'failed'}
+ title={notification.summary_fields.notification_template.name}
+ variant={notification.status === 'failed' ? 'danger' : 'success'}
+ key={`notification-template-alert-${notification.id}`}
+ ouiaId={`notification-template-alert-${notification.id}`}
+ >
+ <>
+ {notification.status === 'successful' && (
+ {i18n._(t`Notification sent successfully`)}
+ )}
+ {notification.status === 'failed' &&
+ notification?.error === 'timed out' && (
+ {i18n._(t`Notification timed out`)}
+ )}
+ {notification.status === 'failed' &&
+ notification?.error !== 'timed out' && (
+ {notification.error}
+ )}
+ >
+
+ ))}
+
>
);
}
diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx
index 345d2d0a42..f0bef1326c 100644
--- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx
+++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx
@@ -1,11 +1,17 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
-import { OrganizationsAPI } from '../../../api';
+import {
+ NotificationsAPI,
+ NotificationTemplatesAPI,
+ OrganizationsAPI,
+} from '../../../api';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import NotificationTemplateList from './NotificationTemplateList';
jest.mock('../../../api');
+jest.useFakeTimers();
+
const mockTemplates = {
data: {
count: 3,
@@ -197,6 +203,43 @@ describe('', () => {
expect(wrapper.find('ToolbarAddButton').length).toBe(1);
});
+ test('should show toast after test resolves', async () => {
+ NotificationTemplatesAPI.test.mockResolvedValueOnce({
+ data: {
+ notification: 9182,
+ },
+ });
+ NotificationsAPI.readDetail.mockResolvedValueOnce({
+ data: {
+ id: 9182,
+ status: 'failed',
+ error: 'There was an error with the notification',
+ summary_fields: {
+ notification_template: {
+ name: 'foobar',
+ },
+ },
+ },
+ });
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+ expect(wrapper.find('Alert').length).toBe(0);
+ await act(async () => {
+ wrapper
+ .find('button[aria-label="Test Notification"]')
+ .at(0)
+ .simulate('click');
+ });
+ wrapper.update();
+ await act(async () => {
+ jest.runAllTimers();
+ });
+ wrapper.update();
+ expect(wrapper.find('Alert').length).toBe(1);
+ });
+
test('should hide add button (rbac)', async () => {
OrganizationsAPI.readOptions.mockResolvedValue({
data: {
diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx
index 93d1f82999..71d32e9821 100644
--- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx
+++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx
@@ -11,13 +11,16 @@ import { timeOfDay } from '../../../util/dates';
import { NotificationTemplatesAPI, NotificationsAPI } from '../../../api';
import StatusLabel from '../../../components/StatusLabel';
import CopyButton from '../../../components/CopyButton';
-import useRequest from '../../../util/useRequest';
+import AlertModal from '../../../components/AlertModal';
+import ErrorDetail from '../../../components/ErrorDetail';
+import useRequest, { useDismissableError } from '../../../util/useRequest';
import { NOTIFICATION_TYPES } from '../constants';
const NUM_RETRIES = 25;
const RETRY_TIMEOUT = 5000;
function NotificationTemplateListItem({
+ onAddToast,
template,
detailUrl,
fetchTemplates,
@@ -66,6 +69,7 @@ function NotificationTemplateListItem({
notificationId
);
if (notification.status !== 'pending') {
+ onAddToast(notification);
setStatus(notification.status);
return;
}
@@ -76,9 +80,11 @@ function NotificationTemplateListItem({
}
setTimeout(pollForStatusChange, RETRY_TIMEOUT);
- }, [template.id])
+ }, [template.id, onAddToast])
);
+ const { error: sendTestError, dismissError } = useDismissableError(error);
+
useEffect(() => {
if (error) {
setStatus('error');
@@ -88,65 +94,78 @@ function NotificationTemplateListItem({
const labelId = `template-name-${template.id}`;
return (
-
- |
-
-
- {template.name}
-
- |
-
- {status && }
- |
-
- {NOTIFICATION_TYPES[template.notification_type] ||
- template.notification_type}
- |
-
-
-
-
-
+ {i18n._(t`Failed to send test notification.`)}
+
+
+ )}
+ >
);
}