mirror of
https://github.com/ansible/awx.git
synced 2026-05-20 07:17:40 -02:30
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:
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user