Refactor notifications list to be more generic. Hook notifictions tab up on JT details.

This commit is contained in:
mabashian 2019-10-03 09:07:14 -04:00
parent cf27ac295a
commit 2457926f0a
16 changed files with 826 additions and 259 deletions

View File

@ -8,6 +8,7 @@ import JobTemplates from './models/JobTemplates';
import Jobs from './models/Jobs';
import Labels from './models/Labels';
import Me from './models/Me';
import NotificationTemplates from './models/NotificationTemplates';
import Organizations from './models/Organizations';
import Projects from './models/Projects';
import ProjectUpdates from './models/ProjectUpdates';
@ -30,6 +31,7 @@ const JobTemplatesAPI = new JobTemplates();
const JobsAPI = new Jobs();
const LabelsAPI = new Labels();
const MeAPI = new Me();
const NotificationTemplatesAPI = new NotificationTemplates();
const OrganizationsAPI = new Organizations();
const ProjectsAPI = new Projects();
const ProjectUpdatesAPI = new ProjectUpdates();
@ -53,6 +55,7 @@ export {
JobsAPI,
LabelsAPI,
MeAPI,
NotificationTemplatesAPI,
OrganizationsAPI,
ProjectsAPI,
ProjectUpdatesAPI,

View File

@ -1,7 +1,8 @@
import Base from '../Base';
import NotificationsMixin from '../mixins/Notifications.mixin';
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
class JobTemplates extends InstanceGroupsMixin(Base) {
class JobTemplates extends InstanceGroupsMixin(NotificationsMixin(Base)) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/job_templates/';

View File

@ -0,0 +1,10 @@
import Base from '../Base';
class NotificationTemplates extends Base {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/notification_templates/';
}
}
export default NotificationTemplates;

View File

@ -4,13 +4,14 @@ import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { OrganizationsAPI } from '@api';
import AlertModal from '@components/AlertModal';
import ErrorDetail from '@components/ErrorDetail';
import NotificationListItem from '@components/NotificationsList/NotificationListItem';
import NotificationListItem from '@components/NotificationList/NotificationListItem';
import PaginatedDataList from '@components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '@util/qs';
import { NotificationTemplatesAPI } from '@api';
const QS_CONFIG = getQSConfig('notification', {
page: 1,
page_size: 5,
@ -23,7 +24,7 @@ const COLUMNS = [
{ key: 'created', name: 'Created', isSortable: true, isNumeric: true },
];
class OrganizationNotifications extends Component {
class NotificationList extends Component {
constructor(props) {
super(props);
this.state = {
@ -57,26 +58,23 @@ class OrganizationNotifications extends Component {
}
async loadNotifications() {
const { id, location } = this.props;
const { id, location, apiModel } = this.props;
const { typeLabels } = this.state;
const params = parseQueryString(QS_CONFIG, location.search);
const promises = [OrganizationsAPI.readNotificationTemplates(id, params)];
const promises = [NotificationTemplatesAPI.read(params)];
if (!typeLabels) {
promises.push(OrganizationsAPI.readOptionsNotificationTemplates(id));
promises.push(NotificationTemplatesAPI.readOptions());
}
this.setState({ contentError: null, hasContentLoading: true });
try {
const {
data: { count: itemCount = 0, results: notifications = [] },
} = await OrganizationsAPI.readNotificationTemplates(id, params);
} = await NotificationTemplatesAPI.read(params);
const optionsResponse = await OrganizationsAPI.readOptionsNotificationTemplates(
id,
params
);
const optionsResponse = await NotificationTemplatesAPI.readOptions();
let idMatchParams;
if (notifications.length > 0) {
@ -90,9 +88,9 @@ class OrganizationNotifications extends Component {
{ data: successTemplates },
{ data: errorTemplates },
] = await Promise.all([
OrganizationsAPI.readNotificationTemplatesStarted(id, idMatchParams),
OrganizationsAPI.readNotificationTemplatesSuccess(id, idMatchParams),
OrganizationsAPI.readNotificationTemplatesError(id, idMatchParams),
apiModel.readNotificationTemplatesStarted(id, idMatchParams),
apiModel.readNotificationTemplatesSuccess(id, idMatchParams),
apiModel.readNotificationTemplatesError(id, idMatchParams),
]);
const stateToUpdate = {
@ -129,7 +127,7 @@ class OrganizationNotifications extends Component {
}
async handleNotificationToggle(notificationId, isCurrentlyOn, status) {
const { id } = this.props;
const { id, apiModel } = this.props;
let stateArrayName;
if (status === 'success') {
@ -158,13 +156,13 @@ class OrganizationNotifications extends Component {
this.setState({ toggleLoading: true });
try {
if (isCurrentlyOn) {
await OrganizationsAPI.disassociateNotificationTemplate(
await apiModel.disassociateNotificationTemplate(
id,
notificationId,
status
);
} else {
await OrganizationsAPI.associateNotificationTemplate(
await apiModel.associateNotificationTemplate(
id,
notificationId,
status
@ -204,7 +202,7 @@ class OrganizationNotifications extends Component {
hasContentLoading={hasContentLoading}
items={notifications}
itemCount={itemCount}
pluralizedItemName="Notifications"
pluralizedItemName={i18n._(t`Notifications`)}
qsConfig={QS_CONFIG}
toolbarColumns={COLUMNS}
renderItem={notification => (
@ -235,7 +233,7 @@ class OrganizationNotifications extends Component {
}
}
OrganizationNotifications.propTypes = {
NotificationList.propTypes = {
id: number.isRequired,
canToggleNotifications: bool.isRequired,
location: shape({
@ -243,5 +241,5 @@ OrganizationNotifications.propTypes = {
}).isRequired,
};
export { OrganizationNotifications as _OrganizationNotifications };
export default withI18n()(withRouter(OrganizationNotifications));
export { NotificationList as _NotificationList };
export default withI18n()(withRouter(NotificationList));

View File

@ -1,13 +1,15 @@
import React from 'react';
import { OrganizationsAPI } from '@api';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils';
import OrganizationNotifications from './OrganizationNotifications';
import { NotificationTemplatesAPI } from '@api';
import NotificationList from './NotificationList';
jest.mock('@api');
describe('<OrganizationNotifications />', () => {
describe('<NotificationList />', () => {
const data = {
count: 2,
results: [
@ -32,7 +34,19 @@ describe('<OrganizationNotifications />', () => {
],
};
OrganizationsAPI.readOptionsNotificationTemplates.mockReturnValue({
const MockModel = jest.fn().mockImplementation(() => {
return {
readNotificationTemplatesSuccess: jest.fn(),
readNotificationTemplatesError: jest.fn(),
readNotificationTemplatesStarted: jest.fn(),
associateNotificationTemplate: jest.fn(),
disassociateNotificationTemplate: jest.fn(),
};
});
const MockModelAPI = new MockModel();
NotificationTemplatesAPI.readOptions.mockReturnValue({
data: {
actions: {
GET: {
@ -45,14 +59,14 @@ describe('<OrganizationNotifications />', () => {
});
beforeEach(() => {
OrganizationsAPI.readNotificationTemplates.mockReturnValue({ data });
OrganizationsAPI.readNotificationTemplatesSuccess.mockReturnValue({
NotificationTemplatesAPI.read.mockReturnValue({ data });
MockModelAPI.readNotificationTemplatesSuccess.mockReturnValue({
data: { results: [{ id: 1 }] },
});
OrganizationsAPI.readNotificationTemplatesError.mockReturnValue({
MockModelAPI.readNotificationTemplatesError.mockReturnValue({
data: { results: [{ id: 2 }] },
});
OrganizationsAPI.readNotificationTemplatesStarted.mockReturnValue({
MockModelAPI.readNotificationTemplatesStarted.mockReturnValue({
data: { results: [{ id: 3 }] },
});
});
@ -63,7 +77,7 @@ describe('<OrganizationNotifications />', () => {
test('initially renders succesfully', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />
<NotificationList id={1} canToggleNotifications apiModel={MockModelAPI} />
);
await sleep(0);
wrapper.update();
@ -72,15 +86,15 @@ describe('<OrganizationNotifications />', () => {
test('should render list fetched of items', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />
<NotificationList id={1} canToggleNotifications apiModel={MockModelAPI} />
);
await sleep(0);
wrapper.update();
expect(OrganizationsAPI.readNotificationTemplates).toHaveBeenCalled();
expect(
wrapper.find('OrganizationNotifications').state('notifications')
).toEqual(data.results);
expect(NotificationTemplatesAPI.read).toHaveBeenCalled();
expect(wrapper.find('NotificationList').state('notifications')).toEqual(
data.results
);
const items = wrapper.find('NotificationListItem');
expect(items).toHaveLength(3);
expect(items.at(0).prop('successTurnedOn')).toEqual(true);
@ -96,13 +110,13 @@ describe('<OrganizationNotifications />', () => {
test('should enable success notification', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />
<NotificationList id={1} canToggleNotifications apiModel={MockModelAPI} />
);
await sleep(0);
wrapper.update();
expect(
wrapper.find('OrganizationNotifications').state('successTemplateIds')
wrapper.find('NotificationList').state('successTemplateIds')
).toEqual([1]);
const items = wrapper.find('NotificationListItem');
items
@ -110,7 +124,7 @@ describe('<OrganizationNotifications />', () => {
.find('Switch')
.at(1)
.prop('onChange')();
expect(OrganizationsAPI.associateNotificationTemplate).toHaveBeenCalledWith(
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
1,
2,
'success'
@ -118,47 +132,48 @@ describe('<OrganizationNotifications />', () => {
await sleep(0);
wrapper.update();
expect(
wrapper.find('OrganizationNotifications').state('successTemplateIds')
wrapper.find('NotificationList').state('successTemplateIds')
).toEqual([1, 2]);
});
test('should enable error notification', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />
<NotificationList id={1} canToggleNotifications apiModel={MockModelAPI} />
);
await sleep(0);
wrapper.update();
expect(
wrapper.find('OrganizationNotifications').state('errorTemplateIds')
).toEqual([2]);
expect(wrapper.find('NotificationList').state('errorTemplateIds')).toEqual([
2,
]);
const items = wrapper.find('NotificationListItem');
items
.at(0)
.find('Switch')
.at(2)
.prop('onChange')();
expect(OrganizationsAPI.associateNotificationTemplate).toHaveBeenCalledWith(
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
1,
1,
'error'
);
await sleep(0);
wrapper.update();
expect(
wrapper.find('OrganizationNotifications').state('errorTemplateIds')
).toEqual([2, 1]);
expect(wrapper.find('NotificationList').state('errorTemplateIds')).toEqual([
2,
1,
]);
});
test('should enable start notification', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />
<NotificationList id={1} canToggleNotifications apiModel={MockModelAPI} />
);
await sleep(0);
wrapper.update();
expect(
wrapper.find('OrganizationNotifications').state('startedTemplateIds')
wrapper.find('NotificationList').state('startedTemplateIds')
).toEqual([3]);
const items = wrapper.find('NotificationListItem');
items
@ -166,7 +181,7 @@ describe('<OrganizationNotifications />', () => {
.find('Switch')
.at(0)
.prop('onChange')();
expect(OrganizationsAPI.associateNotificationTemplate).toHaveBeenCalledWith(
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
1,
1,
'started'
@ -174,19 +189,19 @@ describe('<OrganizationNotifications />', () => {
await sleep(0);
wrapper.update();
expect(
wrapper.find('OrganizationNotifications').state('startedTemplateIds')
wrapper.find('NotificationList').state('startedTemplateIds')
).toEqual([3, 1]);
});
test('should disable success notification', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />
<NotificationList id={1} canToggleNotifications apiModel={MockModelAPI} />
);
await sleep(0);
wrapper.update();
expect(
wrapper.find('OrganizationNotifications').state('successTemplateIds')
wrapper.find('NotificationList').state('successTemplateIds')
).toEqual([1]);
const items = wrapper.find('NotificationListItem');
items
@ -194,51 +209,55 @@ describe('<OrganizationNotifications />', () => {
.find('Switch')
.at(1)
.prop('onChange')();
expect(
OrganizationsAPI.disassociateNotificationTemplate
).toHaveBeenCalledWith(1, 1, 'success');
expect(MockModelAPI.disassociateNotificationTemplate).toHaveBeenCalledWith(
1,
1,
'success'
);
await sleep(0);
wrapper.update();
expect(
wrapper.find('OrganizationNotifications').state('successTemplateIds')
wrapper.find('NotificationList').state('successTemplateIds')
).toEqual([]);
});
test('should disable error notification', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />
<NotificationList id={1} canToggleNotifications apiModel={MockModelAPI} />
);
await sleep(0);
wrapper.update();
expect(
wrapper.find('OrganizationNotifications').state('errorTemplateIds')
).toEqual([2]);
expect(wrapper.find('NotificationList').state('errorTemplateIds')).toEqual([
2,
]);
const items = wrapper.find('NotificationListItem');
items
.at(1)
.find('Switch')
.at(2)
.prop('onChange')();
expect(
OrganizationsAPI.disassociateNotificationTemplate
).toHaveBeenCalledWith(1, 2, 'error');
expect(MockModelAPI.disassociateNotificationTemplate).toHaveBeenCalledWith(
1,
2,
'error'
);
await sleep(0);
wrapper.update();
expect(
wrapper.find('OrganizationNotifications').state('errorTemplateIds')
).toEqual([]);
expect(wrapper.find('NotificationList').state('errorTemplateIds')).toEqual(
[]
);
});
test('should disable start notification', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />
<NotificationList id={1} canToggleNotifications apiModel={MockModelAPI} />
);
await sleep(0);
wrapper.update();
expect(
wrapper.find('OrganizationNotifications').state('startedTemplateIds')
wrapper.find('NotificationList').state('startedTemplateIds')
).toEqual([3]);
const items = wrapper.find('NotificationListItem');
items
@ -246,13 +265,15 @@ describe('<OrganizationNotifications />', () => {
.find('Switch')
.at(0)
.prop('onChange')();
expect(
OrganizationsAPI.disassociateNotificationTemplate
).toHaveBeenCalledWith(1, 3, 'started');
expect(MockModelAPI.disassociateNotificationTemplate).toHaveBeenCalledWith(
1,
3,
'started'
);
await sleep(0);
wrapper.update();
expect(
wrapper.find('OrganizationNotifications').state('startedTemplateIds')
wrapper.find('NotificationList').state('startedTemplateIds')
).toEqual([]);
});
});

View File

@ -106,9 +106,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"$$typeof": Symbol(react.forward_ref),
"attrs": Array [],
"componentStyle": ComponentStyle {
"componentId": "NotificationListItem__DataListCell-j7c411-0",
"componentId": "NotificationListItem__DataListCell-w674ng-0",
"isStatic": false,
"lastClassName": "hoXOpW",
"lastClassName": "dXsFLF",
"rules": Array [
"display:flex;justify-content:",
[Function],
@ -122,7 +122,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"displayName": "NotificationListItem__DataListCell",
"foldedComponentIds": Array [],
"render": [Function],
"styledComponentId": "NotificationListItem__DataListCell-j7c411-0",
"styledComponentId": "NotificationListItem__DataListCell-w674ng-0",
"target": [Function],
"toString": [Function],
"warnTooManyClasses": [Function],
@ -132,10 +132,10 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
forwardedRef={null}
>
<DataListCell
className="NotificationListItem__DataListCell-j7c411-0 kIdLtz"
className="NotificationListItem__DataListCell-w674ng-0 faYgxF"
>
<div
className="pf-c-data-list__cell NotificationListItem__DataListCell-j7c411-0 kIdLtz"
className="pf-c-data-list__cell NotificationListItem__DataListCell-w674ng-0 faYgxF"
>
<Styled(Link)
to={
@ -209,9 +209,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"$$typeof": Symbol(react.forward_ref),
"attrs": Array [],
"componentStyle": ComponentStyle {
"componentId": "NotificationListItem__DataListCell-j7c411-0",
"componentId": "NotificationListItem__DataListCell-w674ng-0",
"isStatic": false,
"lastClassName": "hoXOpW",
"lastClassName": "dXsFLF",
"rules": Array [
"display:flex;justify-content:",
[Function],
@ -225,7 +225,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"displayName": "NotificationListItem__DataListCell",
"foldedComponentIds": Array [],
"render": [Function],
"styledComponentId": "NotificationListItem__DataListCell-j7c411-0",
"styledComponentId": "NotificationListItem__DataListCell-w674ng-0",
"target": [Function],
"toString": [Function],
"warnTooManyClasses": [Function],
@ -235,10 +235,10 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
forwardedRef={null}
>
<DataListCell
className="NotificationListItem__DataListCell-j7c411-0 kIdLtz"
className="NotificationListItem__DataListCell-w674ng-0 faYgxF"
>
<div
className="pf-c-data-list__cell NotificationListItem__DataListCell-j7c411-0 kIdLtz"
className="pf-c-data-list__cell NotificationListItem__DataListCell-w674ng-0 faYgxF"
>
Slack
</div>
@ -255,9 +255,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"$$typeof": Symbol(react.forward_ref),
"attrs": Array [],
"componentStyle": ComponentStyle {
"componentId": "NotificationListItem__DataListCell-j7c411-0",
"componentId": "NotificationListItem__DataListCell-w674ng-0",
"isStatic": false,
"lastClassName": "hoXOpW",
"lastClassName": "dXsFLF",
"rules": Array [
"display:flex;justify-content:",
[Function],
@ -271,7 +271,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"displayName": "NotificationListItem__DataListCell",
"foldedComponentIds": Array [],
"render": [Function],
"styledComponentId": "NotificationListItem__DataListCell-j7c411-0",
"styledComponentId": "NotificationListItem__DataListCell-w674ng-0",
"target": [Function],
"toString": [Function],
"warnTooManyClasses": [Function],
@ -282,11 +282,11 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
righthalf="true"
>
<DataListCell
className="NotificationListItem__DataListCell-j7c411-0 hoXOpW"
className="NotificationListItem__DataListCell-w674ng-0 dXsFLF"
righthalf="true"
>
<div
className="pf-c-data-list__cell NotificationListItem__DataListCell-j7c411-0 hoXOpW"
className="pf-c-data-list__cell NotificationListItem__DataListCell-w674ng-0 dXsFLF"
righthalf="true"
>
<NotificationListItem__Switch
@ -305,9 +305,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"$$typeof": Symbol(react.forward_ref),
"attrs": Array [],
"componentStyle": ComponentStyle {
"componentId": "NotificationListItem__Switch-j7c411-1",
"componentId": "NotificationListItem__Switch-w674ng-1",
"isStatic": true,
"lastClassName": "ceuHGn",
"lastClassName": "hbNxaH",
"rules": Array [
"display:flex;flex-wrap:no-wrap;",
],
@ -315,7 +315,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"displayName": "NotificationListItem__Switch",
"foldedComponentIds": Array [],
"render": [Function],
"styledComponentId": "NotificationListItem__Switch-j7c411-1",
"styledComponentId": "NotificationListItem__Switch-w674ng-1",
"target": [Function],
"toString": [Function],
"warnTooManyClasses": [Function],
@ -332,7 +332,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
>
<Component
aria-label="Toggle notification start"
className="NotificationListItem__Switch-j7c411-1 ceuHGn"
className="NotificationListItem__Switch-w674ng-1 hbNxaH"
id="notification-9000-started-toggle"
isChecked={false}
isDisabled={false}
@ -345,7 +345,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
componentProps={
Object {
"aria-label": "Toggle notification start",
"className": "NotificationListItem__Switch-j7c411-1 ceuHGn",
"className": "NotificationListItem__Switch-w674ng-1 hbNxaH",
"id": "notification-9000-started-toggle",
"isChecked": false,
"isDisabled": false,
@ -358,7 +358,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
>
<Switch
aria-label="Toggle notification start"
className="NotificationListItem__Switch-j7c411-1 ceuHGn"
className="NotificationListItem__Switch-w674ng-1 hbNxaH"
id="notification-9000-started-toggle"
isChecked={false}
isDisabled={false}
@ -373,7 +373,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
}
>
<label
className="pf-c-switch NotificationListItem__Switch-j7c411-1 ceuHGn"
className="pf-c-switch NotificationListItem__Switch-w674ng-1 hbNxaH"
htmlFor="notification-9000-started-toggle"
>
<input
@ -425,9 +425,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"$$typeof": Symbol(react.forward_ref),
"attrs": Array [],
"componentStyle": ComponentStyle {
"componentId": "NotificationListItem__Switch-j7c411-1",
"componentId": "NotificationListItem__Switch-w674ng-1",
"isStatic": true,
"lastClassName": "ceuHGn",
"lastClassName": "hbNxaH",
"rules": Array [
"display:flex;flex-wrap:no-wrap;",
],
@ -435,7 +435,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"displayName": "NotificationListItem__Switch",
"foldedComponentIds": Array [],
"render": [Function],
"styledComponentId": "NotificationListItem__Switch-j7c411-1",
"styledComponentId": "NotificationListItem__Switch-w674ng-1",
"target": [Function],
"toString": [Function],
"warnTooManyClasses": [Function],
@ -452,7 +452,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
>
<Component
aria-label="Toggle notification success"
className="NotificationListItem__Switch-j7c411-1 ceuHGn"
className="NotificationListItem__Switch-w674ng-1 hbNxaH"
id="notification-9000-success-toggle"
isChecked={false}
isDisabled={false}
@ -465,7 +465,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
componentProps={
Object {
"aria-label": "Toggle notification success",
"className": "NotificationListItem__Switch-j7c411-1 ceuHGn",
"className": "NotificationListItem__Switch-w674ng-1 hbNxaH",
"id": "notification-9000-success-toggle",
"isChecked": false,
"isDisabled": false,
@ -478,7 +478,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
>
<Switch
aria-label="Toggle notification success"
className="NotificationListItem__Switch-j7c411-1 ceuHGn"
className="NotificationListItem__Switch-w674ng-1 hbNxaH"
id="notification-9000-success-toggle"
isChecked={false}
isDisabled={false}
@ -493,7 +493,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
}
>
<label
className="pf-c-switch NotificationListItem__Switch-j7c411-1 ceuHGn"
className="pf-c-switch NotificationListItem__Switch-w674ng-1 hbNxaH"
htmlFor="notification-9000-success-toggle"
>
<input
@ -545,9 +545,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"$$typeof": Symbol(react.forward_ref),
"attrs": Array [],
"componentStyle": ComponentStyle {
"componentId": "NotificationListItem__Switch-j7c411-1",
"componentId": "NotificationListItem__Switch-w674ng-1",
"isStatic": true,
"lastClassName": "ceuHGn",
"lastClassName": "hbNxaH",
"rules": Array [
"display:flex;flex-wrap:no-wrap;",
],
@ -555,7 +555,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"displayName": "NotificationListItem__Switch",
"foldedComponentIds": Array [],
"render": [Function],
"styledComponentId": "NotificationListItem__Switch-j7c411-1",
"styledComponentId": "NotificationListItem__Switch-w674ng-1",
"target": [Function],
"toString": [Function],
"warnTooManyClasses": [Function],
@ -572,7 +572,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
>
<Component
aria-label="Toggle notification failure"
className="NotificationListItem__Switch-j7c411-1 ceuHGn"
className="NotificationListItem__Switch-w674ng-1 hbNxaH"
id="notification-9000-error-toggle"
isChecked={false}
isDisabled={false}
@ -585,7 +585,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
componentProps={
Object {
"aria-label": "Toggle notification failure",
"className": "NotificationListItem__Switch-j7c411-1 ceuHGn",
"className": "NotificationListItem__Switch-w674ng-1 hbNxaH",
"id": "notification-9000-error-toggle",
"isChecked": false,
"isDisabled": false,
@ -598,7 +598,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
>
<Switch
aria-label="Toggle notification failure"
className="NotificationListItem__Switch-j7c411-1 ceuHGn"
className="NotificationListItem__Switch-w674ng-1 hbNxaH"
id="notification-9000-error-toggle"
isChecked={false}
isDisabled={false}
@ -613,7 +613,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
}
>
<label
className="pf-c-switch NotificationListItem__Switch-j7c411-1 ceuHGn"
className="pf-c-switch NotificationListItem__Switch-w674ng-1 hbNxaH"
htmlFor="notification-9000-error-toggle"
>
<input

View File

@ -0,0 +1,2 @@
export { default } from './NotificationList';
export { default as NotificationListItem } from './NotificationListItem';

View File

@ -1 +0,0 @@
export { default } from './NotificationListItem';

View File

@ -11,10 +11,10 @@ import styled from 'styled-components';
import CardCloseButton from '@components/CardCloseButton';
import RoutedTabs from '@components/RoutedTabs';
import ContentError from '@components/ContentError';
import NotificationList from '@components/NotificationList/NotificationList';
import { OrganizationAccess } from './OrganizationAccess';
import OrganizationDetail from './OrganizationDetail';
import OrganizationEdit from './OrganizationEdit';
import OrganizationNotifications from './OrganizationNotifications';
import OrganizationTeams from './OrganizationTeams';
import { OrganizationsAPI } from '@api';
@ -228,9 +228,10 @@ class Organization extends Component {
<Route
path="/organizations/:id/notifications"
render={() => (
<OrganizationNotifications
<NotificationList
id={Number(match.params.id)}
canToggleNotifications={canToggleNotifications}
apiModel={OrganizationsAPI}
/>
)}
/>

View File

@ -1 +0,0 @@
export { default } from './OrganizationNotifications';

View File

@ -5,9 +5,10 @@ import { Card, CardHeader, PageSection } from '@patternfly/react-core';
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
import CardCloseButton from '@components/CardCloseButton';
import ContentError from '@components/ContentError';
import NotificationList from '@components/NotificationList';
import RoutedTabs from '@components/RoutedTabs';
import JobTemplateDetail from './JobTemplateDetail';
import { JobTemplatesAPI } from '@api';
import { JobTemplatesAPI, OrganizationsAPI } from '@api';
import JobTemplateEdit from './JobTemplateEdit';
class Template extends Component {
@ -18,12 +19,14 @@ class Template extends Component {
contentError: null,
hasContentLoading: true,
template: null,
isNotifAdmin: false,
};
this.loadTemplate = this.loadTemplate.bind(this);
this.loadTemplateAndRoles = this.loadTemplateAndRoles.bind(this);
}
async componentDidMount() {
await this.loadTemplate();
await this.loadTemplateAndRoles();
}
async componentDidUpdate(prevProps) {
@ -33,6 +36,31 @@ class Template extends Component {
}
}
async loadTemplateAndRoles() {
const { match, setBreadcrumb } = this.props;
const id = parseInt(match.params.id, 10);
this.setState({ contentError: null, hasContentLoading: true });
try {
const [{ data }, notifAdminRes] = await Promise.all([
JobTemplatesAPI.readDetail(id),
OrganizationsAPI.read({
page_size: 1,
role_level: 'notification_admin_role',
}),
]);
setBreadcrumb(data);
this.setState({
template: data,
isNotifAdmin: notifAdminRes.data.results.length > 0,
});
} catch (err) {
this.setState({ contentError: err });
} finally {
this.setState({ hasContentLoading: false });
}
}
async loadTemplate() {
const { setBreadcrumb, match } = this.props;
const { id } = match.params;
@ -50,18 +78,47 @@ class Template extends Component {
}
render() {
const { history, i18n, location, match } = this.props;
const { contentError, hasContentLoading, template } = this.state;
const { history, i18n, location, match, me } = this.props;
const {
contentError,
hasContentLoading,
isNotifAdmin,
template,
} = this.state;
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin;
const tabsArray = [
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
{ name: i18n._(t`Access`), link: '/home', id: 1 },
{ name: i18n._(t`Notifications`), link: '/home', id: 2 },
{ name: i18n._(t`Schedules`), link: '/home', id: 3 },
{ name: i18n._(t`Completed Jobs`), link: '/home', id: 4 },
{ name: i18n._(t`Survey`), link: '/home', id: 5 },
];
if (canSeeNotificationsTab) {
tabsArray.push({
name: i18n._(t`Notifications`),
link: `${match.url}/notifications`,
id: 2,
});
}
tabsArray.push(
{
name: i18n._(t`Schedules`),
link: '/home',
id: canSeeNotificationsTab ? 3 : 2,
},
{
name: i18n._(t`Completed Jobs`),
link: '/home',
id: canSeeNotificationsTab ? 4 : 3,
},
{
name: i18n._(t`Survey`),
link: '/home',
id: canSeeNotificationsTab ? 5 : 4,
}
);
let cardHeader = hasContentLoading ? null : (
<CardHeader style={{ padding: 0 }}>
<RoutedTabs history={history} tabsArray={tabsArray} />
@ -89,6 +146,7 @@ class Template extends Component {
</PageSection>
);
}
return (
<PageSection>
<Card className="awx-c-card">
@ -99,7 +157,7 @@ class Template extends Component {
to="/templates/:templateType/:id/details"
exact
/>
{template && [
{template && (
<Route
key="details"
path="/templates/:templateType/:id/details"
@ -110,30 +168,44 @@ class Template extends Component {
template={template}
/>
)}
/>,
/>
)}
{template && (
<Route
key="edit"
path="/templates/:templateType/:id/edit"
render={() => <JobTemplateEdit template={template} />}
/>,
/>
)}
{canSeeNotificationsTab && (
<Route
key="not-found"
path="*"
render={() =>
!hasContentLoading && (
<ContentError isNotFound>
{match.params.id && (
<Link
to={`/templates/${match.params.templateType}/${match.params.id}/details`}
>
{i18n._(`View Template Details`)}
</Link>
)}
</ContentError>
)
}
/>,
]}
path="/templates/:templateType/:id/notifications"
render={() => (
<NotificationList
id={Number(match.params.id)}
canToggleNotifications={isNotifAdmin}
apiModel={JobTemplatesAPI}
/>
)}
/>
)}
<Route
key="not-found"
path="*"
render={() =>
!hasContentLoading && (
<ContentError isNotFound>
{match.params.id && (
<Link
to={`/templates/${match.params.templateType}/${match.params.id}/details`}
>
{i18n._(`View Template Details`)}
</Link>
)}
</ContentError>
)
}
/>
</Switch>
</Card>
</PageSection>

View File

@ -1,20 +1,52 @@
import React from 'react';
import { JobTemplatesAPI, OrganizationsAPI } from '@api';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import Template, { _Template } from './Template';
import mockJobTemplateData from './shared/data.job_template.json';
jest.mock('@api');
JobTemplatesAPI.readDetail.mockResolvedValue({
data: mockJobTemplateData,
});
OrganizationsAPI.read.mockResolvedValue({
data: {
count: 1,
next: null,
previous: null,
results: [
{
id: 1,
},
],
},
});
const mockMe = {
is_super_user: true,
is_system_auditor: false,
};
describe('<Template />', () => {
test('initially renders succesfully', () => {
mountWithContexts(<Template />);
mountWithContexts(<Template setBreadcrumb={() => {}} me={mockMe} />);
});
test('When component mounts API is called and the response is put in state', async done => {
const loadTemplate = jest.spyOn(_Template.prototype, 'loadTemplate');
const wrapper = mountWithContexts(<Template />);
const loadTemplateAndRoles = jest.spyOn(
_Template.prototype,
'loadTemplateAndRoles'
);
const wrapper = mountWithContexts(
<Template setBreadcrumb={() => {}} me={mockMe} />
);
await waitForElement(
wrapper,
'Template',
el => el.state('hasContentLoading') === true
);
expect(loadTemplate).toHaveBeenCalled();
expect(loadTemplateAndRoles).toHaveBeenCalled();
await waitForElement(
wrapper,
'Template',
@ -22,4 +54,37 @@ describe('<Template />', () => {
);
done();
});
test('notifications tab shown for admins', async done => {
const wrapper = mountWithContexts(
<Template setBreadcrumb={() => {}} me={mockMe} />
);
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
el => el.length === 6
);
expect(tabs.at(2).text()).toEqual('Notifications');
done();
});
test('notifications tab hidden with reduced permissions', async done => {
OrganizationsAPI.read.mockResolvedValue({
data: {
count: 0,
next: null,
previous: null,
results: [],
},
});
const wrapper = mountWithContexts(
<Template setBreadcrumb={() => {}} me={mockMe} />
);
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
el => el.length === 5
);
tabs.forEach(tab => expect(tab.text()).not.toEqual('Notifications'));
done();
});
});

View File

@ -0,0 +1,162 @@
{
"id": 7,
"type": "job_template",
"url": "/api/v2/job_templates/7/",
"related": {
"named_url": "/api/v2/job_templates/Mike's JT/",
"created_by": "/api/v2/users/1/",
"modified_by": "/api/v2/users/1/",
"labels": "/api/v2/job_templates/7/labels/",
"inventory": "/api/v2/inventories/1/",
"project": "/api/v2/projects/6/",
"extra_credentials": "/api/v2/job_templates/7/extra_credentials/",
"credentials": "/api/v2/job_templates/7/credentials/",
"last_job": "/api/v2/jobs/12/",
"jobs": "/api/v2/job_templates/7/jobs/",
"schedules": "/api/v2/job_templates/7/schedules/",
"activity_stream": "/api/v2/job_templates/7/activity_stream/",
"launch": "/api/v2/job_templates/7/launch/",
"notification_templates_started": "/api/v2/job_templates/7/notification_templates_started/",
"notification_templates_success": "/api/v2/job_templates/7/notification_templates_success/",
"notification_templates_error": "/api/v2/job_templates/7/notification_templates_error/",
"access_list": "/api/v2/job_templates/7/access_list/",
"survey_spec": "/api/v2/job_templates/7/survey_spec/",
"object_roles": "/api/v2/job_templates/7/object_roles/",
"instance_groups": "/api/v2/job_templates/7/instance_groups/",
"slice_workflow_jobs": "/api/v2/job_templates/7/slice_workflow_jobs/",
"copy": "/api/v2/job_templates/7/copy/"
},
"summary_fields": {
"inventory": {
"id": 1,
"name": "Mike's Inventory",
"description": "",
"has_active_failures": false,
"total_hosts": 1,
"hosts_with_active_failures": 0,
"total_groups": 0,
"groups_with_active_failures": 0,
"has_inventory_sources": false,
"total_inventory_sources": 0,
"inventory_sources_with_failures": 0,
"organization_id": 1,
"kind": ""
},
"project": {
"id": 6,
"name": "Mike's Project",
"description": "",
"status": "successful",
"scm_type": "git"
},
"last_job": {
"id": 12,
"name": "Mike's JT",
"description": "",
"finished": "2019-10-01T14:34:35.142483Z",
"status": "successful",
"failed": false
},
"last_update": {
"id": 12,
"name": "Mike's JT",
"description": "",
"status": "successful",
"failed": false
},
"created_by": {
"id": 1,
"username": "admin",
"first_name": "",
"last_name": ""
},
"modified_by": {
"id": 1,
"username": "admin",
"first_name": "",
"last_name": ""
},
"object_roles": {
"admin_role": {
"description": "Can manage all aspects of the job template",
"name": "Admin",
"id": 24
},
"execute_role": {
"description": "May run the job template",
"name": "Execute",
"id": 25
},
"read_role": {
"description": "May view settings for the job template",
"name": "Read",
"id": 26
}
},
"user_capabilities": {
"edit": true,
"delete": true,
"start": true,
"schedule": true,
"copy": true
},
"labels": {
"count": 0,
"results": []
},
"survey": {
"title": "",
"description": ""
},
"recent_jobs": [
{
"id": 12,
"status": "successful",
"finished": "2019-10-01T14:34:35.142483Z",
"type": "job"
}
],
"extra_credentials": [],
"credentials": []
},
"created": "2019-09-30T16:18:34.564820Z",
"modified": "2019-10-01T14:47:31.818431Z",
"name": "Mike's JT",
"description": "",
"job_type": "run",
"inventory": 1,
"project": 6,
"playbook": "ping.yml",
"scm_branch": "",
"forks": 0,
"limit": "",
"verbosity": 0,
"extra_vars": "",
"job_tags": "",
"force_handlers": false,
"skip_tags": "",
"start_at_task": "",
"timeout": 0,
"use_fact_cache": false,
"last_job_run": "2019-10-01T14:34:35.142483Z",
"last_job_failed": false,
"next_job_run": null,
"status": "successful",
"host_config_key": "",
"ask_scm_branch_on_launch": false,
"ask_diff_mode_on_launch": false,
"ask_variables_on_launch": false,
"ask_limit_on_launch": false,
"ask_tags_on_launch": false,
"ask_skip_tags_on_launch": false,
"ask_job_type_on_launch": false,
"ask_verbosity_on_launch": false,
"ask_inventory_on_launch": false,
"ask_credential_on_launch": false,
"survey_enabled": true,
"become_enabled": false,
"diff_mode": false,
"allow_simultaneous": false,
"custom_virtualenv": null,
"job_slice_count": 1
}