Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds toast to notification template list whenever test notification finishes #9318

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 toast message to show notification template test result to notification templates list https://github.com/ansible/awx/pull/9318

# 17.0.1 (January 26, 2021)
- Fixed pgdocker directory permissions issue with Local Docker installer: https://github.com/ansible/awx/pull/9152
Expand Down
4 changes: 4 additions & 0 deletions awx/ui_next/src/components/CopyButton/CopyButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function CopyButton({
onCopyFinish,
errorMessage,
i18n,
ouiaId,
}) {
const { isLoading, error: copyError, request: copyItemToAPI } = useRequest(
copyItem
Expand All @@ -35,6 +36,7 @@ function CopyButton({
<>
<Button
id={id}
ouiaId={ouiaId}
isDisabled={isLoading || isDisabled}
aria-label={i18n._(t`Copy`)}
variant="plain"
Expand Down Expand Up @@ -62,10 +64,12 @@ CopyButton.propTypes = {
onCopyFinish: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired,
isDisabled: PropTypes.bool,
ouiaId: PropTypes.string,
};

CopyButton.defaultProps = {
isDisabled: false,
ouiaId: null,
};

export default withI18n()(CopyButton);
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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`;

Expand Down Expand Up @@ -102,6 +109,16 @@ function NotificationTemplatesList({ i18n }) {
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;

return (
Expand Down Expand Up @@ -185,6 +202,7 @@ function NotificationTemplatesList({ i18n }) {
}
renderRow={(template, index) => (
<NotificationTemplateListItem
onAddToast={addTestToast}
key={template.id}
fetchTemplates={fetchTemplates}
template={template}
Expand All @@ -209,6 +227,39 @@ function NotificationTemplatesList({ i18n }) {
{i18n._(t`Failed to delete one or more notification template.`)}
<ErrorDetail error={deletionError} />
</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>
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -197,6 +203,43 @@ describe('<NotificationTemplateList />', () => {
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 () => {
OrganizationsAPI.readOptions.mockResolvedValue({
data: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -66,6 +69,7 @@ function NotificationTemplateListItem({
notificationId
);
if (notification.status !== 'pending') {
onAddToast(notification);
setStatus(notification.status);
return;
}
Expand All @@ -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');
Expand All @@ -88,65 +94,81 @@ function NotificationTemplateListItem({
const labelId = `template-name-${template.id}`;

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

Expand Down