Adds Add button to schedules list along with rbac restrictions

This commit is contained in:
mabashian 2020-03-05 12:33:59 -05:00
parent 4fcd2c594c
commit d33daeee91
10 changed files with 157 additions and 43 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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/`);
}

View File

@ -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;

View File

@ -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 (
<>
<PaginatedDataList
@ -130,6 +149,14 @@ function ScheduleList({ i18n, loadSchedules }) {
onSelectAll={handleSelectAll}
qsConfig={QS_CONFIG}
additionalControls={[
...(canAdd
? [
<ToolbarAddButton
key="add"
linkTo={`${location.pathname}/add`}
/>,
]
: []),
<ToolbarDeleteButton
key="delete"
onDelete={handleDelete}
@ -155,4 +182,13 @@ function ScheduleList({ i18n, loadSchedules }) {
);
}
ScheduleList.propTypes = {
hideAddButton: bool,
loadSchedules: func.isRequired,
loadScheduleOptions: func.isRequired,
};
ScheduleList.defaultProps = {
hideAddButton: false,
};
export default withI18n()(ScheduleList);

View File

@ -11,6 +11,18 @@ SchedulesAPI.destroy = jest.fn();
SchedulesAPI.update.mockResolvedValue({
data: mockSchedules.results[0],
});
SchedulesAPI.read.mockResolvedValue({ data: mockSchedules });
SchedulesAPI.readOptions.mockResolvedValue({
data: {
actions: {
GET: {},
POST: {},
},
},
});
const loadSchedules = params => 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(
<ScheduleList loadSchedules={loadSchedules} />
<ScheduleList
loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions}
/>
);
});
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(
<ScheduleList
loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions}
hideAddButton
/>
);
});
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(<ScheduleList />);
wrapper = mountWithContexts(
<ScheduleList
loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions}
/>
);
});
wrapper.update();
expect(wrapper.find('ContentError').length).toBe(1);

View File

@ -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 {
<Route
path="/projects/:id/schedules"
render={() => (
<ScheduleList loadSchedules={this.loadSchedules} />
<ScheduleList
loadSchedules={this.loadSchedules}
loadScheduleOptions={this.loadScheduleOptions}
/>
)}
/>
)}

View File

@ -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 }) {
<Route path="/schedules">
<PageSection>
<Card>
<ScheduleList loadSchedules={loadSchedules} />
<ScheduleList
loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions}
hideAddButton
/>
</Card>
</PageSection>
</Route>

View File

@ -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 {
<Route
path="/templates/:templateType/:id/schedules"
render={() => (
<ScheduleList loadSchedules={this.loadSchedules} />
<ScheduleList
loadSchedules={this.loadSchedules}
loadScheduleOptions={this.loadScheduleOptions}
/>
)}
/>
)}

View File

@ -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 {
<Route
path="/templates/:templateType/:id/schedules"
render={() => (
<ScheduleList loadSchedules={this.loadSchedules} />
<ScheduleList
loadSchedules={this.loadSchedules}
loadScheduleOptions={this.loadScheduleOptions}
/>
)}
/>
)}