diff --git a/awx/ui_next/src/api/models/JobTemplates.js b/awx/ui_next/src/api/models/JobTemplates.js index 58060371df..ef615a65f6 100644 --- a/awx/ui_next/src/api/models/JobTemplates.js +++ b/awx/ui_next/src/api/models/JobTemplates.js @@ -61,6 +61,10 @@ 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 742150e5aa..3a4049f9f8 100644 --- a/awx/ui_next/src/api/models/Projects.js +++ b/awx/ui_next/src/api/models/Projects.js @@ -21,6 +21,10 @@ 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 45b6f6539f..691c444379 100644 --- a/awx/ui_next/src/api/models/WorkflowJobTemplates.js +++ b/awx/ui_next/src/api/models/WorkflowJobTemplates.js @@ -27,6 +27,10 @@ class WorkflowJobTemplates extends Base { createNode(id, data) { return this.http.post(`${this.baseUrl}${id}/workflow_nodes/`, data); } + + readScheduleList(id, params) { + return this.http.get(`${this.baseUrl}${id}/schedules/`, { params }); + } } export default WorkflowJobTemplates; diff --git a/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.jsx b/awx/ui_next/src/components/ScheduleList/ScheduleList.jsx similarity index 55% rename from awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.jsx rename to awx/ui_next/src/components/ScheduleList/ScheduleList.jsx index 2c1197e7c7..e24e46e0b6 100644 --- a/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.jsx +++ b/awx/ui_next/src/components/ScheduleList/ScheduleList.jsx @@ -3,7 +3,6 @@ import { useLocation } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { SchedulesAPI } from '@api'; -import { Card, PageSection } from '@patternfly/react-core'; import AlertModal from '@components/AlertModal'; import ErrorDetail from '@components/ErrorDetail'; import DataListToolbar from '@components/DataListToolbar'; @@ -12,7 +11,7 @@ import PaginatedDataList, { } from '@components/PaginatedDataList'; import useRequest, { useDeleteItems } from '@util/useRequest'; import { getQSConfig, parseQueryString } from '@util/qs'; -import { ScheduleListItem } from '.'; +import ScheduleListItem from './ScheduleListItem'; const QS_CONFIG = getQSConfig('schedule', { page: 1, @@ -20,7 +19,7 @@ const QS_CONFIG = getQSConfig('schedule', { order_by: 'unified_job_template__polymorphic_ctype__model', }); -function ScheduleList({ i18n }) { +function ScheduleList({ i18n, loadSchedules }) { const [selected, setSelected] = useState([]); const location = useLocation(); @@ -33,14 +32,12 @@ function ScheduleList({ i18n }) { } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); + const response = loadSchedules(params); const { data: { count, results }, - } = await SchedulesAPI.read(params); - return { - itemCount: count, - schedules: results, - }; - }, [location]), + } = await response; + return { itemCount: count, schedules: results }; + }, [location, loadSchedules]), { schedules: [], itemCount: 0, @@ -88,63 +85,61 @@ function ScheduleList({ i18n }) { }; return ( - - - ( - row.id === item.id)} - key={item.id} - onSelect={() => handleSelect(item)} - schedule={item} - /> - )} - toolbarSearchColumns={[ - { - name: i18n._(t`Name`), - key: 'name', - isDefault: true, - }, - ]} - toolbarSortColumns={[ - { - name: i18n._(t`Name`), - key: 'name', - }, - { - name: i18n._(t`Next Run`), - key: 'next_run', - }, - { - name: i18n._(t`Type`), - key: 'unified_job_template__polymorphic_ctype__model', - }, - ]} - renderToolbar={props => ( - , - ]} - /> - )} - /> - + <> + ( + row.id === item.id)} + key={item.id} + onSelect={() => handleSelect(item)} + schedule={item} + /> + )} + toolbarSearchColumns={[ + { + name: i18n._(t`Name`), + key: 'name', + isDefault: true, + }, + ]} + toolbarSortColumns={[ + { + name: i18n._(t`Name`), + key: 'name', + }, + { + name: i18n._(t`Next Run`), + key: 'next_run', + }, + { + name: i18n._(t`Type`), + key: 'unified_job_template__polymorphic_ctype__model', + }, + ]} + renderToolbar={props => ( + , + ]} + /> + )} + /> {deletionError && ( )} - + ); } diff --git a/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.test.jsx b/awx/ui_next/src/components/ScheduleList/ScheduleList.test.jsx similarity index 95% rename from awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.test.jsx rename to awx/ui_next/src/components/ScheduleList/ScheduleList.test.jsx index 706f5f1bca..c257c47c45 100644 --- a/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.test.jsx +++ b/awx/ui_next/src/components/ScheduleList/ScheduleList.test.jsx @@ -3,7 +3,7 @@ import { act } from 'react-dom/test-utils'; import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { SchedulesAPI } from '@api'; import ScheduleList from './ScheduleList'; -import mockSchedules from '../data.schedules.json'; +import mockSchedules from './data.schedules.json'; jest.mock('@api/models/Schedules'); @@ -22,8 +22,11 @@ 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 = mountWithContexts( + + ); }); wrapper.update(); }); diff --git a/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleListItem.jsx b/awx/ui_next/src/components/ScheduleList/ScheduleListItem.jsx similarity index 98% rename from awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleListItem.jsx rename to awx/ui_next/src/components/ScheduleList/ScheduleListItem.jsx index 7d34f8b52d..980cecfac5 100644 --- a/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleListItem.jsx +++ b/awx/ui_next/src/components/ScheduleList/ScheduleListItem.jsx @@ -18,7 +18,7 @@ import { DetailList, Detail } from '@components/DetailList'; import styled from 'styled-components'; import { Schedule } from '@types'; import { formatDateString } from '@util/dates'; -import ScheduleToggle from '../shared/ScheduleToggle'; +import ScheduleToggle from './ScheduleToggle'; const DataListAction = styled(_DataListAction)` align-items: center; diff --git a/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleListItem.test.jsx b/awx/ui_next/src/components/ScheduleList/ScheduleListItem.test.jsx similarity index 100% rename from awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleListItem.test.jsx rename to awx/ui_next/src/components/ScheduleList/ScheduleListItem.test.jsx diff --git a/awx/ui_next/src/screens/Schedule/shared/ScheduleToggle.jsx b/awx/ui_next/src/components/ScheduleList/ScheduleToggle.jsx similarity index 100% rename from awx/ui_next/src/screens/Schedule/shared/ScheduleToggle.jsx rename to awx/ui_next/src/components/ScheduleList/ScheduleToggle.jsx diff --git a/awx/ui_next/src/screens/Schedule/shared/ScheduleToggle.test.jsx b/awx/ui_next/src/components/ScheduleList/ScheduleToggle.test.jsx similarity index 100% rename from awx/ui_next/src/screens/Schedule/shared/ScheduleToggle.test.jsx rename to awx/ui_next/src/components/ScheduleList/ScheduleToggle.test.jsx diff --git a/awx/ui_next/src/screens/Schedule/data.schedules.json b/awx/ui_next/src/components/ScheduleList/data.schedules.json similarity index 100% rename from awx/ui_next/src/screens/Schedule/data.schedules.json rename to awx/ui_next/src/components/ScheduleList/data.schedules.json diff --git a/awx/ui_next/src/components/ScheduleList/index.js b/awx/ui_next/src/components/ScheduleList/index.js new file mode 100644 index 0000000000..35e4093cba --- /dev/null +++ b/awx/ui_next/src/components/ScheduleList/index.js @@ -0,0 +1 @@ +export { default } from './ScheduleList'; diff --git a/awx/ui_next/src/screens/Project/Project.jsx b/awx/ui_next/src/screens/Project/Project.jsx index 65b4560459..9e401fde76 100644 --- a/awx/ui_next/src/screens/Project/Project.jsx +++ b/awx/ui_next/src/screens/Project/Project.jsx @@ -9,10 +9,10 @@ import RoutedTabs from '@components/RoutedTabs'; import ContentError from '@components/ContentError'; import NotificationList from '@components/NotificationList'; import { ResourceAccessList } from '@components/ResourceAccessList'; +import ScheduleList from '@components/ScheduleList'; import ProjectDetail from './ProjectDetail'; import ProjectEdit from './ProjectEdit'; import ProjectJobTemplatesList from './ProjectJobTemplatesList'; -import ProjectSchedules from './ProjectSchedules'; import { OrganizationsAPI, ProjectsAPI } from '@api'; class Project extends Component { @@ -30,6 +30,7 @@ class Project extends Component { }; this.loadProject = this.loadProject.bind(this); this.loadProjectAndRoles = this.loadProjectAndRoles.bind(this); + this.loadSchedules = this.loadSchedules.bind(this); } async componentDidMount() { @@ -103,6 +104,11 @@ class Project extends Component { } } + loadSchedules(params) { + const { project } = this.state; + return ProjectsAPI.readScheduleList(project.id, params); + } + render() { const { location, match, me, i18n } = this.props; @@ -134,16 +140,17 @@ class Project extends Component { }); } - tabsArray.push( - { - name: i18n._(t`Job Templates`), - link: `${match.url}/job_templates`, - }, - { + tabsArray.push({ + name: i18n._(t`Job Templates`), + link: `${match.url}/job_templates`, + }); + + if (project?.scm_type) { + tabsArray.push({ name: i18n._(t`Schedules`), link: `${match.url}/schedules`, - } - ); + }); + } tabsArray.forEach((tab, n) => { tab.id = n; @@ -230,10 +237,14 @@ class Project extends Component { )} /> - } - /> + {project?.scm_type && project.scm_type !== '' && ( + ( + + )} + /> + )} ', () => { done(); }); + test('schedules tab shown for scm based projects.', async done => { + ProjectsAPI.readDetail.mockResolvedValue({ data: mockDetails }); + OrganizationsAPI.read.mockResolvedValue({ + count: 0, + next: null, + previous: null, + data: { results: [] }, + }); + + const wrapper = mountWithContexts( + {}} me={mockMe} /> + ); + const tabs = await waitForElement( + wrapper, + '.pf-c-tabs__item', + el => el.length === 4 + ); + expect(tabs.at(3).text()).toEqual('Schedules'); + done(); + }); + + test('schedules tab hidden for manual projects.', async done => { + const manualDetails = Object.assign(mockDetails, { scm_type: '' }); + ProjectsAPI.readDetail.mockResolvedValue({ data: manualDetails }); + OrganizationsAPI.read.mockResolvedValue({ + count: 0, + next: null, + previous: null, + data: { results: [] }, + }); + + const wrapper = mountWithContexts( + {}} me={mockMe} /> + ); + const tabs = await waitForElement( + wrapper, + '.pf-c-tabs__item', + el => el.length === 3 + ); + tabs.forEach(tab => expect(tab.text()).not.toEqual('Schedules')); + done(); + }); + test('should show content error when user attempts to navigate to erroneous route', async () => { const history = createMemoryHistory({ initialEntries: ['/projects/1/foobar'], diff --git a/awx/ui_next/src/screens/Schedule/ScheduleList/index.js b/awx/ui_next/src/screens/Schedule/ScheduleList/index.js deleted file mode 100644 index 4f6af384b5..0000000000 --- a/awx/ui_next/src/screens/Schedule/ScheduleList/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as ScheduleList } from './ScheduleList'; -export { default as ScheduleListItem } from './ScheduleListItem'; diff --git a/awx/ui_next/src/screens/Schedule/Schedules.jsx b/awx/ui_next/src/screens/Schedule/Schedules.jsx index 930b115ecd..0f7c9b0ee0 100644 --- a/awx/ui_next/src/screens/Schedule/Schedules.jsx +++ b/awx/ui_next/src/screens/Schedule/Schedules.jsx @@ -4,9 +4,15 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import Breadcrumbs from '@components/Breadcrumbs'; -import { ScheduleList } from './ScheduleList'; +import ScheduleList from '@components/ScheduleList'; +import { SchedulesAPI } from '@api'; +import { PageSection, Card } from '@patternfly/react-core'; function Schedules({ i18n }) { + const loadSchedules = params => { + return SchedulesAPI.read(params); + }; + return ( <> - + + + + + diff --git a/awx/ui_next/src/screens/Template/Template.jsx b/awx/ui_next/src/screens/Template/Template.jsx index b46365aae4..84839c1a3f 100644 --- a/awx/ui_next/src/screens/Template/Template.jsx +++ b/awx/ui_next/src/screens/Template/Template.jsx @@ -10,6 +10,7 @@ import ContentError from '@components/ContentError'; import JobList from '@components/JobList'; import NotificationList from '@components/NotificationList'; import RoutedTabs from '@components/RoutedTabs'; +import ScheduleList from '@components/ScheduleList'; import { ResourceAccessList } from '@components/ResourceAccessList'; import JobTemplateDetail from './JobTemplateDetail'; import JobTemplateEdit from './JobTemplateEdit'; @@ -27,6 +28,7 @@ class Template extends Component { }; this.loadTemplate = this.loadTemplate.bind(this); this.loadTemplateAndRoles = this.loadTemplateAndRoles.bind(this); + this.loadSchedules = this.loadSchedules.bind(this); } async componentDidMount() { @@ -81,6 +83,11 @@ class Template extends Component { } } + loadSchedules(params) { + const { template } = this.state; + return JobTemplatesAPI.readScheduleList(template.id, params); + } + render() { const { i18n, location, match, me } = this.props; const { @@ -105,10 +112,6 @@ class Template extends Component { } tabsArray.push( - { - name: i18n._(t`Schedules`), - link: '/home', - }, { name: i18n._(t`Completed Jobs`), link: `${match.url}/completed_jobs`, @@ -119,6 +122,13 @@ class Template extends Component { } ); + if (template) { + tabsArray.push({ + name: i18n._(t`Schedules`), + link: `${match.url}/schedules`, + }); + } + tabsArray.forEach((tab, n) => { tab.id = n; }); @@ -210,6 +220,12 @@ class Template extends Component { )} + {template && ( + } + /> + )} { tab.id = n; }); @@ -162,6 +176,12 @@ class WorkflowJobTemplate extends Component { /> )} + {template && ( + } + /> + )}