mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 02:50:02 -03:30
Adds Add button to schedules list along with rbac restrictions
This commit is contained in:
parent
4fcd2c594c
commit
d33daeee91
12
awx/ui_next/src/api/mixins/Schedules.mixin.js
Normal file
12
awx/ui_next/src/api/mixins/Schedules.mixin.js
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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/`);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user