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 { ( - + )} /> )}