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