mirror of
https://github.com/ansible/awx.git
synced 2026-05-13 04:17:36 -02:30
Merge pull request #7867 from keithjgrant/5651-notification-templates
Notification templates list & details Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -5,6 +5,10 @@ class NotificationTemplates extends Base {
|
|||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/notification_templates/';
|
this.baseUrl = '/api/v2/notification_templates/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test(id) {
|
||||||
|
return this.http.post(`${this.baseUrl}${id}/test/`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotificationTemplates;
|
export default NotificationTemplates;
|
||||||
|
|||||||
51
awx/ui_next/src/components/DetailList/ObjectDetail.jsx
Normal file
51
awx/ui_next/src/components/DetailList/ObjectDetail.jsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import 'styled-components/macro';
|
||||||
|
import React from 'react';
|
||||||
|
import { shape, node, number } from 'prop-types';
|
||||||
|
import { TextListItemVariants } from '@patternfly/react-core';
|
||||||
|
import { DetailName, DetailValue } from './Detail';
|
||||||
|
import CodeMirrorInput from '../CodeMirrorInput';
|
||||||
|
|
||||||
|
function ObjectDetail({ value, label, rows, fullHeight }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DetailName
|
||||||
|
component={TextListItemVariants.dt}
|
||||||
|
fullWidth
|
||||||
|
css="grid-column: 1 / -1"
|
||||||
|
>
|
||||||
|
<div className="pf-c-form__label">
|
||||||
|
<span
|
||||||
|
className="pf-c-form__label-text"
|
||||||
|
css="font-weight: var(--pf-global--FontWeight--bold)"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DetailName>
|
||||||
|
<DetailValue
|
||||||
|
component={TextListItemVariants.dd}
|
||||||
|
fullWidth
|
||||||
|
css="grid-column: 1 / -1; margin-top: -20px"
|
||||||
|
>
|
||||||
|
<CodeMirrorInput
|
||||||
|
mode="json"
|
||||||
|
value={JSON.stringify(value)}
|
||||||
|
readOnly
|
||||||
|
rows={rows}
|
||||||
|
fullHeight={fullHeight}
|
||||||
|
css="margin-top: 10px"
|
||||||
|
/>
|
||||||
|
</DetailValue>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ObjectDetail.propTypes = {
|
||||||
|
value: shape.isRequired,
|
||||||
|
label: node.isRequired,
|
||||||
|
rows: number,
|
||||||
|
};
|
||||||
|
ObjectDetail.defaultProps = {
|
||||||
|
rows: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ObjectDetail;
|
||||||
@@ -3,3 +3,8 @@ export { default as Detail, DetailName, DetailValue } from './Detail';
|
|||||||
export { default as DeletedDetail } from './DeletedDetail';
|
export { default as DeletedDetail } from './DeletedDetail';
|
||||||
export { default as UserDateDetail } from './UserDateDetail';
|
export { default as UserDateDetail } from './UserDateDetail';
|
||||||
export { default as DetailBadge } from './DetailBadge';
|
export { default as DetailBadge } from './DetailBadge';
|
||||||
|
/*
|
||||||
|
NOTE: ObjectDetail cannot be imported here, as it causes circular
|
||||||
|
dependencies in testing environment. Import it directly from
|
||||||
|
DetailList/ObjectDetail
|
||||||
|
*/
|
||||||
|
|||||||
68
awx/ui_next/src/components/StatusLabel/StatusLabel.jsx
Normal file
68
awx/ui_next/src/components/StatusLabel/StatusLabel.jsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import 'styled-components/macro';
|
||||||
|
import React from 'react';
|
||||||
|
import { oneOf } from 'prop-types';
|
||||||
|
import { Label } from '@patternfly/react-core';
|
||||||
|
import {
|
||||||
|
CheckCircleIcon,
|
||||||
|
ExclamationCircleIcon,
|
||||||
|
SyncAltIcon,
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
ClockIcon,
|
||||||
|
} from '@patternfly/react-icons';
|
||||||
|
import styled, { keyframes } from 'styled-components';
|
||||||
|
|
||||||
|
const Spin = keyframes`
|
||||||
|
from {
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(1turn);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RunningIcon = styled(SyncAltIcon)`
|
||||||
|
animation: ${Spin} 1.75s linear infinite;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
success: 'green',
|
||||||
|
failed: 'red',
|
||||||
|
error: 'red',
|
||||||
|
running: 'blue',
|
||||||
|
pending: 'blue',
|
||||||
|
waiting: 'grey',
|
||||||
|
canceled: 'orange',
|
||||||
|
};
|
||||||
|
const icons = {
|
||||||
|
success: CheckCircleIcon,
|
||||||
|
failed: ExclamationCircleIcon,
|
||||||
|
error: ExclamationCircleIcon,
|
||||||
|
running: RunningIcon,
|
||||||
|
pending: ClockIcon,
|
||||||
|
waiting: ClockIcon,
|
||||||
|
canceled: ExclamationTriangleIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function StatusLabel({ status }) {
|
||||||
|
const label = status.charAt(0).toUpperCase() + status.slice(1);
|
||||||
|
const color = colors[status] || 'grey';
|
||||||
|
const Icon = icons[status];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label variant="outline" color={color} icon={Icon ? <Icon /> : null}>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusLabel.propTypes = {
|
||||||
|
status: oneOf([
|
||||||
|
'success',
|
||||||
|
'failed',
|
||||||
|
'error',
|
||||||
|
'running',
|
||||||
|
'pending',
|
||||||
|
'waiting',
|
||||||
|
'canceled',
|
||||||
|
]).isRequired,
|
||||||
|
};
|
||||||
61
awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx
Normal file
61
awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import StatusLabel from './StatusLabel';
|
||||||
|
|
||||||
|
describe('StatusLabel', () => {
|
||||||
|
test('should render success', () => {
|
||||||
|
const wrapper = mount(<StatusLabel status="success" />);
|
||||||
|
expect(wrapper).toHaveLength(1);
|
||||||
|
expect(wrapper.find('CheckCircleIcon')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('Label').prop('color')).toEqual('green');
|
||||||
|
expect(wrapper.text()).toEqual('Success');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render failed', () => {
|
||||||
|
const wrapper = mount(<StatusLabel status="failed" />);
|
||||||
|
expect(wrapper).toHaveLength(1);
|
||||||
|
expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('Label').prop('color')).toEqual('red');
|
||||||
|
expect(wrapper.text()).toEqual('Failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render error', () => {
|
||||||
|
const wrapper = mount(<StatusLabel status="error" />);
|
||||||
|
expect(wrapper).toHaveLength(1);
|
||||||
|
expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('Label').prop('color')).toEqual('red');
|
||||||
|
expect(wrapper.text()).toEqual('Error');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render running', () => {
|
||||||
|
const wrapper = mount(<StatusLabel status="running" />);
|
||||||
|
expect(wrapper).toHaveLength(1);
|
||||||
|
expect(wrapper.find('SyncAltIcon')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('Label').prop('color')).toEqual('blue');
|
||||||
|
expect(wrapper.text()).toEqual('Running');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render pending', () => {
|
||||||
|
const wrapper = mount(<StatusLabel status="pending" />);
|
||||||
|
expect(wrapper).toHaveLength(1);
|
||||||
|
expect(wrapper.find('ClockIcon')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('Label').prop('color')).toEqual('blue');
|
||||||
|
expect(wrapper.text()).toEqual('Pending');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render waiting', () => {
|
||||||
|
const wrapper = mount(<StatusLabel status="waiting" />);
|
||||||
|
expect(wrapper).toHaveLength(1);
|
||||||
|
expect(wrapper.find('ClockIcon')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('Label').prop('color')).toEqual('grey');
|
||||||
|
expect(wrapper.text()).toEqual('Waiting');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render canceled', () => {
|
||||||
|
const wrapper = mount(<StatusLabel status="canceled" />);
|
||||||
|
expect(wrapper).toHaveLength(1);
|
||||||
|
expect(wrapper.find('ExclamationTriangleIcon')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('Label').prop('color')).toEqual('orange');
|
||||||
|
expect(wrapper.text()).toEqual('Canceled');
|
||||||
|
});
|
||||||
|
});
|
||||||
1
awx/ui_next/src/components/StatusLabel/index.js
Normal file
1
awx/ui_next/src/components/StatusLabel/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './StatusLabel';
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import React, { useEffect, useCallback } from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
Switch,
|
||||||
|
Route,
|
||||||
|
Redirect,
|
||||||
|
useParams,
|
||||||
|
useRouteMatch,
|
||||||
|
useLocation,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import useRequest from '../../util/useRequest';
|
||||||
|
import RoutedTabs from '../../components/RoutedTabs';
|
||||||
|
import ContentError from '../../components/ContentError';
|
||||||
|
import { NotificationTemplatesAPI } from '../../api';
|
||||||
|
import NotificationTemplateDetail from './NotificationTemplateDetail';
|
||||||
|
import NotificationTemplateEdit from './NotificationTemplateEdit';
|
||||||
|
|
||||||
|
function NotificationTemplate({ setBreadcrumb, i18n }) {
|
||||||
|
const { id: templateId } = useParams();
|
||||||
|
const match = useRouteMatch();
|
||||||
|
const location = useLocation();
|
||||||
|
const {
|
||||||
|
result: template,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
request: fetchTemplate,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await NotificationTemplatesAPI.readDetail(templateId);
|
||||||
|
setBreadcrumb(data);
|
||||||
|
return data;
|
||||||
|
}, [templateId, setBreadcrumb]),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTemplate();
|
||||||
|
}, [fetchTemplate]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<ContentError error={error}>
|
||||||
|
{error.response.status === 404 && (
|
||||||
|
<span>
|
||||||
|
{i18n._(t`Notification Template not found.`)}{' '}
|
||||||
|
<Link to="/notification_templates">
|
||||||
|
{i18n._(t`View all Notification Templates.`)}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ContentError>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const showCardHeader = !isLoading && !location.pathname.endsWith('edit');
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to Notifications`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/notification_templates`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `${match.url}/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
{showCardHeader && <RoutedTabs tabsArray={tabs} />}
|
||||||
|
<Switch>
|
||||||
|
<Redirect
|
||||||
|
from="/notification_templates/:id"
|
||||||
|
to="/notification_templates/:id/details"
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
{template && (
|
||||||
|
<>
|
||||||
|
<Route path="/notification_templates/:id/edit">
|
||||||
|
<NotificationTemplateEdit
|
||||||
|
template={template}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
<Route path="/notification_templates/:id/details">
|
||||||
|
<NotificationTemplateDetail
|
||||||
|
template={template}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Switch>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(NotificationTemplate);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function NotificationTemplateAdd() {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { Button } from '@patternfly/react-core';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import AlertModal from '../../../components/AlertModal';
|
||||||
|
import { CardBody, CardActionsRow } from '../../../components/Card';
|
||||||
|
import {
|
||||||
|
Detail,
|
||||||
|
DetailList,
|
||||||
|
DeletedDetail,
|
||||||
|
} from '../../../components/DetailList';
|
||||||
|
import ObjectDetail from '../../../components/DetailList/ObjectDetail';
|
||||||
|
import DeleteButton from '../../../components/DeleteButton';
|
||||||
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
|
import { NotificationTemplatesAPI } from '../../../api';
|
||||||
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
|
import { NOTIFICATION_TYPES } from '../constants';
|
||||||
|
|
||||||
|
function NotificationTemplateDetail({ i18n, template }) {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const {
|
||||||
|
notification_configuration: configuration,
|
||||||
|
summary_fields,
|
||||||
|
} = template;
|
||||||
|
|
||||||
|
const { request: deleteTemplate, isLoading, error: deleteError } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
await NotificationTemplatesAPI.destroy(template.id);
|
||||||
|
history.push(`/notification_templates`);
|
||||||
|
}, [template.id, history])
|
||||||
|
);
|
||||||
|
|
||||||
|
const { error, dismissError } = useDismissableError(deleteError);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardBody>
|
||||||
|
<DetailList gutter="sm">
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Name`)}
|
||||||
|
value={template.name}
|
||||||
|
dataCy="nt-detail-name"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Description`)}
|
||||||
|
value={template.description}
|
||||||
|
dataCy="nt-detail-description"
|
||||||
|
/>
|
||||||
|
{summary_fields.organization ? (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Organization`)}
|
||||||
|
value={
|
||||||
|
<Link
|
||||||
|
to={`/organizations/${summary_fields.organization.id}/details`}
|
||||||
|
>
|
||||||
|
{summary_fields.organization.name}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DeletedDetail label={i18n._(t`Organization`)} />
|
||||||
|
)}
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Notification Type`)}
|
||||||
|
value={
|
||||||
|
NOTIFICATION_TYPES[template.notification_type] ||
|
||||||
|
template.notification_type
|
||||||
|
}
|
||||||
|
dataCy="nt-detail-type"
|
||||||
|
/>
|
||||||
|
{template.notification_type === 'email' && (
|
||||||
|
<>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Username`)}
|
||||||
|
value={configuration.username}
|
||||||
|
dataCy="nt-detail-username"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Host`)}
|
||||||
|
value={configuration.host}
|
||||||
|
dataCy="nt-detail-host"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Recipient List`)}
|
||||||
|
value={configuration.recipients} // array
|
||||||
|
dataCy="nt-detail-recipients"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Sender Email`)}
|
||||||
|
value={configuration.sender}
|
||||||
|
dataCy="nt-detail-sender"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Port`)}
|
||||||
|
value={configuration.port}
|
||||||
|
dataCy="nt-detail-port"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Timeout`)}
|
||||||
|
value={configuration.timeout}
|
||||||
|
dataCy="nt-detail-timeout"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Email Options`)}
|
||||||
|
value={
|
||||||
|
configuration.use_ssl ? i18n._(t`Use SSL`) : i18n._(t`Use TLS`)
|
||||||
|
}
|
||||||
|
dataCy="nt-detail-email-options"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{template.notification_type === 'grafana' && (
|
||||||
|
<>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Grafana URL`)}
|
||||||
|
value={configuration.grafana_url}
|
||||||
|
dataCy="nt-detail-grafana-url"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`ID of the Dashboard`)}
|
||||||
|
value={configuration.dashboardId}
|
||||||
|
dataCy="nt-detail-dashboard-id"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`ID of the Panel`)}
|
||||||
|
value={configuration.panelId}
|
||||||
|
dataCy="nt-detail-panel-id"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Tags for the Annotation`)}
|
||||||
|
value={configuration.annotation_tags} // array
|
||||||
|
dataCy="nt-detail-"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Disable SSL Verification`)}
|
||||||
|
value={
|
||||||
|
configuration.grafana_no_verify_ssl
|
||||||
|
? i18n._(t`True`)
|
||||||
|
: i18n._(t`False`)
|
||||||
|
}
|
||||||
|
dataCy="nt-detail-disable-ssl"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{template.notification_type === 'irc' && (
|
||||||
|
<>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`IRC Server Port`)}
|
||||||
|
value={configuration.port}
|
||||||
|
dataCy="nt-detail-irc-port"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`IRC Server Address`)}
|
||||||
|
value={configuration.server}
|
||||||
|
dataCy="nt-detail-irc-server"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`IRC Nick`)}
|
||||||
|
value={configuration.nickname}
|
||||||
|
dataCy="nt-detail-irc-nickname"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Destination Channels or Users`)}
|
||||||
|
value={configuration.targets} // array
|
||||||
|
dataCy="nt-detail-channels"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`SSL Connection`)}
|
||||||
|
value={configuration.use_ssl ? i18n._(t`True`) : i18n._(t`False`)}
|
||||||
|
dataCy="nt-detail-irc-ssl"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{template.notification_type === 'mattermost' && (
|
||||||
|
<>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Target URL`)}
|
||||||
|
value={configuration.mattermost_url}
|
||||||
|
dataCy="nt-detail-mattermost-url"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Username`)}
|
||||||
|
value={configuration.mattermost_username}
|
||||||
|
dataCy="nt-detail-mattermost-username"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Channel`)}
|
||||||
|
value={configuration.mattermost_channel}
|
||||||
|
dataCy="nt-detail-mattermost_channel"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Icon URL`)}
|
||||||
|
value={configuration.mattermost_icon_url}
|
||||||
|
dataCy="nt-detail-mattermost-icon-url"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Disable SSL Verification`)}
|
||||||
|
value={
|
||||||
|
configuration.mattermost_no_verify_ssl
|
||||||
|
? i18n._(t`True`)
|
||||||
|
: i18n._(t`False`)
|
||||||
|
}
|
||||||
|
dataCy="nt-detail-disable-ssl"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{template.notification_type === 'pagerduty' && (
|
||||||
|
<>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Pagerduty Subdomain`)}
|
||||||
|
value={configuration.subdomain}
|
||||||
|
dataCy="nt-detail-pagerduty-subdomain"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`API Service/Integration Key`)}
|
||||||
|
value={configuration.service_key}
|
||||||
|
dataCy="nt-detail-pagerduty-service-key"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Client Identifier`)}
|
||||||
|
value={configuration.client_name}
|
||||||
|
dataCy="nt-detail-pagerduty-client-name"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{template.notification_type === 'rocketchat' && (
|
||||||
|
<>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Target URL`)}
|
||||||
|
value={configuration.rocketchat_url}
|
||||||
|
dataCy="nt-detail-rocketchat-url"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Username`)}
|
||||||
|
value={configuration.rocketchat_username}
|
||||||
|
dataCy="nt-detail-pagerduty-rocketchat-username"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Icon URL`)}
|
||||||
|
value={configuration.rocketchat_icon_url}
|
||||||
|
dataCy="nt-detail-rocketchat-icon-url"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Disable SSL Verification`)}
|
||||||
|
value={
|
||||||
|
configuration.rocketchat_no_verify_ssl
|
||||||
|
? i18n._(t`True`)
|
||||||
|
: i18n._(t`False`)
|
||||||
|
}
|
||||||
|
dataCy="nt-detail-disable-ssl"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{template.notification_type === 'slack' && (
|
||||||
|
<>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Destination Channels`)}
|
||||||
|
value={configuration.channels} // array
|
||||||
|
dataCy="nt-detail-slack-channels"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Notification Color`)}
|
||||||
|
value={configuration.hex_color}
|
||||||
|
dataCy="nt-detail-slack-color"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{template.notification_type === 'twilio' && (
|
||||||
|
<>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Source Phone Number`)}
|
||||||
|
value={configuration.from_number}
|
||||||
|
dataCy="nt-detail-twilio-source-phone"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Destination SMS Number`)}
|
||||||
|
value={configuration.to_numbers} // array
|
||||||
|
dataCy="nt-detail-twilio-destination-numbers"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Account SID`)}
|
||||||
|
value={configuration.account_sid}
|
||||||
|
dataCy="nt-detail-twilio-account-sid"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{template.notification_type === 'webhook' && (
|
||||||
|
<>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Username`)}
|
||||||
|
value={configuration.username}
|
||||||
|
dataCy="nt-detail-webhook-password"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Target URL`)}
|
||||||
|
value={configuration.url}
|
||||||
|
dataCy="nt-detail-webhook-url"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Disable SSL Verification`)}
|
||||||
|
value={
|
||||||
|
configuration.disable_ssl_verification
|
||||||
|
? i18n._(t`True`)
|
||||||
|
: i18n._(t`False`)
|
||||||
|
}
|
||||||
|
dataCy="nt-detail-disable-ssl"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`HTTP Method`)}
|
||||||
|
value={configuration.http_method}
|
||||||
|
dataCy="nt-detail-webhook-http-method"
|
||||||
|
/>
|
||||||
|
<ObjectDetail
|
||||||
|
label={i18n._(t`HTTP Headers`)}
|
||||||
|
value={configuration.headers}
|
||||||
|
rows="6"
|
||||||
|
dataCy="nt-detail-webhook-headers"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DetailList>
|
||||||
|
<CardActionsRow>
|
||||||
|
{summary_fields.user_capabilities &&
|
||||||
|
summary_fields.user_capabilities.edit && (
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to={`/notification_templates/${template.id}/edit`}
|
||||||
|
aria-label={i18n._(t`Edit`)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{summary_fields.user_capabilities &&
|
||||||
|
summary_fields.user_capabilities.delete && (
|
||||||
|
<DeleteButton
|
||||||
|
name={template.name}
|
||||||
|
modalTitle={i18n._(t`Delete Notification`)}
|
||||||
|
onConfirm={deleteTemplate}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
>
|
||||||
|
{i18n._(t`Delete`)}
|
||||||
|
</DeleteButton>
|
||||||
|
)}
|
||||||
|
</CardActionsRow>
|
||||||
|
{error && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={error}
|
||||||
|
variant="error"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={dismissError}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to delete notification.`)}
|
||||||
|
<ErrorDetail error={error} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(NotificationTemplateDetail);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import NotificationTemplateDetail from './NotificationTemplateDetail';
|
||||||
|
|
||||||
|
export default NotificationTemplateDetail;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { CardBody } from '../../../components/Card';
|
||||||
|
import { OrganizationsAPI } from '../../../api';
|
||||||
|
import { Config } from '../../../contexts/Config';
|
||||||
|
|
||||||
|
import NotificationTemplateForm from '../shared/NotificationTemplateForm';
|
||||||
|
|
||||||
|
function NotificationTemplateEdit({ template }) {
|
||||||
|
const detailsUrl = `/notification_templates/${template.id}/details`;
|
||||||
|
const history = useHistory();
|
||||||
|
const [formError, setFormError] = useState(null);
|
||||||
|
|
||||||
|
const handleSubmit = async (
|
||||||
|
values,
|
||||||
|
groupsToAssociate,
|
||||||
|
groupsToDisassociate
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await OrganizationsAPI.update(template.id, values);
|
||||||
|
await Promise.all(
|
||||||
|
groupsToAssociate.map(id =>
|
||||||
|
OrganizationsAPI.associateInstanceGroup(template.id, id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
groupsToDisassociate.map(id =>
|
||||||
|
OrganizationsAPI.disassociateInstanceGroup(template.id, id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
history.push(detailsUrl);
|
||||||
|
} catch (error) {
|
||||||
|
setFormError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
history.push(detailsUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardBody>
|
||||||
|
<Config>
|
||||||
|
{({ me }) => (
|
||||||
|
<NotificationTemplateForm
|
||||||
|
template={template}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
me={me || {}}
|
||||||
|
submitError={formError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Config>
|
||||||
|
</CardBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationTemplateEdit.propTypes = {
|
||||||
|
template: PropTypes.shape().isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
NotificationTemplateEdit.contextTypes = {
|
||||||
|
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NotificationTemplateEdit as _NotificationTemplateEdit };
|
||||||
|
export default NotificationTemplateEdit;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import NotificationTemplateEdit from './NotificationTemplateEdit';
|
||||||
|
|
||||||
|
export default NotificationTemplateEdit;
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import React, { useCallback, useEffect } 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 { NotificationTemplatesAPI } from '../../../api';
|
||||||
|
import PaginatedDataList, {
|
||||||
|
ToolbarAddButton,
|
||||||
|
ToolbarDeleteButton,
|
||||||
|
} from '../../../components/PaginatedDataList';
|
||||||
|
import AlertModal from '../../../components/AlertModal';
|
||||||
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
|
import NotificationTemplateListItem from './NotificationTemplateListItem';
|
||||||
|
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
||||||
|
import useSelected from '../../../util/useSelected';
|
||||||
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
|
|
||||||
|
const QS_CONFIG = getQSConfig('notification-templates', {
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
order_by: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
function NotificationTemplatesList({ i18n }) {
|
||||||
|
const location = useLocation();
|
||||||
|
const match = useRouteMatch();
|
||||||
|
|
||||||
|
const addUrl = `${match.url}/add`;
|
||||||
|
|
||||||
|
const {
|
||||||
|
result: { templates, count, actions },
|
||||||
|
error: contentError,
|
||||||
|
isLoading: isTemplatesLoading,
|
||||||
|
request: fetchTemplates,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
|
const responses = await Promise.all([
|
||||||
|
NotificationTemplatesAPI.read(params),
|
||||||
|
NotificationTemplatesAPI.readOptions(),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
templates: responses[0].data.results,
|
||||||
|
count: responses[0].data.count,
|
||||||
|
actions: responses[1].data.actions,
|
||||||
|
};
|
||||||
|
}, [location]),
|
||||||
|
{
|
||||||
|
templates: [],
|
||||||
|
count: 0,
|
||||||
|
actions: {},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTemplates();
|
||||||
|
}, [fetchTemplates]);
|
||||||
|
|
||||||
|
const { selected, isAllSelected, handleSelect, setSelected } = useSelected(
|
||||||
|
templates
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
deleteItems: deleteTemplates,
|
||||||
|
deletionError,
|
||||||
|
clearDeletionError,
|
||||||
|
} = useDeleteItems(
|
||||||
|
useCallback(async () => {
|
||||||
|
return Promise.all(
|
||||||
|
selected.map(({ id }) => NotificationTemplatesAPI.destroy(id))
|
||||||
|
);
|
||||||
|
}, [selected]),
|
||||||
|
{
|
||||||
|
qsConfig: QS_CONFIG,
|
||||||
|
allItemsSelected: isAllSelected,
|
||||||
|
fetchItems: fetchTemplates,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
await deleteTemplates();
|
||||||
|
setSelected([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const canAdd = actions && actions.POST;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<PaginatedDataList
|
||||||
|
contentError={contentError}
|
||||||
|
hasContentLoading={isTemplatesLoading || isDeleteLoading}
|
||||||
|
items={templates}
|
||||||
|
itemCount={count}
|
||||||
|
pluralizedItemName={i18n._(t`Notification Templates`)}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
onRowClick={handleSelect}
|
||||||
|
toolbarSearchColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
isDefault: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Type`),
|
||||||
|
key: 'notification_type',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
toolbarSortColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Type`),
|
||||||
|
key: 'notification_type',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
renderToolbar={props => (
|
||||||
|
<DataListToolbar
|
||||||
|
{...props}
|
||||||
|
showSelectAll
|
||||||
|
isAllSelected={isAllSelected}
|
||||||
|
onSelectAll={() => setSelected([...templates])}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
additionalControls={[
|
||||||
|
...(canAdd
|
||||||
|
? [<ToolbarAddButton key="add" linkTo={addUrl} />]
|
||||||
|
: []),
|
||||||
|
<ToolbarDeleteButton
|
||||||
|
key="delete"
|
||||||
|
onDelete={handleDelete}
|
||||||
|
itemsToDelete={selected}
|
||||||
|
pluralizedItemName="Organizations"
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderItem={template => (
|
||||||
|
<NotificationTemplateListItem
|
||||||
|
key={template.id}
|
||||||
|
template={template}
|
||||||
|
detailUrl={`${match.url}/${template.id}`}
|
||||||
|
isSelected={selected.some(row => row.id === template.id)}
|
||||||
|
onSelect={() => handleSelect(template)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
emptyStateControls={
|
||||||
|
canAdd ? <ToolbarAddButton key="add" linkTo={addUrl} /> : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
<AlertModal
|
||||||
|
isOpen={deletionError}
|
||||||
|
variant="error"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={clearDeletionError}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to delete one or more organizations.`)}
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
|
</AlertModal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(NotificationTemplatesList);
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { OrganizationsAPI } from '../../../api';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import NotificationTemplateList from './NotificationTemplateList';
|
||||||
|
|
||||||
|
jest.mock('../../../api');
|
||||||
|
|
||||||
|
const mockTemplates = {
|
||||||
|
data: {
|
||||||
|
count: 3,
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
name: 'Boston',
|
||||||
|
id: 1,
|
||||||
|
url: '/notification_templates/1',
|
||||||
|
type: 'slack',
|
||||||
|
summary_fields: {
|
||||||
|
recent_notifications: [
|
||||||
|
{
|
||||||
|
status: 'success',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
user_capabilities: {
|
||||||
|
delete: true,
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Minneapolis',
|
||||||
|
id: 2,
|
||||||
|
url: '/notification_templates/2',
|
||||||
|
summary_fields: {
|
||||||
|
recent_notifications: [],
|
||||||
|
user_capabilities: {
|
||||||
|
delete: true,
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Philidelphia',
|
||||||
|
id: 3,
|
||||||
|
url: '/notification_templates/3',
|
||||||
|
summary_fields: {
|
||||||
|
recent_notifications: [
|
||||||
|
{
|
||||||
|
status: 'failed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 'success',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
user_capabilities: {
|
||||||
|
delete: true,
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<NotificationTemplateList />', () => {
|
||||||
|
let wrapper;
|
||||||
|
beforeEach(() => {
|
||||||
|
OrganizationsAPI.read.mockResolvedValue(mockTemplates);
|
||||||
|
OrganizationsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should load notifications', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<NotificationTemplateList />);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(OrganizationsAPI.read).toHaveBeenCalledTimes(1);
|
||||||
|
expect(wrapper.find('NotificationTemplateListItem').length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should select item', async () => {
|
||||||
|
const itemCheckboxInput = 'input#select-template-1';
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<NotificationTemplateList />);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find(itemCheckboxInput).prop('checked')).toEqual(false);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find(itemCheckboxInput)
|
||||||
|
.closest('DataListCheck')
|
||||||
|
.props()
|
||||||
|
.onChange();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find(itemCheckboxInput).prop('checked')).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should delete notifications', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<NotificationTemplateList />);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(OrganizationsAPI.read).toHaveBeenCalledTimes(1);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('Checkbox#select-all')
|
||||||
|
.props()
|
||||||
|
.onChange(true);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button[aria-label="Delete"]').simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
});
|
||||||
|
const deleteButton = global.document.querySelector(
|
||||||
|
'body div[role="dialog"] button[aria-label="confirm delete"]'
|
||||||
|
);
|
||||||
|
expect(deleteButton).not.toEqual(null);
|
||||||
|
await act(async () => {
|
||||||
|
deleteButton.click();
|
||||||
|
});
|
||||||
|
expect(OrganizationsAPI.destroy).toHaveBeenCalledTimes(3);
|
||||||
|
expect(OrganizationsAPI.read).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show error dialog shown for failed deletion', async () => {
|
||||||
|
const itemCheckboxInput = 'input#select-template-1';
|
||||||
|
OrganizationsAPI.destroy.mockRejectedValue(
|
||||||
|
new Error({
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/api/v2/organizations/1',
|
||||||
|
},
|
||||||
|
data: 'An error occurred',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<NotificationTemplateList />);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find(itemCheckboxInput)
|
||||||
|
.closest('DataListCheck')
|
||||||
|
.props()
|
||||||
|
.onChange();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button[aria-label="Delete"]').simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
});
|
||||||
|
const deleteButton = global.document.querySelector(
|
||||||
|
'body div[role="dialog"] button[aria-label="confirm delete"]'
|
||||||
|
);
|
||||||
|
expect(deleteButton).not.toEqual(null);
|
||||||
|
await act(async () => {
|
||||||
|
deleteButton.click();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
const modal = wrapper.find('Modal');
|
||||||
|
expect(modal.prop('isOpen')).toEqual(true);
|
||||||
|
expect(modal.prop('title')).toEqual('Error!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show add button', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<NotificationTemplateList />);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('ToolbarAddButton').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide add button (rbac)', async () => {
|
||||||
|
OrganizationsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<NotificationTemplateList />);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('ToolbarAddButton').length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import 'styled-components/macro';
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DataListAction as _DataListAction,
|
||||||
|
DataListCheck,
|
||||||
|
DataListItem,
|
||||||
|
DataListItemCells,
|
||||||
|
DataListItemRow,
|
||||||
|
Tooltip,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { PencilAltIcon, BellIcon } from '@patternfly/react-icons';
|
||||||
|
import { NotificationTemplatesAPI } from '../../../api';
|
||||||
|
import DataListCell from '../../../components/DataListCell';
|
||||||
|
import StatusLabel from '../../../components/StatusLabel';
|
||||||
|
import useRequest from '../../../util/useRequest';
|
||||||
|
import { NOTIFICATION_TYPES } from '../constants';
|
||||||
|
|
||||||
|
const DataListAction = styled(_DataListAction)`
|
||||||
|
align-items: center;
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 16px;
|
||||||
|
grid-template-columns: 40px 40px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function NotificationTemplateListItem({
|
||||||
|
template,
|
||||||
|
detailUrl,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
i18n,
|
||||||
|
}) {
|
||||||
|
const recentNotifications = template.summary_fields?.recent_notifications;
|
||||||
|
const latestStatus = recentNotifications
|
||||||
|
? recentNotifications[0]?.status
|
||||||
|
: null;
|
||||||
|
const [status, setStatus] = useState(latestStatus);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStatus(latestStatus);
|
||||||
|
}, [latestStatus]);
|
||||||
|
|
||||||
|
const { request: sendTestNotification, isLoading, error } = useRequest(
|
||||||
|
useCallback(() => {
|
||||||
|
NotificationTemplatesAPI.test(template.id);
|
||||||
|
setStatus('running');
|
||||||
|
}, [template.id])
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
setStatus('error');
|
||||||
|
}
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
const labelId = `template-name-${template.id}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataListItem key={template.id} aria-labelledby={labelId} id={template.id}>
|
||||||
|
<DataListItemRow>
|
||||||
|
<DataListCheck
|
||||||
|
id={`select-template-${template.id}`}
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={onSelect}
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
/>
|
||||||
|
<DataListItemCells
|
||||||
|
dataListCells={[
|
||||||
|
<DataListCell key="name" id={labelId}>
|
||||||
|
<Link to={detailUrl}>
|
||||||
|
<b>{template.name}</b>
|
||||||
|
</Link>
|
||||||
|
</DataListCell>,
|
||||||
|
<DataListCell key="status">
|
||||||
|
{status && <StatusLabel status={status} />}
|
||||||
|
</DataListCell>,
|
||||||
|
<DataListCell key="type">
|
||||||
|
<strong>{i18n._(t`Type:`)}</strong>{' '}
|
||||||
|
{NOTIFICATION_TYPES[template.notification_type] ||
|
||||||
|
template.notification_type}
|
||||||
|
</DataListCell>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<DataListAction aria-label="actions" aria-labelledby={labelId}>
|
||||||
|
<Tooltip content={i18n._(t`Test Notification`)} position="top">
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Test Notification`)}
|
||||||
|
variant="plain"
|
||||||
|
onClick={sendTestNotification}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<BellIcon />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
{template.summary_fields.user_capabilities.edit ? (
|
||||||
|
<Tooltip
|
||||||
|
content={i18n._(t`Edit Notification Template`)}
|
||||||
|
position="top"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Edit Notification Template`)}
|
||||||
|
variant="plain"
|
||||||
|
component={Link}
|
||||||
|
to={`/notification_templates/${template.id}/edit`}
|
||||||
|
>
|
||||||
|
<PencilAltIcon />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</DataListAction>
|
||||||
|
</DataListItemRow>
|
||||||
|
</DataListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(NotificationTemplateListItem);
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import { NotificationTemplatesAPI } from '../../../api';
|
||||||
|
import NotificationTemplateListItem from './NotificationTemplateListItem';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/NotificationTemplates');
|
||||||
|
|
||||||
|
const template = {
|
||||||
|
id: 3,
|
||||||
|
notification_type: 'slack',
|
||||||
|
name: 'Test Notification',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
recent_notifications: [
|
||||||
|
{
|
||||||
|
status: 'success',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<NotificationTemplateListItem />', () => {
|
||||||
|
test('should render template row', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<NotificationTemplateListItem
|
||||||
|
template={template}
|
||||||
|
detailUrl="/notification_templates/3/detail"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const cells = wrapper.find('DataListCell');
|
||||||
|
expect(cells).toHaveLength(3);
|
||||||
|
expect(cells.at(0).text()).toEqual('Test Notification');
|
||||||
|
expect(cells.at(1).text()).toEqual('Success');
|
||||||
|
expect(cells.at(2).text()).toEqual('Type: Slack');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send test notification', async () => {
|
||||||
|
NotificationTemplatesAPI.test.mockResolvedValue({});
|
||||||
|
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<NotificationTemplateListItem
|
||||||
|
template={template}
|
||||||
|
detailUrl="/notification_templates/3/detail"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('Button')
|
||||||
|
.at(0)
|
||||||
|
.invoke('onClick')();
|
||||||
|
});
|
||||||
|
expect(NotificationTemplatesAPI.test).toHaveBeenCalledTimes(1);
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('DataListCell')
|
||||||
|
.at(1)
|
||||||
|
.text()
|
||||||
|
).toEqual('Running');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import NotificationTemplateList from './NotificationTemplateList';
|
||||||
|
|
||||||
|
export default NotificationTemplateList;
|
||||||
|
export { default as NotificationTemplateListItem } from './NotificationTemplateListItem';
|
||||||
@@ -1,28 +1,51 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import { Route, Switch, 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 {
|
import NotificationTemplateList from './NotificationTemplateList';
|
||||||
PageSection,
|
import NotificationTemplateAdd from './NotificationTemplateAdd';
|
||||||
PageSectionVariants,
|
import NotificationTemplate from './NotificationTemplate';
|
||||||
Title,
|
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
class NotificationTemplates extends Component {
|
function NotificationTemplates({ i18n }) {
|
||||||
render() {
|
const match = useRouteMatch();
|
||||||
const { i18n } = this.props;
|
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
||||||
const { light } = PageSectionVariants;
|
'/notification_templates': i18n._(t`Notification Templates`),
|
||||||
|
'/notification_templates/add': i18n._(t`Create New Notification Template`),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
const updateBreadcrumbConfig = useCallback(
|
||||||
<Fragment>
|
notification => {
|
||||||
<PageSection variant={light} className="pf-m-condensed">
|
const { id } = notification;
|
||||||
<Title size="2xl" headingLevel="h2">
|
setBreadcrumbConfig({
|
||||||
{i18n._(t`Notification Templates`)}
|
'/notification_templates': i18n._(t`Notification Templates`),
|
||||||
</Title>
|
'/notification_templates/add': i18n._(
|
||||||
</PageSection>
|
t`Create New Notification Template`
|
||||||
<PageSection />
|
),
|
||||||
</Fragment>
|
[`/notification_templates/${id}`]: notification.name,
|
||||||
);
|
[`/notification_templates/${id}/edit`]: i18n._(t`Edit Details`),
|
||||||
}
|
[`/notification_templates/${id}/details`]: i18n._(t`Details`),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[i18n]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
||||||
|
<Switch>
|
||||||
|
<Route path={`${match.url}/add`}>
|
||||||
|
<NotificationTemplateAdd />
|
||||||
|
</Route>
|
||||||
|
<Route path={`${match.url}/:id`}>
|
||||||
|
<NotificationTemplate setBreadcrumb={updateBreadcrumbConfig} />
|
||||||
|
</Route>
|
||||||
|
<Route path={`${match.url}`}>
|
||||||
|
<NotificationTemplateList />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n()(NotificationTemplates);
|
export default withI18n()(NotificationTemplates);
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import NotificationTemplates from './NotificationTemplates';
|
import NotificationTemplates from './NotificationTemplates';
|
||||||
|
|
||||||
describe('<NotificationTemplates />', () => {
|
describe('<NotificationTemplates />', () => {
|
||||||
let pageWrapper;
|
let pageWrapper;
|
||||||
let pageSections;
|
let pageSections;
|
||||||
let title;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
pageWrapper = mountWithContexts(<NotificationTemplates />);
|
pageWrapper = mountWithContexts(<NotificationTemplates />);
|
||||||
pageSections = pageWrapper.find('PageSection');
|
pageSections = pageWrapper.find('PageSection');
|
||||||
title = pageWrapper.find('Title');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -22,8 +18,6 @@ describe('<NotificationTemplates />', () => {
|
|||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(pageWrapper.length).toBe(1);
|
expect(pageWrapper.length).toBe(1);
|
||||||
expect(pageSections.length).toBe(2);
|
expect(pageSections.length).toBe(2);
|
||||||
expect(title.length).toBe(1);
|
|
||||||
expect(title.props().size).toBe('2xl');
|
|
||||||
expect(pageSections.first().props().variant).toBe('light');
|
expect(pageSections.first().props().variant).toBe('light');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
12
awx/ui_next/src/screens/NotificationTemplate/constants.js
Normal file
12
awx/ui_next/src/screens/NotificationTemplate/constants.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* eslint-disable-next-line import/prefer-default-export */
|
||||||
|
export const NOTIFICATION_TYPES = {
|
||||||
|
email: 'Email',
|
||||||
|
grafana: 'Grafana',
|
||||||
|
irc: 'IRC',
|
||||||
|
mattermost: 'Mattermost',
|
||||||
|
pagerduty: 'Pagerduty',
|
||||||
|
rocketchat: 'Rocket.Chat',
|
||||||
|
slack: 'Slack',
|
||||||
|
twilio: 'Twilio',
|
||||||
|
webhook: 'Webhook',
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function NotificationTemplateForm() {
|
||||||
|
//
|
||||||
|
}
|
||||||
@@ -62,12 +62,10 @@ function OrganizationListItem({
|
|||||||
/>
|
/>
|
||||||
<DataListItemCells
|
<DataListItemCells
|
||||||
dataListCells={[
|
dataListCells={[
|
||||||
<DataListCell key="divider">
|
<DataListCell key="name" id={labelId}>
|
||||||
<span id={labelId}>
|
<Link to={`${detailUrl}`}>
|
||||||
<Link to={`${detailUrl}`}>
|
<b>{organization.name}</b>
|
||||||
<b>{organization.name}</b>
|
</Link>
|
||||||
</Link>
|
|
||||||
</span>
|
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
<DataListCell key="related-field-counts">
|
<DataListCell key="related-field-counts">
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
@@ -85,11 +83,7 @@ function OrganizationListItem({
|
|||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<DataListAction
|
<DataListAction aria-label="actions" aria-labelledby={labelId}>
|
||||||
aria-label="actions"
|
|
||||||
aria-labelledby={labelId}
|
|
||||||
id={labelId}
|
|
||||||
>
|
|
||||||
{organization.summary_fields.user_capabilities.edit ? (
|
{organization.summary_fields.user_capabilities.edit ? (
|
||||||
<Tooltip content={i18n._(t`Edit Organization`)} position="top">
|
<Tooltip content={i18n._(t`Edit Organization`)} position="top">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function TemplateListItem({
|
|||||||
/>
|
/>
|
||||||
<DataListItemCells
|
<DataListItemCells
|
||||||
dataListCells={[
|
dataListCells={[
|
||||||
<DataListCell key="divider">
|
<DataListCell key="name" id={labelId}>
|
||||||
<span>
|
<span>
|
||||||
<Link to={`${detailUrl}`}>
|
<Link to={`${detailUrl}`}>
|
||||||
<b>{template.name}</b>
|
<b>{template.name}</b>
|
||||||
@@ -105,11 +105,7 @@ function TemplateListItem({
|
|||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<DataListAction
|
<DataListAction aria-label="actions" aria-labelledby={labelId}>
|
||||||
aria-label="actions"
|
|
||||||
aria-labelledby={labelId}
|
|
||||||
id={labelId}
|
|
||||||
>
|
|
||||||
{canLaunch && template.type === 'job_template' && (
|
{canLaunch && template.type === 'job_template' && (
|
||||||
<Tooltip content={i18n._(t`Launch Template`)} position="top">
|
<Tooltip content={i18n._(t`Launch Template`)} position="top">
|
||||||
<LaunchButton resource={template}>
|
<LaunchButton resource={template}>
|
||||||
|
|||||||
Reference in New Issue
Block a user