mirror of
https://github.com/ansible/awx.git
synced 2026-05-24 00:57:48 -02:30
Adds the All Schedules list
This commit is contained in:
@@ -17,6 +17,7 @@ import Organizations from './models/Organizations';
|
|||||||
import Projects from './models/Projects';
|
import Projects from './models/Projects';
|
||||||
import ProjectUpdates from './models/ProjectUpdates';
|
import ProjectUpdates from './models/ProjectUpdates';
|
||||||
import Root from './models/Root';
|
import Root from './models/Root';
|
||||||
|
import Schedules from './models/Schedules';
|
||||||
import SystemJobs from './models/SystemJobs';
|
import SystemJobs from './models/SystemJobs';
|
||||||
import Teams from './models/Teams';
|
import Teams from './models/Teams';
|
||||||
import UnifiedJobTemplates from './models/UnifiedJobTemplates';
|
import UnifiedJobTemplates from './models/UnifiedJobTemplates';
|
||||||
@@ -46,6 +47,7 @@ const OrganizationsAPI = new Organizations();
|
|||||||
const ProjectsAPI = new Projects();
|
const ProjectsAPI = new Projects();
|
||||||
const ProjectUpdatesAPI = new ProjectUpdates();
|
const ProjectUpdatesAPI = new ProjectUpdates();
|
||||||
const RootAPI = new Root();
|
const RootAPI = new Root();
|
||||||
|
const SchedulesAPI = new Schedules();
|
||||||
const SystemJobsAPI = new SystemJobs();
|
const SystemJobsAPI = new SystemJobs();
|
||||||
const TeamsAPI = new Teams();
|
const TeamsAPI = new Teams();
|
||||||
const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
|
const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
|
||||||
@@ -76,6 +78,7 @@ export {
|
|||||||
ProjectsAPI,
|
ProjectsAPI,
|
||||||
ProjectUpdatesAPI,
|
ProjectUpdatesAPI,
|
||||||
RootAPI,
|
RootAPI,
|
||||||
|
SchedulesAPI,
|
||||||
SystemJobsAPI,
|
SystemJobsAPI,
|
||||||
TeamsAPI,
|
TeamsAPI,
|
||||||
UnifiedJobTemplatesAPI,
|
UnifiedJobTemplatesAPI,
|
||||||
|
|||||||
10
awx/ui_next/src/api/models/Schedules.js
Normal file
10
awx/ui_next/src/api/models/Schedules.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import Base from '../Base';
|
||||||
|
|
||||||
|
class Schedules extends Base {
|
||||||
|
constructor(http) {
|
||||||
|
super(http);
|
||||||
|
this.baseUrl = '/api/v2/schedules/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Schedules;
|
||||||
205
awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.jsx
Normal file
205
awx/ui_next/src/screens/Schedule/ScheduleList/ScheduleList.jsx
Normal file
@@ -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 (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<PaginatedDataList
|
||||||
|
contentError={contentError}
|
||||||
|
hasContentLoading={hasContentLoading}
|
||||||
|
items={schedules}
|
||||||
|
itemCount={scheduleCount}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
onRowClick={handleSelect}
|
||||||
|
renderItem={item => (
|
||||||
|
<ScheduleListItem
|
||||||
|
isSelected={selected.some(row => 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 => (
|
||||||
|
<DataListToolbar
|
||||||
|
{...props}
|
||||||
|
showSelectAll
|
||||||
|
isAllSelected={isAllSelected}
|
||||||
|
onSelectAll={handleSelectAll}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
additionalControls={[
|
||||||
|
<ToolbarDeleteButton
|
||||||
|
key="delete"
|
||||||
|
onDelete={handleDelete}
|
||||||
|
itemsToDelete={selected}
|
||||||
|
pluralizedItemName={i18n._(t`Schedules`)}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
{toggleError && !toggleLoading && (
|
||||||
|
<AlertModal
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
isOpen={toggleError && !toggleLoading}
|
||||||
|
onClose={() => setToggleError(null)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to toggle schedule.`)}
|
||||||
|
<ErrorDetail error={toggleError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
{deletionError && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={deletionError}
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={() => setDeletionError(null)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to delete one or more schedules.`)}
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(ScheduleList);
|
||||||
@@ -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(<ScheduleList />);
|
||||||
|
});
|
||||||
|
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(<ScheduleList />);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<DataListItem
|
||||||
|
key={schedule.id}
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
id={`${schedule.id}`}
|
||||||
|
>
|
||||||
|
<DataListItemRow>
|
||||||
|
<DataListCheck
|
||||||
|
id={`select-schedule-${schedule.id}`}
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={onSelect}
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
/>
|
||||||
|
<DataListItemCells
|
||||||
|
dataListCells={[
|
||||||
|
<DataListCell key="name">
|
||||||
|
<VerticalSeparator />
|
||||||
|
<Link to={`${scheduleBaseUrl}/details`}>
|
||||||
|
<b>{schedule.name}</b>
|
||||||
|
</Link>
|
||||||
|
</DataListCell>,
|
||||||
|
<DataListCell key="type">
|
||||||
|
{
|
||||||
|
jobTypeLabels[
|
||||||
|
schedule.summary_fields.unified_job_template.unified_job_type
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</DataListCell>,
|
||||||
|
<DataListCell key="next_run">
|
||||||
|
{schedule.next_run && (
|
||||||
|
<DetailList stacked>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Next Run`)}
|
||||||
|
value={formatDateString(schedule.next_run)}
|
||||||
|
/>
|
||||||
|
</DetailList>
|
||||||
|
)}
|
||||||
|
</DataListCell>,
|
||||||
|
<ActionButtonCell lastcolumn="true" key="action">
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
schedule.enabled
|
||||||
|
? i18n._(t`Schedule is active`)
|
||||||
|
: i18n._(t`Schedule is inactive`)
|
||||||
|
}
|
||||||
|
position="top"
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
id={`schedule-${schedule.id}-toggle`}
|
||||||
|
label={i18n._(t`On`)}
|
||||||
|
labelOff={i18n._(t`Off`)}
|
||||||
|
isChecked={schedule.enabled}
|
||||||
|
isDisabled={
|
||||||
|
toggleLoading ||
|
||||||
|
!schedule.summary_fields.user_capabilities.edit
|
||||||
|
}
|
||||||
|
onChange={() => onToggleSchedule(schedule)}
|
||||||
|
aria-label={i18n._(t`Toggle schedule`)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
{schedule.summary_fields.user_capabilities.edit && (
|
||||||
|
<Tooltip content={i18n._(t`Edit Schedule`)} position="top">
|
||||||
|
<ListActionButton
|
||||||
|
variant="plain"
|
||||||
|
component={Link}
|
||||||
|
to={`${scheduleBaseUrl}/edit`}
|
||||||
|
>
|
||||||
|
<PencilAltIcon />
|
||||||
|
</ListActionButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</ActionButtonCell>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</DataListItemRow>
|
||||||
|
</DataListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScheduleListItem.propTypes = {
|
||||||
|
isSelected: bool.isRequired,
|
||||||
|
onToggleSchedule: func.isRequired,
|
||||||
|
onSelect: func.isRequired,
|
||||||
|
schedule: Schedule.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(ScheduleListItem);
|
||||||
@@ -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(
|
||||||
|
<ScheduleListItem
|
||||||
|
isSelected={false}
|
||||||
|
onSelect={onSelect}
|
||||||
|
schedule={mockSchedule}
|
||||||
|
onToggleSchedule={onToggleSchedule}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
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(
|
||||||
|
<ScheduleListItem
|
||||||
|
isSelected={false}
|
||||||
|
onSelect={onSelect}
|
||||||
|
schedule={{
|
||||||
|
...mockSchedule,
|
||||||
|
summary_fields: {
|
||||||
|
...mockSchedule.summary_fields,
|
||||||
|
user_capabilities: {
|
||||||
|
edit: false,
|
||||||
|
delete: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onToggleSchedule={onToggleSchedule}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
2
awx/ui_next/src/screens/Schedule/ScheduleList/index.js
Normal file
2
awx/ui_next/src/screens/Schedule/ScheduleList/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as ScheduleList } from './ScheduleList';
|
||||||
|
export { default as ScheduleListItem } from './ScheduleListItem';
|
||||||
@@ -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 { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
|
||||||
PageSection,
|
|
||||||
PageSectionVariants,
|
|
||||||
Title,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
class Schedules extends Component {
|
import Breadcrumbs from '@components/Breadcrumbs';
|
||||||
render() {
|
import { ScheduleList } from './ScheduleList';
|
||||||
const { i18n } = this.props;
|
|
||||||
const { light } = PageSectionVariants;
|
|
||||||
|
|
||||||
return (
|
function Schedules({ i18n }) {
|
||||||
<Fragment>
|
return (
|
||||||
<PageSection variant={light} className="pf-m-condensed">
|
<>
|
||||||
<Title size="2xl">{i18n._(t`Schedules`)}</Title>
|
<Breadcrumbs
|
||||||
</PageSection>
|
breadcrumbConfig={{
|
||||||
<PageSection />
|
'/schedules': i18n._(t`Schedules`),
|
||||||
</Fragment>
|
}}
|
||||||
);
|
/>
|
||||||
}
|
<Switch>
|
||||||
|
<Route path="/schedules">
|
||||||
|
<ScheduleList />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n()(Schedules);
|
export default withI18n()(Schedules);
|
||||||
|
|||||||
@@ -1,29 +1,36 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import Schedules from './Schedules';
|
import Schedules from './Schedules';
|
||||||
|
|
||||||
describe('<Schedules />', () => {
|
describe('<Schedules />', () => {
|
||||||
let pageWrapper;
|
let wrapper;
|
||||||
let pageSections;
|
|
||||||
let title;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
pageWrapper = mountWithContexts(<Schedules />);
|
|
||||||
pageSections = pageWrapper.find('PageSection');
|
|
||||||
title = pageWrapper.find('Title');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
pageWrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders succesfully', () => {
|
||||||
expect(pageWrapper.length).toBe(1);
|
wrapper = mountWithContexts(<Schedules />);
|
||||||
expect(pageSections.length).toBe(2);
|
});
|
||||||
expect(title.length).toBe(1);
|
|
||||||
expect(title.props().size).toBe('2xl');
|
test('should display schedule list breadcrumb heading', () => {
|
||||||
expect(pageSections.first().props().variant).toBe('light');
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/schedules'],
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper = mountWithContexts(<Schedules />, {
|
||||||
|
context: {
|
||||||
|
router: {
|
||||||
|
history,
|
||||||
|
route: {
|
||||||
|
location: history.location,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('Crumb').length).toBe(1);
|
||||||
|
expect(wrapper.find('BreadcrumbHeading').text()).toBe('Schedules');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
106
awx/ui_next/src/screens/Schedule/data.schedules.json
Normal file
106
awx/ui_next/src/screens/Schedule/data.schedules.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -271,3 +271,32 @@ export const SortColumns = arrayOf(
|
|||||||
key: string.isRequired,
|
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,
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user