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,65 +94,81 @@ function NotificationTemplateListItem({
const labelId = `template-name-${template.id}`; const labelId = `template-name-${template.id}`;
return ( return (
<Tr id={`notification-template-row-${template.id}`}> <>
<Td <Tr id={`notification-template-row-${template.id}`}>
select={{ <Td
rowIndex, select={{
isSelected, rowIndex,
onSelect, isSelected,
}} onSelect,
dataLabel={i18n._(t`Selected`)} }}
/> dataLabel={i18n._(t`Selected`)}
<Td id={labelId} dataLabel={i18n._(t`Name`)}> />
<Link to={`${detailUrl}`}> <Td id={labelId} dataLabel={i18n._(t`Name`)}>
<b>{template.name}</b> <Link to={`${detailUrl}`}>
</Link> <b>{template.name}</b>
</Td> </Link>
<Td dataLabel={i18n._(t`Status`)}> </Td>
{status && <StatusLabel status={status} />} <Td dataLabel={i18n._(t`Status`)}>
</Td> {status && <StatusLabel status={status} />}
<Td dataLabel={i18n._(t`Type`)}> </Td>
{NOTIFICATION_TYPES[template.notification_type] || <Td dataLabel={i18n._(t`Type`)}>
template.notification_type} {NOTIFICATION_TYPES[template.notification_type] ||
</Td> template.notification_type}
<ActionsTd dataLabel={i18n._(t`Actions`)}> </Td>
<ActionItem visible tooltip={i18n._(t`Test notification`)}> <ActionsTd dataLabel={i18n._(t`Actions`)}>
<Button <ActionItem visible tooltip={i18n._(t`Test notification`)}>
aria-label={i18n._(t`Test Notification`)} <Button
variant="plain" ouiaId={`notification-test-button-${template.id}`}
onClick={sendTestNotification} aria-label={i18n._(t`Test Notification`)}
isDisabled={isLoading || status === 'running'} variant="plain"
onClick={sendTestNotification}
isDisabled={isLoading || status === 'running'}
>
<BellIcon />
</Button>
</ActionItem>
<ActionItem
visible={template.summary_fields.user_capabilities.edit}
tooltip={i18n._(t`Edit`)}
> >
<BellIcon /> <Button
</Button> ouiaId={`notification-edit-button-${template.id}`}
</ActionItem> aria-label={i18n._(t`Edit Notification Template`)}
<ActionItem variant="plain"
visible={template.summary_fields.user_capabilities.edit} component={Link}
tooltip={i18n._(t`Edit`)} to={`/notification_templates/${template.id}/edit`}
> >
<Button <PencilAltIcon />
aria-label={i18n._(t`Edit Notification Template`)} </Button>
variant="plain" </ActionItem>
component={Link} <ActionItem
to={`/notification_templates/${template.id}/edit`} visible={template.summary_fields.user_capabilities.copy}
tooltip={i18n._(t`Copy Notification Template`)}
> >
<PencilAltIcon /> <CopyButton
</Button> ouiaId={`notification-copy-button-${template.id}`}
</ActionItem> copyItem={copyTemplate}
<ActionItem isCopyDisabled={isCopyDisabled}
visible={template.summary_fields.user_capabilities.copy} onCopyStart={handleCopyStart}
tooltip={i18n._(t`Copy Notification Template`)} onCopyFinish={handleCopyFinish}
errorMessage={i18n._(t`Failed to copy template.`)}
/>
</ActionItem>
</ActionsTd>
</Tr>
{sendTestError && (
<AlertModal
isOpen
variant="error"
title={i18n._(t`Error!`)}
onClose={dismissError}
> >
<CopyButton {i18n._(t`Failed to send test notification.`)}
copyItem={copyTemplate} <ErrorDetail error={sendTestError} />
isCopyDisabled={isCopyDisabled} </AlertModal>
onCopyStart={handleCopyStart} )}
onCopyFinish={handleCopyFinish} </>
errorMessage={i18n._(t`Failed to copy template.`)}
/>
</ActionItem>
</ActionsTd>
</Tr>
); );
} }