Merge pull request #4896 from mabashian/ui_next-notifs

Refactor notifications list and add it to JT details

Reviewed-by: Michael Abashian
             https://github.com/mabashian
This commit is contained in:
softwarefactory-project-zuul[bot]
2019-10-04 14:50:35 +00:00
committed by GitHub
17 changed files with 829 additions and 259 deletions

View File

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

View File

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

View File

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

View File

@@ -106,9 +106,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"$$typeof": Symbol(react.forward_ref), "$$typeof": Symbol(react.forward_ref),
"attrs": Array [], "attrs": Array [],
"componentStyle": ComponentStyle { "componentStyle": ComponentStyle {
"componentId": "NotificationListItem__DataListCell-j7c411-0", "componentId": "NotificationListItem__DataListCell-w674ng-0",
"isStatic": false, "isStatic": false,
"lastClassName": "hoXOpW", "lastClassName": "dXsFLF",
"rules": Array [ "rules": Array [
"display:flex;justify-content:", "display:flex;justify-content:",
[Function], [Function],
@@ -122,7 +122,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"displayName": "NotificationListItem__DataListCell", "displayName": "NotificationListItem__DataListCell",
"foldedComponentIds": Array [], "foldedComponentIds": Array [],
"render": [Function], "render": [Function],
"styledComponentId": "NotificationListItem__DataListCell-j7c411-0", "styledComponentId": "NotificationListItem__DataListCell-w674ng-0",
"target": [Function], "target": [Function],
"toString": [Function], "toString": [Function],
"warnTooManyClasses": [Function], "warnTooManyClasses": [Function],
@@ -132,10 +132,10 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
forwardedRef={null} forwardedRef={null}
> >
<DataListCell <DataListCell
className="NotificationListItem__DataListCell-j7c411-0 kIdLtz" className="NotificationListItem__DataListCell-w674ng-0 faYgxF"
> >
<div <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) <Styled(Link)
to={ to={
@@ -209,9 +209,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"$$typeof": Symbol(react.forward_ref), "$$typeof": Symbol(react.forward_ref),
"attrs": Array [], "attrs": Array [],
"componentStyle": ComponentStyle { "componentStyle": ComponentStyle {
"componentId": "NotificationListItem__DataListCell-j7c411-0", "componentId": "NotificationListItem__DataListCell-w674ng-0",
"isStatic": false, "isStatic": false,
"lastClassName": "hoXOpW", "lastClassName": "dXsFLF",
"rules": Array [ "rules": Array [
"display:flex;justify-content:", "display:flex;justify-content:",
[Function], [Function],
@@ -225,7 +225,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"displayName": "NotificationListItem__DataListCell", "displayName": "NotificationListItem__DataListCell",
"foldedComponentIds": Array [], "foldedComponentIds": Array [],
"render": [Function], "render": [Function],
"styledComponentId": "NotificationListItem__DataListCell-j7c411-0", "styledComponentId": "NotificationListItem__DataListCell-w674ng-0",
"target": [Function], "target": [Function],
"toString": [Function], "toString": [Function],
"warnTooManyClasses": [Function], "warnTooManyClasses": [Function],
@@ -235,10 +235,10 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
forwardedRef={null} forwardedRef={null}
> >
<DataListCell <DataListCell
className="NotificationListItem__DataListCell-j7c411-0 kIdLtz" className="NotificationListItem__DataListCell-w674ng-0 faYgxF"
> >
<div <div
className="pf-c-data-list__cell NotificationListItem__DataListCell-j7c411-0 kIdLtz" className="pf-c-data-list__cell NotificationListItem__DataListCell-w674ng-0 faYgxF"
> >
Slack Slack
</div> </div>
@@ -255,9 +255,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"$$typeof": Symbol(react.forward_ref), "$$typeof": Symbol(react.forward_ref),
"attrs": Array [], "attrs": Array [],
"componentStyle": ComponentStyle { "componentStyle": ComponentStyle {
"componentId": "NotificationListItem__DataListCell-j7c411-0", "componentId": "NotificationListItem__DataListCell-w674ng-0",
"isStatic": false, "isStatic": false,
"lastClassName": "hoXOpW", "lastClassName": "dXsFLF",
"rules": Array [ "rules": Array [
"display:flex;justify-content:", "display:flex;justify-content:",
[Function], [Function],
@@ -271,7 +271,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"displayName": "NotificationListItem__DataListCell", "displayName": "NotificationListItem__DataListCell",
"foldedComponentIds": Array [], "foldedComponentIds": Array [],
"render": [Function], "render": [Function],
"styledComponentId": "NotificationListItem__DataListCell-j7c411-0", "styledComponentId": "NotificationListItem__DataListCell-w674ng-0",
"target": [Function], "target": [Function],
"toString": [Function], "toString": [Function],
"warnTooManyClasses": [Function], "warnTooManyClasses": [Function],
@@ -282,11 +282,11 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
righthalf="true" righthalf="true"
> >
<DataListCell <DataListCell
className="NotificationListItem__DataListCell-j7c411-0 hoXOpW" className="NotificationListItem__DataListCell-w674ng-0 dXsFLF"
righthalf="true" righthalf="true"
> >
<div <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" righthalf="true"
> >
<NotificationListItem__Switch <NotificationListItem__Switch
@@ -305,9 +305,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"$$typeof": Symbol(react.forward_ref), "$$typeof": Symbol(react.forward_ref),
"attrs": Array [], "attrs": Array [],
"componentStyle": ComponentStyle { "componentStyle": ComponentStyle {
"componentId": "NotificationListItem__Switch-j7c411-1", "componentId": "NotificationListItem__Switch-w674ng-1",
"isStatic": true, "isStatic": true,
"lastClassName": "ceuHGn", "lastClassName": "hbNxaH",
"rules": Array [ "rules": Array [
"display:flex;flex-wrap:no-wrap;", "display:flex;flex-wrap:no-wrap;",
], ],
@@ -315,7 +315,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"displayName": "NotificationListItem__Switch", "displayName": "NotificationListItem__Switch",
"foldedComponentIds": Array [], "foldedComponentIds": Array [],
"render": [Function], "render": [Function],
"styledComponentId": "NotificationListItem__Switch-j7c411-1", "styledComponentId": "NotificationListItem__Switch-w674ng-1",
"target": [Function], "target": [Function],
"toString": [Function], "toString": [Function],
"warnTooManyClasses": [Function], "warnTooManyClasses": [Function],
@@ -332,7 +332,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
> >
<Component <Component
aria-label="Toggle notification start" aria-label="Toggle notification start"
className="NotificationListItem__Switch-j7c411-1 ceuHGn" className="NotificationListItem__Switch-w674ng-1 hbNxaH"
id="notification-9000-started-toggle" id="notification-9000-started-toggle"
isChecked={false} isChecked={false}
isDisabled={false} isDisabled={false}
@@ -345,7 +345,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
componentProps={ componentProps={
Object { Object {
"aria-label": "Toggle notification start", "aria-label": "Toggle notification start",
"className": "NotificationListItem__Switch-j7c411-1 ceuHGn", "className": "NotificationListItem__Switch-w674ng-1 hbNxaH",
"id": "notification-9000-started-toggle", "id": "notification-9000-started-toggle",
"isChecked": false, "isChecked": false,
"isDisabled": false, "isDisabled": false,
@@ -358,7 +358,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
> >
<Switch <Switch
aria-label="Toggle notification start" aria-label="Toggle notification start"
className="NotificationListItem__Switch-j7c411-1 ceuHGn" className="NotificationListItem__Switch-w674ng-1 hbNxaH"
id="notification-9000-started-toggle" id="notification-9000-started-toggle"
isChecked={false} isChecked={false}
isDisabled={false} isDisabled={false}
@@ -373,7 +373,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
} }
> >
<label <label
className="pf-c-switch NotificationListItem__Switch-j7c411-1 ceuHGn" className="pf-c-switch NotificationListItem__Switch-w674ng-1 hbNxaH"
htmlFor="notification-9000-started-toggle" htmlFor="notification-9000-started-toggle"
> >
<input <input
@@ -425,9 +425,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"$$typeof": Symbol(react.forward_ref), "$$typeof": Symbol(react.forward_ref),
"attrs": Array [], "attrs": Array [],
"componentStyle": ComponentStyle { "componentStyle": ComponentStyle {
"componentId": "NotificationListItem__Switch-j7c411-1", "componentId": "NotificationListItem__Switch-w674ng-1",
"isStatic": true, "isStatic": true,
"lastClassName": "ceuHGn", "lastClassName": "hbNxaH",
"rules": Array [ "rules": Array [
"display:flex;flex-wrap:no-wrap;", "display:flex;flex-wrap:no-wrap;",
], ],
@@ -435,7 +435,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"displayName": "NotificationListItem__Switch", "displayName": "NotificationListItem__Switch",
"foldedComponentIds": Array [], "foldedComponentIds": Array [],
"render": [Function], "render": [Function],
"styledComponentId": "NotificationListItem__Switch-j7c411-1", "styledComponentId": "NotificationListItem__Switch-w674ng-1",
"target": [Function], "target": [Function],
"toString": [Function], "toString": [Function],
"warnTooManyClasses": [Function], "warnTooManyClasses": [Function],
@@ -452,7 +452,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
> >
<Component <Component
aria-label="Toggle notification success" aria-label="Toggle notification success"
className="NotificationListItem__Switch-j7c411-1 ceuHGn" className="NotificationListItem__Switch-w674ng-1 hbNxaH"
id="notification-9000-success-toggle" id="notification-9000-success-toggle"
isChecked={false} isChecked={false}
isDisabled={false} isDisabled={false}
@@ -465,7 +465,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
componentProps={ componentProps={
Object { Object {
"aria-label": "Toggle notification success", "aria-label": "Toggle notification success",
"className": "NotificationListItem__Switch-j7c411-1 ceuHGn", "className": "NotificationListItem__Switch-w674ng-1 hbNxaH",
"id": "notification-9000-success-toggle", "id": "notification-9000-success-toggle",
"isChecked": false, "isChecked": false,
"isDisabled": false, "isDisabled": false,
@@ -478,7 +478,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
> >
<Switch <Switch
aria-label="Toggle notification success" aria-label="Toggle notification success"
className="NotificationListItem__Switch-j7c411-1 ceuHGn" className="NotificationListItem__Switch-w674ng-1 hbNxaH"
id="notification-9000-success-toggle" id="notification-9000-success-toggle"
isChecked={false} isChecked={false}
isDisabled={false} isDisabled={false}
@@ -493,7 +493,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
} }
> >
<label <label
className="pf-c-switch NotificationListItem__Switch-j7c411-1 ceuHGn" className="pf-c-switch NotificationListItem__Switch-w674ng-1 hbNxaH"
htmlFor="notification-9000-success-toggle" htmlFor="notification-9000-success-toggle"
> >
<input <input
@@ -545,9 +545,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"$$typeof": Symbol(react.forward_ref), "$$typeof": Symbol(react.forward_ref),
"attrs": Array [], "attrs": Array [],
"componentStyle": ComponentStyle { "componentStyle": ComponentStyle {
"componentId": "NotificationListItem__Switch-j7c411-1", "componentId": "NotificationListItem__Switch-w674ng-1",
"isStatic": true, "isStatic": true,
"lastClassName": "ceuHGn", "lastClassName": "hbNxaH",
"rules": Array [ "rules": Array [
"display:flex;flex-wrap:no-wrap;", "display:flex;flex-wrap:no-wrap;",
], ],
@@ -555,7 +555,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"displayName": "NotificationListItem__Switch", "displayName": "NotificationListItem__Switch",
"foldedComponentIds": Array [], "foldedComponentIds": Array [],
"render": [Function], "render": [Function],
"styledComponentId": "NotificationListItem__Switch-j7c411-1", "styledComponentId": "NotificationListItem__Switch-w674ng-1",
"target": [Function], "target": [Function],
"toString": [Function], "toString": [Function],
"warnTooManyClasses": [Function], "warnTooManyClasses": [Function],
@@ -572,7 +572,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
> >
<Component <Component
aria-label="Toggle notification failure" aria-label="Toggle notification failure"
className="NotificationListItem__Switch-j7c411-1 ceuHGn" className="NotificationListItem__Switch-w674ng-1 hbNxaH"
id="notification-9000-error-toggle" id="notification-9000-error-toggle"
isChecked={false} isChecked={false}
isDisabled={false} isDisabled={false}
@@ -585,7 +585,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
componentProps={ componentProps={
Object { Object {
"aria-label": "Toggle notification failure", "aria-label": "Toggle notification failure",
"className": "NotificationListItem__Switch-j7c411-1 ceuHGn", "className": "NotificationListItem__Switch-w674ng-1 hbNxaH",
"id": "notification-9000-error-toggle", "id": "notification-9000-error-toggle",
"isChecked": false, "isChecked": false,
"isDisabled": false, "isDisabled": false,
@@ -598,7 +598,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
> >
<Switch <Switch
aria-label="Toggle notification failure" aria-label="Toggle notification failure"
className="NotificationListItem__Switch-j7c411-1 ceuHGn" className="NotificationListItem__Switch-w674ng-1 hbNxaH"
id="notification-9000-error-toggle" id="notification-9000-error-toggle"
isChecked={false} isChecked={false}
isDisabled={false} isDisabled={false}
@@ -613,7 +613,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
} }
> >
<label <label
className="pf-c-switch NotificationListItem__Switch-j7c411-1 ceuHGn" className="pf-c-switch NotificationListItem__Switch-w674ng-1 hbNxaH"
htmlFor="notification-9000-error-toggle" htmlFor="notification-9000-error-toggle"
> >
<input <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 CardCloseButton from '@components/CardCloseButton';
import RoutedTabs from '@components/RoutedTabs'; import RoutedTabs from '@components/RoutedTabs';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import NotificationList from '@components/NotificationList/NotificationList';
import { OrganizationAccess } from './OrganizationAccess'; import { OrganizationAccess } from './OrganizationAccess';
import OrganizationDetail from './OrganizationDetail'; import OrganizationDetail from './OrganizationDetail';
import OrganizationEdit from './OrganizationEdit'; import OrganizationEdit from './OrganizationEdit';
import OrganizationNotifications from './OrganizationNotifications';
import OrganizationTeams from './OrganizationTeams'; import OrganizationTeams from './OrganizationTeams';
import { OrganizationsAPI } from '@api'; import { OrganizationsAPI } from '@api';
@@ -228,9 +228,10 @@ class Organization extends Component {
<Route <Route
path="/organizations/:id/notifications" path="/organizations/:id/notifications"
render={() => ( render={() => (
<OrganizationNotifications <NotificationList
id={Number(match.params.id)} id={Number(match.params.id)}
canToggleNotifications={canToggleNotifications} 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 { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
import CardCloseButton from '@components/CardCloseButton'; import CardCloseButton from '@components/CardCloseButton';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import NotificationList from '@components/NotificationList';
import RoutedTabs from '@components/RoutedTabs'; import RoutedTabs from '@components/RoutedTabs';
import JobTemplateDetail from './JobTemplateDetail'; import JobTemplateDetail from './JobTemplateDetail';
import { JobTemplatesAPI } from '@api'; import { JobTemplatesAPI, OrganizationsAPI } from '@api';
import JobTemplateEdit from './JobTemplateEdit'; import JobTemplateEdit from './JobTemplateEdit';
class Template extends Component { class Template extends Component {
@@ -18,12 +19,14 @@ class Template extends Component {
contentError: null, contentError: null,
hasContentLoading: true, hasContentLoading: true,
template: null, template: null,
isNotifAdmin: false,
}; };
this.loadTemplate = this.loadTemplate.bind(this); this.loadTemplate = this.loadTemplate.bind(this);
this.loadTemplateAndRoles = this.loadTemplateAndRoles.bind(this);
} }
async componentDidMount() { async componentDidMount() {
await this.loadTemplate(); await this.loadTemplateAndRoles();
} }
async componentDidUpdate(prevProps) { 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() { async loadTemplate() {
const { setBreadcrumb, match } = this.props; const { setBreadcrumb, match } = this.props;
const { id } = match.params; const { id } = match.params;
@@ -50,18 +78,47 @@ class Template extends Component {
} }
render() { render() {
const { history, i18n, location, match } = this.props; const { history, i18n, location, match, me } = this.props;
const { contentError, hasContentLoading, template } = this.state; const {
contentError,
hasContentLoading,
isNotifAdmin,
template,
} = this.state;
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin;
const tabsArray = [ const tabsArray = [
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 }, { name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
{ name: i18n._(t`Access`), link: '/home', id: 1 }, { 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 : ( let cardHeader = hasContentLoading ? null : (
<CardHeader style={{ padding: 0 }}> <CardHeader style={{ padding: 0 }}>
<RoutedTabs history={history} tabsArray={tabsArray} /> <RoutedTabs history={history} tabsArray={tabsArray} />
@@ -89,6 +146,7 @@ class Template extends Component {
</PageSection> </PageSection>
); );
} }
return ( return (
<PageSection> <PageSection>
<Card className="awx-c-card"> <Card className="awx-c-card">
@@ -99,7 +157,7 @@ class Template extends Component {
to="/templates/:templateType/:id/details" to="/templates/:templateType/:id/details"
exact exact
/> />
{template && [ {template && (
<Route <Route
key="details" key="details"
path="/templates/:templateType/:id/details" path="/templates/:templateType/:id/details"
@@ -110,30 +168,44 @@ class Template extends Component {
template={template} template={template}
/> />
)} )}
/>, />
)}
{template && (
<Route <Route
key="edit" key="edit"
path="/templates/:templateType/:id/edit" path="/templates/:templateType/:id/edit"
render={() => <JobTemplateEdit template={template} />} render={() => <JobTemplateEdit template={template} />}
/>, />
)}
{canSeeNotificationsTab && (
<Route <Route
key="not-found" path="/templates/:templateType/:id/notifications"
path="*" render={() => (
render={() => <NotificationList
!hasContentLoading && ( id={Number(match.params.id)}
<ContentError isNotFound> canToggleNotifications={isNotifAdmin}
{match.params.id && ( apiModel={JobTemplatesAPI}
<Link />
to={`/templates/${match.params.templateType}/${match.params.id}/details`} )}
> />
{i18n._(`View Template Details`)} )}
</Link> <Route
)} key="not-found"
</ContentError> 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> </Switch>
</Card> </Card>
</PageSection> </PageSection>

View File

@@ -1,20 +1,52 @@
import React from 'react'; import React from 'react';
import { JobTemplatesAPI, OrganizationsAPI } from '@api';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import Template, { _Template } from './Template'; 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 />', () => { describe('<Template />', () => {
test('initially renders succesfully', () => { 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 => { test('When component mounts API is called and the response is put in state', async done => {
const loadTemplate = jest.spyOn(_Template.prototype, 'loadTemplate'); const loadTemplateAndRoles = jest.spyOn(
const wrapper = mountWithContexts(<Template />); _Template.prototype,
'loadTemplateAndRoles'
);
const wrapper = mountWithContexts(
<Template setBreadcrumb={() => {}} me={mockMe} />
);
await waitForElement( await waitForElement(
wrapper, wrapper,
'Template', 'Template',
el => el.state('hasContentLoading') === true el => el.state('hasContentLoading') === true
); );
expect(loadTemplate).toHaveBeenCalled(); expect(loadTemplateAndRoles).toHaveBeenCalled();
await waitForElement( await waitForElement(
wrapper, wrapper,
'Template', 'Template',
@@ -22,4 +54,37 @@ describe('<Template />', () => {
); );
done(); 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

@@ -37,6 +37,9 @@ class Templates extends Component {
[`/templates/${template.type}/${template.id}/edit`]: i18n._( [`/templates/${template.type}/${template.id}/edit`]: i18n._(
t`Edit Details` t`Edit Details`
), ),
[`/templates/${template.type}/${template.id}/notifications`]: i18n._(
t`Notifications`
),
}; };
this.setState({ breadcrumbConfig }); this.setState({ breadcrumbConfig });
}; };

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
}