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 Base from '../Base';
import NotificationsMixin from '../mixins/Notifications.mixin'; import NotificationsMixin from '../mixins/Notifications.mixin';
import InstanceGroupsMixin from '../mixins/InstanceGroups.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) { constructor(http) {
super(http); super(http);
this.baseUrl = '/api/v2/job_templates/'; this.baseUrl = '/api/v2/job_templates/';
@@ -61,10 +64,6 @@ class JobTemplates extends InstanceGroupsMixin(NotificationsMixin(Base)) {
params, params,
}); });
} }
readScheduleList(id, params) {
return this.http.get(`${this.baseUrl}${id}/schedules/`, { params });
}
} }
export default JobTemplates; export default JobTemplates;

View File

@@ -1,8 +1,11 @@
import Base from '../Base'; import Base from '../Base';
import NotificationsMixin from '../mixins/Notifications.mixin'; import NotificationsMixin from '../mixins/Notifications.mixin';
import LaunchUpdateMixin from '../mixins/LaunchUpdate.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) { constructor(http) {
super(http); super(http);
this.baseUrl = '/api/v2/projects/'; this.baseUrl = '/api/v2/projects/';
@@ -21,10 +24,6 @@ class Projects extends LaunchUpdateMixin(NotificationsMixin(Base)) {
return this.http.get(`${this.baseUrl}${id}/playbooks/`); return this.http.get(`${this.baseUrl}${id}/playbooks/`);
} }
readScheduleList(id, params) {
return this.http.get(`${this.baseUrl}${id}/schedules/`, { params });
}
readSync(id) { readSync(id) {
return this.http.get(`${this.baseUrl}${id}/update/`); return this.http.get(`${this.baseUrl}${id}/update/`);
} }

View File

@@ -1,6 +1,7 @@
import Base from '../Base'; import Base from '../Base';
import SchedulesMixin from '../mixins/Schedules.mixin';
class WorkflowJobTemplates extends Base { class WorkflowJobTemplates extends SchedulesMixin(Base) {
constructor(http) { constructor(http) {
super(http); super(http);
this.baseUrl = '/api/v2/workflow_job_templates/'; this.baseUrl = '/api/v2/workflow_job_templates/';
@@ -45,12 +46,6 @@ class WorkflowJobTemplates extends Base {
params, params,
}); });
} }
readScheduleList(id, params) {
return this.http.get(`${this.baseUrl}${id}/schedules/`, {
params,
});
}
} }
export default WorkflowJobTemplates; export default WorkflowJobTemplates;

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { bool, func } from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { SchedulesAPI } from '@api'; import { SchedulesAPI } from '@api';
@@ -7,6 +8,7 @@ import AlertModal from '@components/AlertModal';
import ErrorDetail from '@components/ErrorDetail'; import ErrorDetail from '@components/ErrorDetail';
import DataListToolbar from '@components/DataListToolbar'; import DataListToolbar from '@components/DataListToolbar';
import PaginatedDataList, { import PaginatedDataList, {
ToolbarAddButton,
ToolbarDeleteButton, ToolbarDeleteButton,
} from '@components/PaginatedDataList'; } from '@components/PaginatedDataList';
import useRequest, { useDeleteItems } from '@util/useRequest'; import useRequest, { useDeleteItems } from '@util/useRequest';
@@ -19,28 +21,40 @@ const QS_CONFIG = getQSConfig('schedule', {
order_by: 'unified_job_template__polymorphic_ctype__model', order_by: 'unified_job_template__polymorphic_ctype__model',
}); });
function ScheduleList({ i18n, loadSchedules }) { function ScheduleList({
i18n,
loadSchedules,
loadScheduleOptions,
hideAddButton,
}) {
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const location = useLocation(); const location = useLocation();
const { const {
result: { schedules, itemCount }, result: { schedules, itemCount, actions },
error: contentError, error: contentError,
isLoading, isLoading,
request: fetchSchedules, request: fetchSchedules,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const params = parseQueryString(QS_CONFIG, location.search); const params = parseQueryString(QS_CONFIG, location.search);
const response = loadSchedules(params); const [
const { {
data: { count, results }, data: { count, results },
} = await response; },
return { itemCount: count, schedules: results }; scheduleActions,
}, [location, loadSchedules]), ] = await Promise.all([loadSchedules(params), loadScheduleOptions()]);
return {
schedules: results,
itemCount: count,
actions: scheduleActions.data.actions,
};
}, [location, loadSchedules, loadScheduleOptions]),
{ {
schedules: [], schedules: [],
itemCount: 0, itemCount: 0,
actions: {},
} }
); );
@@ -84,6 +98,11 @@ function ScheduleList({ i18n, loadSchedules }) {
setSelected([]); setSelected([]);
}; };
const canAdd =
actions &&
Object.prototype.hasOwnProperty.call(actions, 'POST') &&
!hideAddButton;
return ( return (
<> <>
<PaginatedDataList <PaginatedDataList
@@ -130,6 +149,14 @@ function ScheduleList({ i18n, loadSchedules }) {
onSelectAll={handleSelectAll} onSelectAll={handleSelectAll}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd
? [
<ToolbarAddButton
key="add"
linkTo={`${location.pathname}/add`}
/>,
]
: []),
<ToolbarDeleteButton <ToolbarDeleteButton
key="delete" key="delete"
onDelete={handleDelete} 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); export default withI18n()(ScheduleList);

View File

@@ -11,6 +11,18 @@ SchedulesAPI.destroy = jest.fn();
SchedulesAPI.update.mockResolvedValue({ SchedulesAPI.update.mockResolvedValue({
data: mockSchedules.results[0], 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', () => { describe('ScheduleList', () => {
let wrapper; let wrapper;
@@ -21,11 +33,12 @@ describe('ScheduleList', () => {
describe('read call successful', () => { describe('read call successful', () => {
beforeAll(async () => { beforeAll(async () => {
SchedulesAPI.read.mockResolvedValue({ data: mockSchedules });
const loadSchedules = params => SchedulesAPI.read(params);
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<ScheduleList loadSchedules={loadSchedules} /> <ScheduleList
loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions}
/>
); );
}); });
wrapper.update(); wrapper.update();
@@ -40,6 +53,10 @@ describe('ScheduleList', () => {
expect(wrapper.find('ScheduleListItem').length).toBe(5); 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 () => { test('should check and uncheck the row item', async () => {
expect( expect(
wrapper.find('DataListCheck[id="select-schedule-1"]').props().checked 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', () => { describe('read call unsuccessful', () => {
test('should show content error when read call unsuccessful', async () => { test('should show content error when read call unsuccessful', async () => {
SchedulesAPI.read.mockRejectedValue(new Error()); SchedulesAPI.read.mockRejectedValue(new Error());
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<ScheduleList />); wrapper = mountWithContexts(
<ScheduleList
loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions}
/>
);
}); });
wrapper.update(); wrapper.update();
expect(wrapper.find('ContentError').length).toBe(1); expect(wrapper.find('ContentError').length).toBe(1);

View File

@@ -31,6 +31,7 @@ class Project extends Component {
this.loadProject = this.loadProject.bind(this); this.loadProject = this.loadProject.bind(this);
this.loadProjectAndRoles = this.loadProjectAndRoles.bind(this); this.loadProjectAndRoles = this.loadProjectAndRoles.bind(this);
this.loadSchedules = this.loadSchedules.bind(this); this.loadSchedules = this.loadSchedules.bind(this);
this.loadScheduleOptions = this.loadScheduleOptions.bind(this);
} }
async componentDidMount() { async componentDidMount() {
@@ -104,9 +105,14 @@ class Project extends Component {
} }
} }
loadScheduleOptions() {
const { project } = this.state;
return ProjectsAPI.readScheduleOptions(project.id);
}
loadSchedules(params) { loadSchedules(params) {
const { project } = this.state; const { project } = this.state;
return ProjectsAPI.readScheduleList(project.id, params); return ProjectsAPI.readSchedules(project.id, params);
} }
render() { render() {
@@ -241,7 +247,10 @@ class Project extends Component {
<Route <Route
path="/projects/:id/schedules" path="/projects/:id/schedules"
render={() => ( 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'; import { PageSection, Card } from '@patternfly/react-core';
function Schedules({ i18n }) { function Schedules({ i18n }) {
const loadScheduleOptions = () => {
return SchedulesAPI.readOptions();
};
const loadSchedules = params => { const loadSchedules = params => {
return SchedulesAPI.read(params); return SchedulesAPI.read(params);
}; };
@@ -24,7 +28,11 @@ function Schedules({ i18n }) {
<Route path="/schedules"> <Route path="/schedules">
<PageSection> <PageSection>
<Card> <Card>
<ScheduleList loadSchedules={loadSchedules} /> <ScheduleList
loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions}
hideAddButton
/>
</Card> </Card>
</PageSection> </PageSection>
</Route> </Route>

View File

@@ -29,6 +29,7 @@ class Template extends Component {
this.loadTemplate = this.loadTemplate.bind(this); this.loadTemplate = this.loadTemplate.bind(this);
this.loadTemplateAndRoles = this.loadTemplateAndRoles.bind(this); this.loadTemplateAndRoles = this.loadTemplateAndRoles.bind(this);
this.loadSchedules = this.loadSchedules.bind(this); this.loadSchedules = this.loadSchedules.bind(this);
this.loadScheduleOptions = this.loadScheduleOptions.bind(this);
} }
async componentDidMount() { async componentDidMount() {
@@ -83,9 +84,14 @@ class Template extends Component {
} }
} }
loadScheduleOptions() {
const { template } = this.state;
return JobTemplatesAPI.readScheduleOptions(template.id);
}
loadSchedules(params) { loadSchedules(params) {
const { template } = this.state; const { template } = this.state;
return JobTemplatesAPI.readScheduleList(template.id, params); return JobTemplatesAPI.readSchedules(template.id, params);
} }
render() { render() {
@@ -111,6 +117,13 @@ class Template extends Component {
}); });
} }
if (template) {
tabsArray.push({
name: i18n._(t`Schedules`),
link: `${match.url}/schedules`,
});
}
tabsArray.push( tabsArray.push(
{ {
name: i18n._(t`Completed Jobs`), 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) => { tabsArray.forEach((tab, n) => {
tab.id = n; tab.id = n;
}); });
@@ -225,7 +231,10 @@ class Template extends Component {
<Route <Route
path="/templates/:templateType/:id/schedules" path="/templates/:templateType/:id/schedules"
render={() => ( 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.loadTemplate = this.loadTemplate.bind(this);
this.loadSchedules = this.loadSchedules.bind(this); this.loadSchedules = this.loadSchedules.bind(this);
this.loadScheduleOptions = this.loadScheduleOptions.bind(this);
} }
async componentDidMount() { async componentDidMount() {
@@ -76,9 +77,14 @@ class WorkflowJobTemplate extends Component {
} }
} }
loadScheduleOptions() {
const { template } = this.state;
return WorkflowJobTemplatesAPI.readScheduleOptions(template.id);
}
loadSchedules(params) { loadSchedules(params) {
const { template } = this.state; const { template } = this.state;
return WorkflowJobTemplatesAPI.readScheduleList(template.id, params); return WorkflowJobTemplatesAPI.readSchedules(template.id, params);
} }
render() { render() {
@@ -199,7 +205,10 @@ class WorkflowJobTemplate extends Component {
<Route <Route
path="/templates/:templateType/:id/schedules" path="/templates/:templateType/:id/schedules"
render={() => ( render={() => (
<ScheduleList loadSchedules={this.loadSchedules} /> <ScheduleList
loadSchedules={this.loadSchedules}
loadScheduleOptions={this.loadScheduleOptions}
/>
)} )}
/> />
)} )}