diff --git a/awx/ui_next/src/api/mixins/Schedules.mixin.js b/awx/ui_next/src/api/mixins/Schedules.mixin.js
new file mode 100644
index 0000000000..4ea44f418e
--- /dev/null
+++ b/awx/ui_next/src/api/mixins/Schedules.mixin.js
@@ -0,0 +1,12 @@
+const SchedulesMixin = parent =>
+ class extends parent {
+ readSchedules(id, params) {
+ return this.http.get(`${this.baseUrl}${id}/schedules/`, { params });
+ }
+
+ readScheduleOptions(id) {
+ return this.http.options(`${this.baseUrl}${id}/schedules/`);
+ }
+ };
+
+export default SchedulesMixin;
diff --git a/awx/ui_next/src/api/models/JobTemplates.js b/awx/ui_next/src/api/models/JobTemplates.js
index ef615a65f6..9e2e4ee851 100644
--- a/awx/ui_next/src/api/models/JobTemplates.js
+++ b/awx/ui_next/src/api/models/JobTemplates.js
@@ -1,8 +1,11 @@
import Base from '../Base';
import NotificationsMixin from '../mixins/Notifications.mixin';
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
+import SchedulesMixin from '../mixins/Schedules.mixin';
-class JobTemplates extends InstanceGroupsMixin(NotificationsMixin(Base)) {
+class JobTemplates extends SchedulesMixin(
+ InstanceGroupsMixin(NotificationsMixin(Base))
+) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/job_templates/';
@@ -61,10 +64,6 @@ class JobTemplates extends InstanceGroupsMixin(NotificationsMixin(Base)) {
params,
});
}
-
- readScheduleList(id, params) {
- return this.http.get(`${this.baseUrl}${id}/schedules/`, { params });
- }
}
export default JobTemplates;
diff --git a/awx/ui_next/src/api/models/Projects.js b/awx/ui_next/src/api/models/Projects.js
index 3a4049f9f8..3761c61961 100644
--- a/awx/ui_next/src/api/models/Projects.js
+++ b/awx/ui_next/src/api/models/Projects.js
@@ -1,8 +1,11 @@
import Base from '../Base';
import NotificationsMixin from '../mixins/Notifications.mixin';
import LaunchUpdateMixin from '../mixins/LaunchUpdate.mixin';
+import SchedulesMixin from '../mixins/Schedules.mixin';
-class Projects extends LaunchUpdateMixin(NotificationsMixin(Base)) {
+class Projects extends SchedulesMixin(
+ LaunchUpdateMixin(NotificationsMixin(Base))
+) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/projects/';
@@ -21,10 +24,6 @@ class Projects extends LaunchUpdateMixin(NotificationsMixin(Base)) {
return this.http.get(`${this.baseUrl}${id}/playbooks/`);
}
- readScheduleList(id, params) {
- return this.http.get(`${this.baseUrl}${id}/schedules/`, { params });
- }
-
readSync(id) {
return this.http.get(`${this.baseUrl}${id}/update/`);
}
diff --git a/awx/ui_next/src/api/models/WorkflowJobTemplates.js b/awx/ui_next/src/api/models/WorkflowJobTemplates.js
index a71fe68cbc..0725a1a3e4 100644
--- a/awx/ui_next/src/api/models/WorkflowJobTemplates.js
+++ b/awx/ui_next/src/api/models/WorkflowJobTemplates.js
@@ -1,6 +1,7 @@
import Base from '../Base';
+import SchedulesMixin from '../mixins/Schedules.mixin';
-class WorkflowJobTemplates extends Base {
+class WorkflowJobTemplates extends SchedulesMixin(Base) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/workflow_job_templates/';
@@ -45,12 +46,6 @@ class WorkflowJobTemplates extends Base {
params,
});
}
-
- readScheduleList(id, params) {
- return this.http.get(`${this.baseUrl}${id}/schedules/`, {
- params,
- });
- }
}
export default WorkflowJobTemplates;
diff --git a/awx/ui_next/src/components/ScheduleList/ScheduleList.jsx b/awx/ui_next/src/components/ScheduleList/ScheduleList.jsx
index e24e46e0b6..999f28a6b9 100644
--- a/awx/ui_next/src/components/ScheduleList/ScheduleList.jsx
+++ b/awx/ui_next/src/components/ScheduleList/ScheduleList.jsx
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
+import { bool, func } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { SchedulesAPI } from '@api';
@@ -7,6 +8,7 @@ import AlertModal from '@components/AlertModal';
import ErrorDetail from '@components/ErrorDetail';
import DataListToolbar from '@components/DataListToolbar';
import PaginatedDataList, {
+ ToolbarAddButton,
ToolbarDeleteButton,
} from '@components/PaginatedDataList';
import useRequest, { useDeleteItems } from '@util/useRequest';
@@ -19,28 +21,40 @@ const QS_CONFIG = getQSConfig('schedule', {
order_by: 'unified_job_template__polymorphic_ctype__model',
});
-function ScheduleList({ i18n, loadSchedules }) {
+function ScheduleList({
+ i18n,
+ loadSchedules,
+ loadScheduleOptions,
+ hideAddButton,
+}) {
const [selected, setSelected] = useState([]);
const location = useLocation();
const {
- result: { schedules, itemCount },
+ result: { schedules, itemCount, actions },
error: contentError,
isLoading,
request: fetchSchedules,
} = useRequest(
useCallback(async () => {
const params = parseQueryString(QS_CONFIG, location.search);
- const response = loadSchedules(params);
- const {
- data: { count, results },
- } = await response;
- return { itemCount: count, schedules: results };
- }, [location, loadSchedules]),
+ const [
+ {
+ data: { count, results },
+ },
+ scheduleActions,
+ ] = await Promise.all([loadSchedules(params), loadScheduleOptions()]);
+ return {
+ schedules: results,
+ itemCount: count,
+ actions: scheduleActions.data.actions,
+ };
+ }, [location, loadSchedules, loadScheduleOptions]),
{
schedules: [],
itemCount: 0,
+ actions: {},
}
);
@@ -84,6 +98,11 @@ function ScheduleList({ i18n, loadSchedules }) {
setSelected([]);
};
+ const canAdd =
+ actions &&
+ Object.prototype.hasOwnProperty.call(actions, 'POST') &&
+ !hideAddButton;
+
return (
<>
,
+ ]
+ : []),
SchedulesAPI.read(params);
+const loadScheduleOptions = () => SchedulesAPI.readOptions();
describe('ScheduleList', () => {
let wrapper;
@@ -21,11 +33,12 @@ describe('ScheduleList', () => {
describe('read call successful', () => {
beforeAll(async () => {
- SchedulesAPI.read.mockResolvedValue({ data: mockSchedules });
- const loadSchedules = params => SchedulesAPI.read(params);
await act(async () => {
wrapper = mountWithContexts(
-
+
);
});
wrapper.update();
@@ -40,6 +53,10 @@ describe('ScheduleList', () => {
expect(wrapper.find('ScheduleListItem').length).toBe(5);
});
+ test('should show add button', () => {
+ expect(wrapper.find('ToolbarAddButton').length).toBe(1);
+ });
+
test('should check and uncheck the row item', async () => {
expect(
wrapper.find('DataListCheck[id="select-schedule-1"]').props().checked
@@ -153,11 +170,32 @@ describe('ScheduleList', () => {
});
});
+ describe('hidden add button', () => {
+ test('should hide add button when flag is passed', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
+ expect(wrapper.find('ToolbarAddButton').length).toBe(0);
+ });
+ });
+
describe('read call unsuccessful', () => {
test('should show content error when read call unsuccessful', async () => {
SchedulesAPI.read.mockRejectedValue(new Error());
await act(async () => {
- wrapper = mountWithContexts();
+ wrapper = mountWithContexts(
+
+ );
});
wrapper.update();
expect(wrapper.find('ContentError').length).toBe(1);
diff --git a/awx/ui_next/src/screens/Project/Project.jsx b/awx/ui_next/src/screens/Project/Project.jsx
index 9e401fde76..de6b609bdb 100644
--- a/awx/ui_next/src/screens/Project/Project.jsx
+++ b/awx/ui_next/src/screens/Project/Project.jsx
@@ -31,6 +31,7 @@ class Project extends Component {
this.loadProject = this.loadProject.bind(this);
this.loadProjectAndRoles = this.loadProjectAndRoles.bind(this);
this.loadSchedules = this.loadSchedules.bind(this);
+ this.loadScheduleOptions = this.loadScheduleOptions.bind(this);
}
async componentDidMount() {
@@ -104,9 +105,14 @@ class Project extends Component {
}
}
+ loadScheduleOptions() {
+ const { project } = this.state;
+ return ProjectsAPI.readScheduleOptions(project.id);
+ }
+
loadSchedules(params) {
const { project } = this.state;
- return ProjectsAPI.readScheduleList(project.id, params);
+ return ProjectsAPI.readSchedules(project.id, params);
}
render() {
@@ -241,7 +247,10 @@ class Project extends Component {
(
-
+
)}
/>
)}
diff --git a/awx/ui_next/src/screens/Schedule/Schedules.jsx b/awx/ui_next/src/screens/Schedule/Schedules.jsx
index 0f7c9b0ee0..514f4b4392 100644
--- a/awx/ui_next/src/screens/Schedule/Schedules.jsx
+++ b/awx/ui_next/src/screens/Schedule/Schedules.jsx
@@ -9,6 +9,10 @@ import { SchedulesAPI } from '@api';
import { PageSection, Card } from '@patternfly/react-core';
function Schedules({ i18n }) {
+ const loadScheduleOptions = () => {
+ return SchedulesAPI.readOptions();
+ };
+
const loadSchedules = params => {
return SchedulesAPI.read(params);
};
@@ -24,7 +28,11 @@ function Schedules({ i18n }) {
-
+
diff --git a/awx/ui_next/src/screens/Template/Template.jsx b/awx/ui_next/src/screens/Template/Template.jsx
index 61f63e70e4..a600563a4a 100644
--- a/awx/ui_next/src/screens/Template/Template.jsx
+++ b/awx/ui_next/src/screens/Template/Template.jsx
@@ -29,6 +29,7 @@ class Template extends Component {
this.loadTemplate = this.loadTemplate.bind(this);
this.loadTemplateAndRoles = this.loadTemplateAndRoles.bind(this);
this.loadSchedules = this.loadSchedules.bind(this);
+ this.loadScheduleOptions = this.loadScheduleOptions.bind(this);
}
async componentDidMount() {
@@ -83,9 +84,14 @@ class Template extends Component {
}
}
+ loadScheduleOptions() {
+ const { template } = this.state;
+ return JobTemplatesAPI.readScheduleOptions(template.id);
+ }
+
loadSchedules(params) {
const { template } = this.state;
- return JobTemplatesAPI.readScheduleList(template.id, params);
+ return JobTemplatesAPI.readSchedules(template.id, params);
}
render() {
@@ -111,6 +117,13 @@ class Template extends Component {
});
}
+ if (template) {
+ tabsArray.push({
+ name: i18n._(t`Schedules`),
+ link: `${match.url}/schedules`,
+ });
+ }
+
tabsArray.push(
{
name: i18n._(t`Completed Jobs`),
@@ -122,13 +135,6 @@ class Template extends Component {
}
);
- if (template) {
- tabsArray.push({
- name: i18n._(t`Schedules`),
- link: `${match.url}/schedules`,
- });
- }
-
tabsArray.forEach((tab, n) => {
tab.id = n;
});
@@ -225,7 +231,10 @@ class Template extends Component {
(
-
+
)}
/>
)}
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx
index 6c63e16ab0..1a71bb644f 100644
--- a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx
@@ -29,6 +29,7 @@ class WorkflowJobTemplate extends Component {
};
this.loadTemplate = this.loadTemplate.bind(this);
this.loadSchedules = this.loadSchedules.bind(this);
+ this.loadScheduleOptions = this.loadScheduleOptions.bind(this);
}
async componentDidMount() {
@@ -76,9 +77,14 @@ class WorkflowJobTemplate extends Component {
}
}
+ loadScheduleOptions() {
+ const { template } = this.state;
+ return WorkflowJobTemplatesAPI.readScheduleOptions(template.id);
+ }
+
loadSchedules(params) {
const { template } = this.state;
- return WorkflowJobTemplatesAPI.readScheduleList(template.id, params);
+ return WorkflowJobTemplatesAPI.readSchedules(template.id, params);
}
render() {
@@ -199,7 +205,10 @@ class WorkflowJobTemplate extends Component {
(
-
+
)}
/>
)}