Merge pull request #9318 from mabashian/9223-notif-toast

Adds toast to notification template list whenever test notification finishes

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2021-03-09 17:25:26 +00:00
committed by GitHub
5 changed files with 182 additions and 61 deletions

View File

@@ -16,6 +16,7 @@ This is a list of high-level changes for each release of AWX. A full list of com
- Added ability to relaunch against failed hosts: https://github.com/ansible/awx/pull/9225 - Added ability to relaunch against failed hosts: https://github.com/ansible/awx/pull/9225
- Added pending workflow approval count to the application header https://github.com/ansible/awx/pull/9334 - Added pending workflow approval count to the application header https://github.com/ansible/awx/pull/9334
- Added user interface for management jobs: https://github.com/ansible/awx/pull/9224 - Added user interface for management jobs: https://github.com/ansible/awx/pull/9224
- Added toast message to show notification template test result to notification templates list https://github.com/ansible/awx/pull/9318
# 17.1.0 (March 9th, 2021) # 17.1.0 (March 9th, 2021)
- Addressed a security issue in AWX (CVE-2021-20253) - Addressed a security issue in AWX (CVE-2021-20253)

View File

@@ -17,6 +17,7 @@ function CopyButton({
onCopyFinish, onCopyFinish,
errorMessage, errorMessage,
i18n, i18n,
ouiaId,
}) { }) {
const { isLoading, error: copyError, request: copyItemToAPI } = useRequest( const { isLoading, error: copyError, request: copyItemToAPI } = useRequest(
copyItem copyItem
@@ -35,6 +36,7 @@ function CopyButton({
<> <>
<Button <Button
id={id} id={id}
ouiaId={ouiaId}
isDisabled={isLoading || isDisabled} isDisabled={isLoading || isDisabled}
aria-label={i18n._(t`Copy`)} aria-label={i18n._(t`Copy`)}
variant="plain" variant="plain"
@@ -62,10 +64,12 @@ CopyButton.propTypes = {
onCopyFinish: PropTypes.func.isRequired, onCopyFinish: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired, errorMessage: PropTypes.string.isRequired,
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
ouiaId: PropTypes.string,
}; };
CopyButton.defaultProps = { CopyButton.defaultProps = {
isDisabled: false, isDisabled: false,
ouiaId: null,
}; };
export default withI18n()(CopyButton); export default withI18n()(CopyButton);

View File

@@ -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 { useLocation, useRouteMatch } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; 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 { NotificationTemplatesAPI } from '../../../api';
import PaginatedTable, { import PaginatedTable, {
HeaderRow, HeaderRow,
@@ -29,6 +35,7 @@ const QS_CONFIG = getQSConfig('notification-templates', {
function NotificationTemplatesList({ i18n }) { function NotificationTemplatesList({ i18n }) {
const location = useLocation(); const location = useLocation();
const match = useRouteMatch(); const match = useRouteMatch();
const [testToasts, setTestToasts] = useState([]);
const addUrl = `${match.url}/add`; const addUrl = `${match.url}/add`;
@@ -102,6 +109,16 @@ function NotificationTemplatesList({ i18n }) {
setSelected([]); setSelected([]);
}; };
const addTestToast = useCallback(notification => {
setTestToasts(oldToasts => [...oldToasts, notification]);
}, []);
const removeTestToast = notificationId => {
setTestToasts(oldToasts =>
oldToasts.filter(toast => toast.id !== notificationId)
);
};
const canAdd = actions && actions.POST; const canAdd = actions && actions.POST;
return ( return (
@@ -185,6 +202,7 @@ function NotificationTemplatesList({ i18n }) {
} }
renderRow={(template, index) => ( renderRow={(template, index) => (
<NotificationTemplateListItem <NotificationTemplateListItem
onAddToast={addTestToast}
key={template.id} key={template.id}
fetchTemplates={fetchTemplates} fetchTemplates={fetchTemplates}
template={template} template={template}
@@ -209,6 +227,39 @@ function NotificationTemplatesList({ i18n }) {
{i18n._(t`Failed to delete one or more notification template.`)} {i18n._(t`Failed to delete one or more notification template.`)}
<ErrorDetail error={deletionError} /> <ErrorDetail error={deletionError} />
</AlertModal> </AlertModal>
<AlertGroup ouiaId="notification-template-alerts" isToast>
{testToasts
.filter(notification => notification.status !== 'pending')
.map(notification => (
<Alert
actionClose={
<AlertActionCloseButton
onClose={() => 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' && (
<p>{i18n._(t`Notification sent successfully`)}</p>
)}
{notification.status === 'failed' &&
notification?.error === 'timed out' && (
<p>{i18n._(t`Notification timed out`)}</p>
)}
{notification.status === 'failed' &&
notification?.error !== 'timed out' && (
<p>{notification.error}</p>
)}
</>
</Alert>
))}
</AlertGroup>
</> </>
); );
} }

View File

@@ -1,11 +1,17 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { OrganizationsAPI } from '../../../api'; import {
NotificationsAPI,
NotificationTemplatesAPI,
OrganizationsAPI,
} from '../../../api';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import NotificationTemplateList from './NotificationTemplateList'; import NotificationTemplateList from './NotificationTemplateList';
jest.mock('../../../api'); jest.mock('../../../api');
jest.useFakeTimers();
const mockTemplates = { const mockTemplates = {
data: { data: {
count: 3, count: 3,
@@ -197,6 +203,43 @@ describe('<NotificationTemplateList />', () => {
expect(wrapper.find('ToolbarAddButton').length).toBe(1); 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(<NotificationTemplateList />);
});
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 () => { test('should hide add button (rbac)', async () => {
OrganizationsAPI.readOptions.mockResolvedValue({ OrganizationsAPI.readOptions.mockResolvedValue({
data: { data: {

View File

@@ -11,13 +11,16 @@ import { timeOfDay } from '../../../util/dates';
import { NotificationTemplatesAPI, NotificationsAPI } from '../../../api'; import { NotificationTemplatesAPI, NotificationsAPI } from '../../../api';
import StatusLabel from '../../../components/StatusLabel'; import StatusLabel from '../../../components/StatusLabel';
import CopyButton from '../../../components/CopyButton'; 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'; import { NOTIFICATION_TYPES } from '../constants';
const NUM_RETRIES = 25; const NUM_RETRIES = 25;
const RETRY_TIMEOUT = 5000; const RETRY_TIMEOUT = 5000;
function NotificationTemplateListItem({ function NotificationTemplateListItem({
onAddToast,
template, template,
detailUrl, detailUrl,
fetchTemplates, fetchTemplates,
@@ -66,6 +69,7 @@ function NotificationTemplateListItem({
notificationId notificationId
); );
if (notification.status !== 'pending') { if (notification.status !== 'pending') {
onAddToast(notification);
setStatus(notification.status); setStatus(notification.status);
return; return;
} }
@@ -76,9 +80,11 @@ function NotificationTemplateListItem({
} }
setTimeout(pollForStatusChange, RETRY_TIMEOUT); setTimeout(pollForStatusChange, RETRY_TIMEOUT);
}, [template.id]) }, [template.id, onAddToast])
); );
const { error: sendTestError, dismissError } = useDismissableError(error);
useEffect(() => { useEffect(() => {
if (error) { if (error) {
setStatus('error'); setStatus('error');
@@ -88,6 +94,7 @@ function NotificationTemplateListItem({
const labelId = `template-name-${template.id}`; const labelId = `template-name-${template.id}`;
return ( return (
<>
<Tr id={`notification-template-row-${template.id}`}> <Tr id={`notification-template-row-${template.id}`}>
<Td <Td
select={{ select={{
@@ -112,6 +119,7 @@ function NotificationTemplateListItem({
<ActionsTd dataLabel={i18n._(t`Actions`)}> <ActionsTd dataLabel={i18n._(t`Actions`)}>
<ActionItem visible tooltip={i18n._(t`Test notification`)}> <ActionItem visible tooltip={i18n._(t`Test notification`)}>
<Button <Button
ouiaId={`notification-test-button-${template.id}`}
aria-label={i18n._(t`Test Notification`)} aria-label={i18n._(t`Test Notification`)}
variant="plain" variant="plain"
onClick={sendTestNotification} onClick={sendTestNotification}
@@ -125,6 +133,7 @@ function NotificationTemplateListItem({
tooltip={i18n._(t`Edit`)} tooltip={i18n._(t`Edit`)}
> >
<Button <Button
ouiaId={`notification-edit-button-${template.id}`}
aria-label={i18n._(t`Edit Notification Template`)} aria-label={i18n._(t`Edit Notification Template`)}
variant="plain" variant="plain"
component={Link} component={Link}
@@ -138,6 +147,7 @@ function NotificationTemplateListItem({
tooltip={i18n._(t`Copy Notification Template`)} tooltip={i18n._(t`Copy Notification Template`)}
> >
<CopyButton <CopyButton
ouiaId={`notification-copy-button-${template.id}`}
copyItem={copyTemplate} copyItem={copyTemplate}
isCopyDisabled={isCopyDisabled} isCopyDisabled={isCopyDisabled}
onCopyStart={handleCopyStart} onCopyStart={handleCopyStart}
@@ -147,6 +157,18 @@ function NotificationTemplateListItem({
</ActionItem> </ActionItem>
</ActionsTd> </ActionsTd>
</Tr> </Tr>
{sendTestError && (
<AlertModal
isOpen
variant="error"
title={i18n._(t`Error!`)}
onClose={dismissError}
>
{i18n._(t`Failed to send test notification.`)}
<ErrorDetail error={sendTestError} />
</AlertModal>
)}
</>
); );
} }