diff --git a/awx/ui_next/src/api/index.js b/awx/ui_next/src/api/index.js
index cf033c16ae..e5f1f34557 100644
--- a/awx/ui_next/src/api/index.js
+++ b/awx/ui_next/src/api/index.js
@@ -17,6 +17,7 @@ import Organizations from './models/Organizations';
import Projects from './models/Projects';
import ProjectUpdates from './models/ProjectUpdates';
import Root from './models/Root';
+import Schedules from './models/Schedules';
import SystemJobs from './models/SystemJobs';
import Teams from './models/Teams';
import UnifiedJobTemplates from './models/UnifiedJobTemplates';
@@ -46,6 +47,7 @@ const OrganizationsAPI = new Organizations();
const ProjectsAPI = new Projects();
const ProjectUpdatesAPI = new ProjectUpdates();
const RootAPI = new Root();
+const SchedulesAPI = new Schedules();
const SystemJobsAPI = new SystemJobs();
const TeamsAPI = new Teams();
const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
@@ -76,6 +78,7 @@ export {
ProjectsAPI,
ProjectUpdatesAPI,
RootAPI,
+ SchedulesAPI,
SystemJobsAPI,
TeamsAPI,
UnifiedJobTemplatesAPI,
diff --git a/awx/ui_next/src/api/models/Schedules.js b/awx/ui_next/src/api/models/Schedules.js
new file mode 100644
index 0000000000..e5581d2875
--- /dev/null
+++ b/awx/ui_next/src/api/models/Schedules.js
@@ -0,0 +1,10 @@
+import Base from '../Base';
+
+class Schedules extends Base {
+ constructor(http) {
+ super(http);
+ this.baseUrl = '/api/v2/schedules/';
+ }
+}
+
+export default Schedules;
diff --git a/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.jsx b/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.jsx
new file mode 100644
index 0000000000..36b0b9ccdd
--- /dev/null
+++ b/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.jsx
@@ -0,0 +1,205 @@
+import React, { useState, useEffect } from 'react';
+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';
+import PaginatedDataList, {
+ ToolbarDeleteButton,
+} from '@components/PaginatedDataList';
+import { getQSConfig, parseQueryString } from '@util/qs';
+import { ScheduleListItem } from '.';
+
+const QS_CONFIG = getQSConfig('schedule', {
+ page: 1,
+ page_size: 20,
+ order_by: 'unified_job_template__polymorphic_ctype__model',
+});
+
+function ScheduleList({ i18n }) {
+ const [contentError, setContentError] = useState(null);
+ const [scheduleCount, setScheduleCount] = useState(0);
+ const [schedules, setSchedules] = useState([]);
+ const [deletionError, setDeletionError] = useState(null);
+ const [hasContentLoading, setHasContentLoading] = useState(true);
+ const [selected, setSelected] = useState([]);
+ const [toggleError, setToggleError] = useState(null);
+ const [toggleLoading, setToggleLoading] = useState(null);
+
+ const location = useLocation();
+
+ const loadSchedules = async ({ search }) => {
+ const params = parseQueryString(QS_CONFIG, search);
+ setContentError(null);
+ setHasContentLoading(true);
+ try {
+ const {
+ data: { count, results },
+ } = await SchedulesAPI.read(params);
+
+ setSchedules(results);
+ setScheduleCount(count);
+ } catch (error) {
+ setContentError(error);
+ } finally {
+ setHasContentLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadSchedules(location);
+ }, [location]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const handleSelectAll = isSelected => {
+ setSelected(isSelected ? [...schedules] : []);
+ };
+
+ const handleSelect = row => {
+ if (selected.some(s => s.id === row.id)) {
+ setSelected(selected.filter(s => s.id !== row.id));
+ } else {
+ setSelected(selected.concat(row));
+ }
+ };
+
+ const handleDelete = async () => {
+ setHasContentLoading(true);
+
+ try {
+ await Promise.all(
+ selected.map(schedule => SchedulesAPI.destroy(schedule.id))
+ );
+ } catch (error) {
+ setDeletionError(error);
+ }
+
+ const params = parseQueryString(QS_CONFIG, location.search);
+ try {
+ const {
+ data: { count, results },
+ } = await SchedulesAPI.read(params);
+
+ setSchedules(results);
+ setScheduleCount(count);
+ setSelected([]);
+ } catch (error) {
+ setContentError(error);
+ }
+
+ setHasContentLoading(false);
+ };
+
+ const handleScheduleToggle = async scheduleToToggle => {
+ setToggleLoading(scheduleToToggle.id);
+ try {
+ const { data: updatedSchedule } = await SchedulesAPI.update(
+ scheduleToToggle.id,
+ {
+ enabled: !scheduleToToggle.enabled,
+ }
+ );
+ setSchedules(
+ schedules.map(schedule =>
+ schedule.id === updatedSchedule.id ? updatedSchedule : schedule
+ )
+ );
+ } catch (err) {
+ setToggleError(err);
+ } finally {
+ setToggleLoading(null);
+ }
+ };
+
+ const isAllSelected =
+ selected.length > 0 && selected.length === schedules.length;
+
+ return (
+
+
+ (
+ row.id === item.id)}
+ key={item.id}
+ onSelect={() => handleSelect(item)}
+ onToggleSchedule={handleScheduleToggle}
+ schedule={item}
+ toggleLoading={toggleLoading === item.id}
+ />
+ )}
+ 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 => (
+ ,
+ ]}
+ />
+ )}
+ />
+
+ {toggleError && !toggleLoading && (
+ setToggleError(null)}
+ >
+ {i18n._(t`Failed to toggle schedule.`)}
+
+
+ )}
+ {deletionError && (
+ setDeletionError(null)}
+ >
+ {i18n._(t`Failed to delete one or more schedules.`)}
+
+
+ )}
+
+ );
+}
+
+export default withI18n()(ScheduleList);
diff --git a/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.test.jsx b/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.test.jsx
new file mode 100644
index 0000000000..aaece78d10
--- /dev/null
+++ b/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.test.jsx
@@ -0,0 +1,163 @@
+import React from 'react';
+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';
+
+jest.mock('@api/models/Schedules');
+
+SchedulesAPI.destroy = jest.fn();
+SchedulesAPI.update.mockResolvedValue({
+ data: mockSchedules.results[0],
+});
+
+describe('ScheduleList', () => {
+ let wrapper;
+
+ afterAll(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('read call successful', () => {
+ beforeAll(async () => {
+ SchedulesAPI.read.mockResolvedValue({ data: mockSchedules });
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+ });
+
+ afterAll(() => {
+ wrapper.unmount();
+ });
+
+ test('should fetch schedules from api and render the list', () => {
+ expect(SchedulesAPI.read).toHaveBeenCalled();
+ expect(wrapper.find('ScheduleListItem').length).toBe(5);
+ });
+
+ test('should check and uncheck the row item', async () => {
+ expect(
+ wrapper.find('PFDataListCheck[id="select-schedule-1"]').props().checked
+ ).toBe(false);
+ await act(async () => {
+ wrapper
+ .find('PFDataListCheck[id="select-schedule-1"]')
+ .invoke('onChange')(true);
+ });
+ wrapper.update();
+ expect(
+ wrapper.find('PFDataListCheck[id="select-schedule-1"]').props().checked
+ ).toBe(true);
+ await act(async () => {
+ wrapper
+ .find('PFDataListCheck[id="select-schedule-1"]')
+ .invoke('onChange')(false);
+ });
+ wrapper.update();
+ expect(
+ wrapper.find('PFDataListCheck[id="select-schedule-1"]').props().checked
+ ).toBe(false);
+ });
+
+ test('should check all row items when select all is checked', async () => {
+ wrapper.find('PFDataListCheck').forEach(el => {
+ expect(el.props().checked).toBe(false);
+ });
+ await act(async () => {
+ wrapper.find('Checkbox#select-all').invoke('onChange')(true);
+ });
+ wrapper.update();
+ wrapper.find('PFDataListCheck').forEach(el => {
+ expect(el.props().checked).toBe(true);
+ });
+ await act(async () => {
+ wrapper.find('Checkbox#select-all').invoke('onChange')(false);
+ });
+ wrapper.update();
+ wrapper.find('PFDataListCheck').forEach(el => {
+ expect(el.props().checked).toBe(false);
+ });
+ });
+
+ test('should call api delete schedules for each selected schedule', async () => {
+ await act(async () => {
+ wrapper
+ .find('PFDataListCheck[id="select-schedule-3"]')
+ .invoke('onChange')();
+ });
+ wrapper.update();
+ await act(async () => {
+ wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
+ });
+ wrapper.update();
+ expect(SchedulesAPI.destroy).toHaveBeenCalledTimes(1);
+ });
+
+ test('should show error modal when schedule is not successfully deleted from api', async () => {
+ SchedulesAPI.destroy.mockImplementationOnce(() =>
+ Promise.reject(new Error())
+ );
+ expect(wrapper.find('Modal').length).toBe(0);
+ await act(async () => {
+ wrapper
+ .find('PFDataListCheck[id="select-schedule-2"]')
+ .invoke('onChange')();
+ });
+ wrapper.update();
+ await act(async () => {
+ wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
+ });
+ wrapper.update();
+ expect(wrapper.find('Modal').length).toBe(1);
+ await act(async () => {
+ wrapper.find('ModalBoxCloseButton').invoke('onClose')();
+ });
+ wrapper.update();
+ expect(wrapper.find('Modal').length).toBe(0);
+ });
+
+ test('should call api update schedules when toggle clicked', async () => {
+ await act(async () => {
+ wrapper
+ .find('Switch[id="schedule-5-toggle"]')
+ .first()
+ .invoke('onChange')();
+ });
+ wrapper.update();
+ expect(SchedulesAPI.update).toHaveBeenCalledTimes(1);
+ });
+
+ test('should show error modal when schedule is not successfully updated on toggle', async () => {
+ SchedulesAPI.update.mockImplementationOnce(() =>
+ Promise.reject(new Error())
+ );
+ expect(wrapper.find('Modal').length).toBe(0);
+ await act(async () => {
+ wrapper
+ .find('Switch[id="schedule-1-toggle"]')
+ .first()
+ .invoke('onChange')();
+ });
+ wrapper.update();
+ expect(wrapper.find('Modal').length).toBe(1);
+ await act(async () => {
+ wrapper.find('ModalBoxCloseButton').invoke('onClose')();
+ });
+ wrapper.update();
+ expect(wrapper.find('Modal').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.update();
+ expect(wrapper.find('ContentError').length).toBe(1);
+ });
+ });
+});
diff --git a/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleListItem.jsx b/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleListItem.jsx
new file mode 100644
index 0000000000..1538d1ac91
--- /dev/null
+++ b/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleListItem.jsx
@@ -0,0 +1,157 @@
+import React from 'react';
+import { bool, func } from 'prop-types';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { Link } from 'react-router-dom';
+import {
+ DataListItem,
+ DataListItemRow,
+ DataListItemCells as _DataListItemCells,
+ Tooltip,
+} from '@patternfly/react-core';
+import { PencilAltIcon } from '@patternfly/react-icons';
+
+import ActionButtonCell from '@components/ActionButtonCell';
+import { DetailList, Detail } from '@components/DetailList';
+import DataListCell from '@components/DataListCell';
+import DataListCheck from '@components/DataListCheck';
+import ListActionButton from '@components/ListActionButton';
+import Switch from '@components/Switch';
+import VerticalSeparator from '@components/VerticalSeparator';
+import styled from 'styled-components';
+import { Schedule } from '@types';
+import { formatDateString } from '@util/dates';
+
+const DataListItemCells = styled(_DataListItemCells)`
+ ${DataListCell}:first-child {
+ flex-grow: 2;
+ }
+`;
+
+function ScheduleListItem({
+ i18n,
+ isSelected,
+ onSelect,
+ onToggleSchedule,
+ schedule,
+ toggleLoading,
+}) {
+ const labelId = `check-action-${schedule.id}`;
+
+ const jobTypeLabels = {
+ inventory_update: i18n._(t`Inventory Sync`),
+ job: i18n._(t`Playbook Run`),
+ project_update: i18n._(t`SCM Update`),
+ system_job: i18n._(t`Management Job`),
+ workflow_job: i18n._(t`Workflow Job`),
+ };
+
+ let scheduleBaseUrl;
+
+ switch (schedule.summary_fields.unified_job_template.unified_job_type) {
+ case 'inventory_update':
+ scheduleBaseUrl = `/inventories/${schedule.summary_fields.inventory.id}/sources/${schedule.summary_fields.unified_job_template.id}/schedules/${schedule.id}`;
+ break;
+ case 'job':
+ scheduleBaseUrl = `/templates/job_template/${schedule.summary_fields.unified_job_template.id}/schedules/${schedule.id}`;
+ break;
+ case 'project_update':
+ scheduleBaseUrl = `/projects/${schedule.summary_fields.unified_job_template.id}/schedules/${schedule.id}`;
+ break;
+ case 'system_job':
+ scheduleBaseUrl = `/management_jobs/${schedule.summary_fields.unified_job_template.id}/schedules/${schedule.id}`;
+ break;
+ case 'workflow_job':
+ scheduleBaseUrl = `/templates/workflow_job_template/${schedule.summary_fields.unified_job_template.id}/schedules/${schedule.id}`;
+ break;
+ default:
+ break;
+ }
+
+ return (
+
+
+
+
+
+
+ {schedule.name}
+
+ ,
+
+ {
+ jobTypeLabels[
+ schedule.summary_fields.unified_job_template.unified_job_type
+ ]
+ }
+ ,
+
+ {schedule.next_run && (
+
+
+
+ )}
+ ,
+
+
+ onToggleSchedule(schedule)}
+ aria-label={i18n._(t`Toggle schedule`)}
+ />
+
+ {schedule.summary_fields.user_capabilities.edit && (
+
+
+
+
+
+ )}
+ ,
+ ]}
+ />
+
+
+ );
+}
+
+ScheduleListItem.propTypes = {
+ isSelected: bool.isRequired,
+ onToggleSchedule: func.isRequired,
+ onSelect: func.isRequired,
+ schedule: Schedule.isRequired,
+};
+
+export default withI18n()(ScheduleListItem);
diff --git a/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleListItem.test.jsx b/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleListItem.test.jsx
new file mode 100644
index 0000000000..df2b55bcbb
--- /dev/null
+++ b/awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleListItem.test.jsx
@@ -0,0 +1,180 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import ScheduleListItem from './ScheduleListItem';
+
+const mockSchedule = {
+ rrule:
+ 'DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1',
+ id: 6,
+ type: 'schedule',
+ url: '/api/v2/schedules/6/',
+ related: {},
+ summary_fields: {
+ unified_job_template: {
+ id: 12,
+ name: 'Mock JT',
+ description: '',
+ unified_job_type: 'job',
+ },
+ user_capabilities: {
+ edit: true,
+ delete: true,
+ },
+ },
+ created: '2020-02-12T21:05:08.460029Z',
+ modified: '2020-02-12T21:05:52.840596Z',
+ name: 'Mock Schedule',
+ description: 'every day for 1 time',
+ extra_data: {},
+ inventory: null,
+ scm_branch: null,
+ job_type: null,
+ job_tags: null,
+ skip_tags: null,
+ limit: null,
+ diff_mode: null,
+ verbosity: null,
+ unified_job_template: 12,
+ enabled: true,
+ dtstart: '2020-02-20T05:00:00Z',
+ dtend: '2020-02-20T05:00:00Z',
+ next_run: '2020-02-20T05:00:00Z',
+ timezone: 'America/New_York',
+ until: '',
+};
+
+const onToggleSchedule = jest.fn();
+const onSelect = jest.fn();
+
+describe('ScheduleListItem', () => {
+ let wrapper;
+ describe('User has edit permissions', () => {
+ beforeAll(() => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ afterAll(() => {
+ wrapper.unmount();
+ });
+ test('Name correctly shown with correct link', () => {
+ expect(
+ wrapper
+ .find('DataListCell')
+ .first()
+ .text()
+ ).toBe('Mock Schedule');
+ expect(
+ wrapper
+ .find('DataListCell')
+ .first()
+ .find('Link')
+ .props().to
+ ).toBe('/templates/job_template/12/schedules/6/details');
+ });
+ test('Type correctly shown', () => {
+ expect(
+ wrapper
+ .find('DataListCell')
+ .at(2)
+ .text()
+ ).toBe('Playbook Run');
+ });
+ test('Edit button shown with correct link', () => {
+ expect(wrapper.find('PencilAltIcon').length).toBe(1);
+ expect(
+ wrapper
+ .find('ListActionButton')
+ .find('Link')
+ .props().to
+ ).toBe('/templates/job_template/12/schedules/6/edit');
+ });
+ test('Toggle button enabled', () => {
+ expect(
+ wrapper
+ .find('Switch')
+ .first()
+ .props().isDisabled
+ ).toBe(false);
+ });
+ test('Clicking toggle makes expected callback', () => {
+ wrapper
+ .find('Switch')
+ .first()
+ .find('input')
+ .simulate('change');
+ expect(onToggleSchedule).toHaveBeenCalledWith(mockSchedule);
+ });
+ test('Clicking checkbox makes expected callback', () => {
+ wrapper
+ .find('PFDataListCheck')
+ .first()
+ .find('input')
+ .simulate('change');
+ expect(onSelect).toHaveBeenCalledTimes(1);
+ });
+ });
+ describe('User has read-only permissions', () => {
+ beforeAll(() => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ afterAll(() => {
+ wrapper.unmount();
+ });
+ test('Name correctly shown with correct link', () => {
+ expect(
+ wrapper
+ .find('DataListCell')
+ .first()
+ .text()
+ ).toBe('Mock Schedule');
+ expect(
+ wrapper
+ .find('DataListCell')
+ .first()
+ .find('Link')
+ .props().to
+ ).toBe('/templates/job_template/12/schedules/6/details');
+ });
+ test('Type correctly shown', () => {
+ expect(
+ wrapper
+ .find('DataListCell')
+ .at(2)
+ .text()
+ ).toBe('Playbook Run');
+ });
+ test('Edit button hidden', () => {
+ expect(wrapper.find('PencilAltIcon').length).toBe(0);
+ });
+ test('Toggle button disabled', () => {
+ expect(
+ wrapper
+ .find('Switch')
+ .first()
+ .props().isDisabled
+ ).toBe(true);
+ });
+ });
+});
diff --git a/awx/ui_next/src/screens/Schedule/ScheduleList/index.js b/awx/ui_next/src/screens/Schedule/ScheduleList/index.js
new file mode 100644
index 0000000000..4f6af384b5
--- /dev/null
+++ b/awx/ui_next/src/screens/Schedule/ScheduleList/index.js
@@ -0,0 +1,2 @@
+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 b9375f86e8..930b115ecd 100644
--- a/awx/ui_next/src/screens/Schedule/Schedules.jsx
+++ b/awx/ui_next/src/screens/Schedule/Schedules.jsx
@@ -1,26 +1,26 @@
-import React, { Component, Fragment } from 'react';
+import React from 'react';
+import { Route, Switch } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
-import {
- PageSection,
- PageSectionVariants,
- Title,
-} from '@patternfly/react-core';
-class Schedules extends Component {
- render() {
- const { i18n } = this.props;
- const { light } = PageSectionVariants;
+import Breadcrumbs from '@components/Breadcrumbs';
+import { ScheduleList } from './ScheduleList';
- return (
-
-
- {i18n._(t`Schedules`)}
-
-
-
- );
- }
+function Schedules({ i18n }) {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
}
export default withI18n()(Schedules);
diff --git a/awx/ui_next/src/screens/Schedule/Schedules.test.jsx b/awx/ui_next/src/screens/Schedule/Schedules.test.jsx
index 5296ecf689..ebdc67a046 100644
--- a/awx/ui_next/src/screens/Schedule/Schedules.test.jsx
+++ b/awx/ui_next/src/screens/Schedule/Schedules.test.jsx
@@ -1,29 +1,36 @@
import React from 'react';
-
import { mountWithContexts } from '@testUtils/enzymeHelpers';
-
+import { createMemoryHistory } from 'history';
import Schedules from './Schedules';
describe('', () => {
- let pageWrapper;
- let pageSections;
- let title;
-
- beforeEach(() => {
- pageWrapper = mountWithContexts();
- pageSections = pageWrapper.find('PageSection');
- title = pageWrapper.find('Title');
- });
+ let wrapper;
afterEach(() => {
- pageWrapper.unmount();
+ wrapper.unmount();
});
- test('initially renders without crashing', () => {
- expect(pageWrapper.length).toBe(1);
- expect(pageSections.length).toBe(2);
- expect(title.length).toBe(1);
- expect(title.props().size).toBe('2xl');
- expect(pageSections.first().props().variant).toBe('light');
+ test('initially renders succesfully', () => {
+ wrapper = mountWithContexts();
+ });
+
+ test('should display schedule list breadcrumb heading', () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/schedules'],
+ });
+
+ wrapper = mountWithContexts(, {
+ context: {
+ router: {
+ history,
+ route: {
+ location: history.location,
+ },
+ },
+ },
+ });
+
+ expect(wrapper.find('Crumb').length).toBe(1);
+ expect(wrapper.find('BreadcrumbHeading').text()).toBe('Schedules');
});
});
diff --git a/awx/ui_next/src/screens/Schedule/data.schedules.json b/awx/ui_next/src/screens/Schedule/data.schedules.json
new file mode 100644
index 0000000000..13ef941811
--- /dev/null
+++ b/awx/ui_next/src/screens/Schedule/data.schedules.json
@@ -0,0 +1,106 @@
+{
+ "count": 5,
+ "next": "/api/v2/schedules/",
+ "previous": null,
+ "results": [
+ {
+ "url": "/api/v2/schedules/1",
+ "rrule":
+ "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1",
+ "id": 1,
+ "summary_fields": {
+ "unified_job_template": {
+ "id": 6,
+ "name": "Mock JT",
+ "description": "",
+ "unified_job_type": "job"
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true
+ }
+ },
+ "name": "Mock JT Schedule",
+ "next_run": "2020-02-20T05:00:00Z"
+ }, {
+ "url": "/api/v2/schedules/2",
+ "rrule":
+ "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1",
+ "id": 2,
+ "summary_fields": {
+ "unified_job_template": {
+ "id": 7,
+ "name": "Mock WFJT",
+ "description": "",
+ "unified_job_type": "workflow_job"
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true
+ }
+ },
+ "name": "Mock WFJT Schedule",
+ "next_run": "2020-02-20T05:00:00Z"
+ }, {
+ "url": "/api/v2/schedules/3",
+ "rrule":
+ "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1",
+ "id": 3,
+ "summary_fields": {
+ "unified_job_template": {
+ "id": 8,
+ "name": "Mock Project Update",
+ "description": "",
+ "unified_job_type": "project_update"
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true
+ }
+ },
+ "name": "Mock Project Update Schedule",
+ "next_run": "2020-02-20T05:00:00Z"
+ }, {
+ "url": "/api/v2/schedules/4",
+ "rrule":
+ "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1",
+ "id": 4,
+ "summary_fields": {
+ "unified_job_template": {
+ "id": 9,
+ "name": "Mock System Job",
+ "description": "",
+ "unified_job_type": "system_job"
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true
+ }
+ },
+ "name": "Mock System Job Schedule",
+ "next_run": "2020-02-20T05:00:00Z"
+ }, {
+ "url": "/api/v2/schedules/5",
+ "rrule":
+ "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1",
+ "id": 5,
+ "summary_fields": {
+ "unified_job_template": {
+ "id": 10,
+ "name": "Mock Inventory Update",
+ "description": "",
+ "unified_job_type": "inventory_update"
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true
+ },
+ "inventory": {
+ "id": 11
+ }
+ },
+ "name": "Mock Inventory Update Schedule",
+ "next_run": "2020-02-20T05:00:00Z"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/awx/ui_next/src/types.js b/awx/ui_next/src/types.js
index 1c072c8383..2ef6f718ba 100644
--- a/awx/ui_next/src/types.js
+++ b/awx/ui_next/src/types.js
@@ -271,3 +271,32 @@ export const SortColumns = arrayOf(
key: string.isRequired,
})
);
+
+export const Schedule = shape({
+ rrule: string.isRequired,
+ id: number.isRequired,
+ type: string,
+ url: string,
+ related: shape({}),
+ summary_fields: shape({}),
+ created: string,
+ modified: string,
+ name: string.isRequired,
+ description: string,
+ extra_data: shape({}),
+ inventory: number,
+ scm_branch: string,
+ job_type: string,
+ job_tags: string,
+ skip_tags: string,
+ limit: string,
+ diff_mode: bool,
+ verbosity: string,
+ unified_job_template: number,
+ enabled: bool,
+ dtstart: string,
+ dtend: string,
+ next_run: string,
+ timezone: string,
+ until: string,
+});