mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03: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:
commit
025a979cb2
@ -5,6 +5,10 @@ class NotificationTemplates extends Base {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/notification_templates/';
|
||||
}
|
||||
|
||||
test(id) {
|
||||
return this.http.post(`${this.baseUrl}${id}/test/`);
|
||||
}
|
||||
}
|
||||
|
||||
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 UserDateDetail } from './UserDateDetail';
|
||||
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 { t } from '@lingui/macro';
|
||||
import {
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
import NotificationTemplateList from './NotificationTemplateList';
|
||||
import NotificationTemplateAdd from './NotificationTemplateAdd';
|
||||
import NotificationTemplate from './NotificationTemplate';
|
||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
||||
|
||||
class NotificationTemplates extends Component {
|
||||
render() {
|
||||
const { i18n } = this.props;
|
||||
const { light } = PageSectionVariants;
|
||||
function NotificationTemplates({ i18n }) {
|
||||
const match = useRouteMatch();
|
||||
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
||||
'/notification_templates': i18n._(t`Notification Templates`),
|
||||
'/notification_templates/add': i18n._(t`Create New Notification Template`),
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageSection variant={light} className="pf-m-condensed">
|
||||
<Title size="2xl" headingLevel="h2">
|
||||
{i18n._(t`Notification Templates`)}
|
||||
</Title>
|
||||
</PageSection>
|
||||
<PageSection />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
const updateBreadcrumbConfig = useCallback(
|
||||
notification => {
|
||||
const { id } = notification;
|
||||
setBreadcrumbConfig({
|
||||
'/notification_templates': i18n._(t`Notification Templates`),
|
||||
'/notification_templates/add': i18n._(
|
||||
t`Create New Notification Template`
|
||||
),
|
||||
[`/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);
|
||||
|
||||
@ -1,18 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
|
||||
import NotificationTemplates from './NotificationTemplates';
|
||||
|
||||
describe('<NotificationTemplates />', () => {
|
||||
let pageWrapper;
|
||||
let pageSections;
|
||||
let title;
|
||||
|
||||
beforeEach(() => {
|
||||
pageWrapper = mountWithContexts(<NotificationTemplates />);
|
||||
pageSections = pageWrapper.find('PageSection');
|
||||
title = pageWrapper.find('Title');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -22,8 +18,6 @@ describe('<NotificationTemplates />', () => {
|
||||
test('initially renders without crashing', () => {
|
||||
expect(pageWrapper.length).toBe(1);
|
||||
expect(pageSections.length).toBe(2);
|
||||
expect(title.length).toBe(1);
|
||||
expect(title.props().size).toBe('2xl');
|
||||
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
|
||||
dataListCells={[
|
||||
<DataListCell key="divider">
|
||||
<span id={labelId}>
|
||||
<Link to={`${detailUrl}`}>
|
||||
<b>{organization.name}</b>
|
||||
</Link>
|
||||
</span>
|
||||
<DataListCell key="name" id={labelId}>
|
||||
<Link to={`${detailUrl}`}>
|
||||
<b>{organization.name}</b>
|
||||
</Link>
|
||||
</DataListCell>,
|
||||
<DataListCell key="related-field-counts">
|
||||
<ListGroup>
|
||||
@ -85,11 +83,7 @@ function OrganizationListItem({
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
<DataListAction
|
||||
aria-label="actions"
|
||||
aria-labelledby={labelId}
|
||||
id={labelId}
|
||||
>
|
||||
<DataListAction aria-label="actions" aria-labelledby={labelId}>
|
||||
{organization.summary_fields.user_capabilities.edit ? (
|
||||
<Tooltip content={i18n._(t`Edit Organization`)} position="top">
|
||||
<Button
|
||||
|
||||
@ -78,7 +78,7 @@ function TemplateListItem({
|
||||
/>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key="divider">
|
||||
<DataListCell key="name" id={labelId}>
|
||||
<span>
|
||||
<Link to={`${detailUrl}`}>
|
||||
<b>{template.name}</b>
|
||||
@ -105,11 +105,7 @@ function TemplateListItem({
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
<DataListAction
|
||||
aria-label="actions"
|
||||
aria-labelledby={labelId}
|
||||
id={labelId}
|
||||
>
|
||||
<DataListAction aria-label="actions" aria-labelledby={labelId}>
|
||||
{canLaunch && template.type === 'job_template' && (
|
||||
<Tooltip content={i18n._(t`Launch Template`)} position="top">
|
||||
<LaunchButton resource={template}>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user