mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
updates based on pr feedback
run prettier update hasContentError to contentError in all the places function naming updates
This commit is contained in:
parent
357887417c
commit
bdfeb2cb9c
@ -1,48 +1,46 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import {
|
||||
encodeQueryString
|
||||
} from '@util/qs';
|
||||
import { encodeQueryString } from '@util/qs';
|
||||
|
||||
const defaultHttp = axios.create({
|
||||
xsrfCookieName: 'csrftoken',
|
||||
xsrfHeaderName: 'X-CSRFToken',
|
||||
paramsSerializer(params) {
|
||||
return encodeQueryString(params);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
class Base {
|
||||
constructor (http = defaultHttp, baseURL) {
|
||||
constructor(http = defaultHttp, baseURL) {
|
||||
this.http = http;
|
||||
this.baseUrl = baseURL;
|
||||
}
|
||||
|
||||
create (data) {
|
||||
create(data) {
|
||||
return this.http.post(this.baseUrl, data);
|
||||
}
|
||||
|
||||
destroy (id) {
|
||||
destroy(id) {
|
||||
return this.http.delete(`${this.baseUrl}${id}/`);
|
||||
}
|
||||
|
||||
read (params) {
|
||||
read(params) {
|
||||
return this.http.get(this.baseUrl, { params });
|
||||
}
|
||||
|
||||
readDetail (id) {
|
||||
readDetail(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/`);
|
||||
}
|
||||
|
||||
readOptions () {
|
||||
readOptions() {
|
||||
return this.http.options(this.baseUrl);
|
||||
}
|
||||
|
||||
replace (id, data) {
|
||||
replace(id, data) {
|
||||
return this.http.put(`${this.baseUrl}${id}/`, data);
|
||||
}
|
||||
|
||||
update (id, data) {
|
||||
update(id, data) {
|
||||
return this.http.patch(`${this.baseUrl}${id}/`, data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,14 +3,14 @@ import Base from './Base';
|
||||
describe('Base', () => {
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockBaseURL = '/api/v2/organizations/';
|
||||
const mockHttp = ({
|
||||
const mockHttp = {
|
||||
delete: jest.fn(createPromise),
|
||||
get: jest.fn(createPromise),
|
||||
options: jest.fn(createPromise),
|
||||
patch: jest.fn(createPromise),
|
||||
post: jest.fn(createPromise),
|
||||
put: jest.fn(createPromise)
|
||||
});
|
||||
put: jest.fn(createPromise),
|
||||
};
|
||||
|
||||
const BaseAPI = new Base(mockHttp, mockBaseURL);
|
||||
|
||||
@ -18,7 +18,7 @@ describe('Base', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('create calls http method with expected data', async (done) => {
|
||||
test('create calls http method with expected data', async done => {
|
||||
const data = { name: 'test ' };
|
||||
await BaseAPI.create(data);
|
||||
|
||||
@ -28,19 +28,21 @@ describe('Base', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
test('destroy calls http method with expected data', async (done) => {
|
||||
test('destroy calls http method with expected data', async done => {
|
||||
const resourceId = 1;
|
||||
await BaseAPI.destroy(resourceId);
|
||||
|
||||
expect(mockHttp.delete).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.delete.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
|
||||
expect(mockHttp.delete.mock.calls[0][0]).toEqual(
|
||||
`${mockBaseURL}${resourceId}/`
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('read calls http method with expected data', async (done) => {
|
||||
test('read calls http method with expected data', async done => {
|
||||
const testParams = { foo: 'bar' };
|
||||
const testParamsDuplicates = { foo: ['bar', 'baz']};
|
||||
const testParamsDuplicates = { foo: ['bar', 'baz'] };
|
||||
|
||||
await BaseAPI.read(testParams);
|
||||
await BaseAPI.read();
|
||||
@ -48,25 +50,29 @@ describe('Base', () => {
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(3);
|
||||
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[0][1]).toEqual({"params": {"foo": "bar"}});
|
||||
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: { foo: 'bar' } });
|
||||
expect(mockHttp.get.mock.calls[1][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[1][1]).toEqual({"params": undefined});
|
||||
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: undefined });
|
||||
expect(mockHttp.get.mock.calls[2][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[2][1]).toEqual({"params": {"foo": ["bar", "baz"]}});
|
||||
expect(mockHttp.get.mock.calls[2][1]).toEqual({
|
||||
params: { foo: ['bar', 'baz'] },
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
test('readDetail calls http method with expected data', async (done) => {
|
||||
test('readDetail calls http method with expected data', async done => {
|
||||
const resourceId = 1;
|
||||
|
||||
await BaseAPI.readDetail(resourceId);
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
|
||||
expect(mockHttp.get.mock.calls[0][0]).toEqual(
|
||||
`${mockBaseURL}${resourceId}/`
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
test('readOptions calls http method with expected data', async (done) => {
|
||||
test('readOptions calls http method with expected data', async done => {
|
||||
await BaseAPI.readOptions();
|
||||
|
||||
expect(mockHttp.options).toHaveBeenCalledTimes(1);
|
||||
@ -74,27 +80,31 @@ describe('Base', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
test('replace calls http method with expected data', async (done) => {
|
||||
test('replace calls http method with expected data', async done => {
|
||||
const resourceId = 1;
|
||||
const data = { name: 'test ' };
|
||||
|
||||
await BaseAPI.replace(resourceId, data);
|
||||
|
||||
expect(mockHttp.put).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.put.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
|
||||
expect(mockHttp.put.mock.calls[0][0]).toEqual(
|
||||
`${mockBaseURL}${resourceId}/`
|
||||
);
|
||||
expect(mockHttp.put.mock.calls[0][1]).toEqual(data);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('update calls http method with expected data', async (done) => {
|
||||
test('update calls http method with expected data', async done => {
|
||||
const resourceId = 1;
|
||||
const data = { name: 'test ' };
|
||||
|
||||
await BaseAPI.update(resourceId, data);
|
||||
|
||||
expect(mockHttp.patch).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttp.patch.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
|
||||
expect(mockHttp.patch.mock.calls[0][0]).toEqual(
|
||||
`${mockBaseURL}${resourceId}/`
|
||||
);
|
||||
expect(mockHttp.patch.mock.calls[0][1]).toEqual(data);
|
||||
|
||||
done();
|
||||
|
||||
@ -1,15 +1,24 @@
|
||||
const InstanceGroupsMixin = (parent) => class extends parent {
|
||||
readInstanceGroups (resourceId, params) {
|
||||
return this.http.get(`${this.baseUrl}${resourceId}/instance_groups/`, params);
|
||||
}
|
||||
const InstanceGroupsMixin = parent =>
|
||||
class extends parent {
|
||||
readInstanceGroups(resourceId, params) {
|
||||
return this.http.get(
|
||||
`${this.baseUrl}${resourceId}/instance_groups/`,
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
associateInstanceGroup (resourceId, instanceGroupId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId });
|
||||
}
|
||||
associateInstanceGroup(resourceId, instanceGroupId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, {
|
||||
id: instanceGroupId,
|
||||
});
|
||||
}
|
||||
|
||||
disassociateInstanceGroup (resourceId, instanceGroupId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId, disassociate: true });
|
||||
}
|
||||
};
|
||||
disassociateInstanceGroup(resourceId, instanceGroupId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, {
|
||||
id: instanceGroupId,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default InstanceGroupsMixin;
|
||||
|
||||
@ -1,65 +1,106 @@
|
||||
const NotificationsMixin = (parent) => class extends parent {
|
||||
readOptionsNotificationTemplates(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/notification_templates/`);
|
||||
}
|
||||
|
||||
readNotificationTemplates (id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/notification_templates/`, params);
|
||||
}
|
||||
|
||||
readNotificationTemplatesSuccess (id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/notification_templates_success/`, params);
|
||||
}
|
||||
|
||||
readNotificationTemplatesError (id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/notification_templates_error/`, params);
|
||||
}
|
||||
|
||||
associateNotificationTemplatesSuccess (resourceId, notificationId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_success/`, { id: notificationId });
|
||||
}
|
||||
|
||||
disassociateNotificationTemplatesSuccess (resourceId, notificationId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_success/`, { id: notificationId, disassociate: true });
|
||||
}
|
||||
|
||||
associateNotificationTemplatesError (resourceId, notificationId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_error/`, { id: notificationId });
|
||||
}
|
||||
|
||||
disassociateNotificationTemplatesError (resourceId, notificationId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_error/`, { id: notificationId, disassociate: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a helper method meant to simplify setting the "on" or "off" status of
|
||||
* a related notification.
|
||||
*
|
||||
* @param[resourceId] - id of the base resource
|
||||
* @param[notificationId] - id of the notification
|
||||
* @param[notificationType] - the type of notification, options are "success" and "error"
|
||||
* @param[associationState] - Boolean for associating or disassociating, options are true or false
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
updateNotificationTemplateAssociation (resourceId, notificationId, notificationType, associationState) {
|
||||
if (notificationType === 'success' && associationState === true) {
|
||||
return this.associateNotificationTemplatesSuccess(resourceId, notificationId);
|
||||
const NotificationsMixin = parent =>
|
||||
class extends parent {
|
||||
readOptionsNotificationTemplates(id) {
|
||||
return this.http.options(`${this.baseUrl}${id}/notification_templates/`);
|
||||
}
|
||||
|
||||
if (notificationType === 'success' && associationState === false) {
|
||||
return this.disassociateNotificationTemplatesSuccess(resourceId, notificationId);
|
||||
readNotificationTemplates(id, params) {
|
||||
return this.http.get(
|
||||
`${this.baseUrl}${id}/notification_templates/`,
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'error' && associationState === true) {
|
||||
return this.associateNotificationTemplatesError(resourceId, notificationId);
|
||||
readNotificationTemplatesSuccess(id, params) {
|
||||
return this.http.get(
|
||||
`${this.baseUrl}${id}/notification_templates_success/`,
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'error' && associationState === false) {
|
||||
return this.disassociateNotificationTemplatesError(resourceId, notificationId);
|
||||
readNotificationTemplatesError(id, params) {
|
||||
return this.http.get(
|
||||
`${this.baseUrl}${id}/notification_templates_error/`,
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported notificationType, associationState combination: ${notificationType}, ${associationState}`);
|
||||
}
|
||||
};
|
||||
associateNotificationTemplatesSuccess(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_success/`,
|
||||
{ id: notificationId }
|
||||
);
|
||||
}
|
||||
|
||||
disassociateNotificationTemplatesSuccess(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_success/`,
|
||||
{ id: notificationId, disassociate: true }
|
||||
);
|
||||
}
|
||||
|
||||
associateNotificationTemplatesError(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_error/`,
|
||||
{ id: notificationId }
|
||||
);
|
||||
}
|
||||
|
||||
disassociateNotificationTemplatesError(resourceId, notificationId) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${resourceId}/notification_templates_error/`,
|
||||
{ id: notificationId, disassociate: true }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a helper method meant to simplify setting the "on" or "off" status of
|
||||
* a related notification.
|
||||
*
|
||||
* @param[resourceId] - id of the base resource
|
||||
* @param[notificationId] - id of the notification
|
||||
* @param[notificationType] - the type of notification, options are "success" and "error"
|
||||
* @param[associationState] - Boolean for associating or disassociating, options are true or false
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
updateNotificationTemplateAssociation(
|
||||
resourceId,
|
||||
notificationId,
|
||||
notificationType,
|
||||
associationState
|
||||
) {
|
||||
if (notificationType === 'success' && associationState === true) {
|
||||
return this.associateNotificationTemplatesSuccess(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'success' && associationState === false) {
|
||||
return this.disassociateNotificationTemplatesSuccess(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'error' && associationState === true) {
|
||||
return this.associateNotificationTemplatesError(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationType === 'error' && associationState === false) {
|
||||
return this.disassociateNotificationTemplatesError(
|
||||
resourceId,
|
||||
notificationId
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unsupported notificationType, associationState combination: ${notificationType}, ${associationState}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default NotificationsMixin;
|
||||
|
||||
@ -3,16 +3,16 @@ import NotificationsMixin from '../mixins/Notifications.mixin';
|
||||
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
|
||||
|
||||
class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
|
||||
constructor (http) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/organizations/';
|
||||
}
|
||||
|
||||
readAccessList (id, params) {
|
||||
readAccessList(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/access_list/`, { params });
|
||||
}
|
||||
|
||||
readTeams (id, params) {
|
||||
readTeams(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/teams/`, { params });
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { describeNotificationMixin } from '../../../testUtils/apiReusable';
|
||||
describe('OrganizationsAPI', () => {
|
||||
const orgId = 1;
|
||||
const createPromise = () => Promise.resolve();
|
||||
const mockHttp = ({ get: jest.fn(createPromise) });
|
||||
const mockHttp = { get: jest.fn(createPromise) };
|
||||
|
||||
const OrganizationsAPI = new Organizations(mockHttp);
|
||||
|
||||
@ -12,9 +12,9 @@ describe('OrganizationsAPI', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('read access list calls get with expected params', async (done) => {
|
||||
test('read access list calls get with expected params', async done => {
|
||||
const testParams = { foo: 'bar' };
|
||||
const testParamsDuplicates = { foo: ['bar', 'baz']};
|
||||
const testParamsDuplicates = { foo: ['bar', 'baz'] };
|
||||
|
||||
const mockBaseURL = `/api/v2/organizations/${orgId}/access_list/`;
|
||||
|
||||
@ -24,17 +24,19 @@ describe('OrganizationsAPI', () => {
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(3);
|
||||
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[0][1]).toEqual({"params": undefined});
|
||||
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: undefined });
|
||||
expect(mockHttp.get.mock.calls[1][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[1][1]).toEqual({"params": {"foo": "bar"}});
|
||||
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: { foo: 'bar' } });
|
||||
expect(mockHttp.get.mock.calls[2][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[2][1]).toEqual({"params": {"foo": ["bar", "baz"]}});
|
||||
expect(mockHttp.get.mock.calls[2][1]).toEqual({
|
||||
params: { foo: ['bar', 'baz'] },
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
test('read teams calls get with expected params', async (done) => {
|
||||
test('read teams calls get with expected params', async done => {
|
||||
const testParams = { foo: 'bar' };
|
||||
const testParamsDuplicates = { foo: ['bar', 'baz']};
|
||||
const testParamsDuplicates = { foo: ['bar', 'baz'] };
|
||||
|
||||
const mockBaseURL = `/api/v2/organizations/${orgId}/teams/`;
|
||||
|
||||
@ -44,11 +46,13 @@ describe('OrganizationsAPI', () => {
|
||||
|
||||
expect(mockHttp.get).toHaveBeenCalledTimes(3);
|
||||
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[0][1]).toEqual({"params": undefined});
|
||||
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: undefined });
|
||||
expect(mockHttp.get.mock.calls[1][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[1][1]).toEqual({"params": {"foo": "bar"}});
|
||||
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: { foo: 'bar' } });
|
||||
expect(mockHttp.get.mock.calls[2][0]).toEqual(`${mockBaseURL}`);
|
||||
expect(mockHttp.get.mock.calls[2][1]).toEqual({"params": {"foo": ["bar", "baz"]}});
|
||||
expect(mockHttp.get.mock.calls[2][1]).toEqual({
|
||||
params: { foo: ['bar', 'baz'] },
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,14 +8,13 @@ import SelectRoleStep from './SelectRoleStep';
|
||||
import SelectableCard from './SelectableCard';
|
||||
import { TeamsAPI, UsersAPI } from '../../api';
|
||||
|
||||
const readUsers = async (queryParams) => UsersAPI.read(
|
||||
Object.assign(queryParams, { is_superuser: false })
|
||||
);
|
||||
const readUsers = async queryParams =>
|
||||
UsersAPI.read(Object.assign(queryParams, { is_superuser: false }));
|
||||
|
||||
const readTeams = async (queryParams) => TeamsAPI.read(queryParams);
|
||||
const readTeams = async queryParams => TeamsAPI.read(queryParams);
|
||||
|
||||
class AddResourceRole extends React.Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
@ -25,67 +24,69 @@ class AddResourceRole extends React.Component {
|
||||
currentStepId: 1,
|
||||
};
|
||||
|
||||
this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind(this);
|
||||
this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind(
|
||||
this
|
||||
);
|
||||
this.handleResourceSelect = this.handleResourceSelect.bind(this);
|
||||
this.handleRoleCheckboxClick = this.handleRoleCheckboxClick.bind(this);
|
||||
this.handleWizardNext = this.handleWizardNext.bind(this);
|
||||
this.handleWizardSave = this.handleWizardSave.bind(this);
|
||||
}
|
||||
|
||||
handleResourceCheckboxClick (user) {
|
||||
handleResourceCheckboxClick(user) {
|
||||
const { selectedResourceRows } = this.state;
|
||||
|
||||
const selectedIndex = selectedResourceRows
|
||||
.findIndex(selectedRow => selectedRow.id === user.id);
|
||||
const selectedIndex = selectedResourceRows.findIndex(
|
||||
selectedRow => selectedRow.id === user.id
|
||||
);
|
||||
|
||||
if (selectedIndex > -1) {
|
||||
selectedResourceRows.splice(selectedIndex, 1);
|
||||
this.setState({ selectedResourceRows });
|
||||
} else {
|
||||
this.setState(prevState => ({
|
||||
selectedResourceRows: [...prevState.selectedResourceRows, user]
|
||||
selectedResourceRows: [...prevState.selectedResourceRows, user],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleRoleCheckboxClick (role) {
|
||||
handleRoleCheckboxClick(role) {
|
||||
const { selectedRoleRows } = this.state;
|
||||
|
||||
const selectedIndex = selectedRoleRows
|
||||
.findIndex(selectedRow => selectedRow.id === role.id);
|
||||
const selectedIndex = selectedRoleRows.findIndex(
|
||||
selectedRow => selectedRow.id === role.id
|
||||
);
|
||||
|
||||
if (selectedIndex > -1) {
|
||||
selectedRoleRows.splice(selectedIndex, 1);
|
||||
this.setState({ selectedRoleRows });
|
||||
} else {
|
||||
this.setState(prevState => ({
|
||||
selectedRoleRows: [...prevState.selectedRoleRows, role]
|
||||
selectedRoleRows: [...prevState.selectedRoleRows, role],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleResourceSelect (resourceType) {
|
||||
handleResourceSelect(resourceType) {
|
||||
this.setState({
|
||||
selectedResource: resourceType,
|
||||
selectedResourceRows: [],
|
||||
selectedRoleRows: []
|
||||
selectedRoleRows: [],
|
||||
});
|
||||
}
|
||||
|
||||
handleWizardNext (step) {
|
||||
handleWizardNext(step) {
|
||||
this.setState({
|
||||
currentStepId: step.id,
|
||||
});
|
||||
}
|
||||
|
||||
async handleWizardSave () {
|
||||
const {
|
||||
onSave
|
||||
} = this.props;
|
||||
async handleWizardSave() {
|
||||
const { onSave } = this.props;
|
||||
const {
|
||||
selectedResourceRows,
|
||||
selectedRoleRows,
|
||||
selectedResource
|
||||
selectedResource,
|
||||
} = this.state;
|
||||
|
||||
try {
|
||||
@ -95,11 +96,17 @@ class AddResourceRole extends React.Component {
|
||||
for (let j = 0; j < selectedRoleRows.length; j++) {
|
||||
if (selectedResource === 'users') {
|
||||
roleRequests.push(
|
||||
UsersAPI.associateRole(selectedResourceRows[i].id, selectedRoleRows[j].id)
|
||||
UsersAPI.associateRole(
|
||||
selectedResourceRows[i].id,
|
||||
selectedRoleRows[j].id
|
||||
)
|
||||
);
|
||||
} else if (selectedResource === 'teams') {
|
||||
roleRequests.push(
|
||||
TeamsAPI.associateRole(selectedResourceRows[i].id, selectedRoleRows[j].id)
|
||||
TeamsAPI.associateRole(
|
||||
selectedResourceRows[i].id,
|
||||
selectedRoleRows[j].id
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -112,25 +119,31 @@ class AddResourceRole extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
selectedResource,
|
||||
selectedResourceRows,
|
||||
selectedRoleRows,
|
||||
currentStepId,
|
||||
} = this.state;
|
||||
const {
|
||||
onClose,
|
||||
roles,
|
||||
i18n
|
||||
} = this.props;
|
||||
const { onClose, roles, i18n } = this.props;
|
||||
|
||||
const userColumns = [
|
||||
{ name: i18n._(t`Username`), key: 'username', isSortable: true, isSearchable: true }
|
||||
{
|
||||
name: i18n._(t`Username`),
|
||||
key: 'username',
|
||||
isSortable: true,
|
||||
isSearchable: true,
|
||||
},
|
||||
];
|
||||
|
||||
const teamColumns = [
|
||||
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true }
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isSortable: true,
|
||||
isSearchable: true,
|
||||
},
|
||||
];
|
||||
|
||||
let wizardTitle = '';
|
||||
@ -164,7 +177,7 @@ class AddResourceRole extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableNext: selectedResource !== null
|
||||
enableNext: selectedResource !== null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@ -195,7 +208,7 @@ class AddResourceRole extends React.Component {
|
||||
)}
|
||||
</Fragment>
|
||||
),
|
||||
enableNext: selectedResourceRows.length > 0
|
||||
enableNext: selectedResourceRows.length > 0,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@ -211,8 +224,8 @@ class AddResourceRole extends React.Component {
|
||||
/>
|
||||
),
|
||||
nextButtonText: i18n._(t`Save`),
|
||||
enableNext: selectedRoleRows.length > 0
|
||||
}
|
||||
enableNext: selectedRoleRows.length > 0,
|
||||
},
|
||||
];
|
||||
|
||||
const currentStep = steps.find(step => step.id === currentStepId);
|
||||
@ -236,11 +249,11 @@ class AddResourceRole extends React.Component {
|
||||
AddResourceRole.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
roles: PropTypes.shape()
|
||||
roles: PropTypes.shape(),
|
||||
};
|
||||
|
||||
AddResourceRole.defaultProps = {
|
||||
roles: {}
|
||||
roles: {},
|
||||
};
|
||||
|
||||
export { AddResourceRole as _AddResourceRole };
|
||||
|
||||
@ -40,10 +40,7 @@ class SelectResourceStep extends React.Component {
|
||||
|
||||
async readResourceList() {
|
||||
const { onSearch, location } = this.props;
|
||||
const queryParams = parseQueryString(
|
||||
this.qsConfig,
|
||||
location.search
|
||||
);
|
||||
const queryParams = parseQueryString(this.qsConfig, location.search);
|
||||
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
|
||||
@ -7,7 +7,7 @@ import SelectResourceStep from './SelectResourceStep';
|
||||
|
||||
describe('<SelectResourceStep />', () => {
|
||||
const columns = [
|
||||
{ name: 'Username', key: 'username', isSortable: true, isSearchable: true }
|
||||
{ name: 'Username', key: 'username', isSortable: true, isSearchable: true },
|
||||
];
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
@ -30,9 +30,9 @@ describe('<SelectResourceStep />', () => {
|
||||
count: 2,
|
||||
results: [
|
||||
{ id: 1, username: 'foo', url: 'item/1' },
|
||||
{ id: 2, username: 'bar', url: 'item/2' }
|
||||
]
|
||||
}
|
||||
{ id: 2, username: 'bar', url: 'item/2' },
|
||||
],
|
||||
},
|
||||
});
|
||||
mountWithContexts(
|
||||
<SelectResourceStep
|
||||
@ -46,25 +46,25 @@ describe('<SelectResourceStep />', () => {
|
||||
expect(handleSearch).toHaveBeenCalledWith({
|
||||
order_by: 'username',
|
||||
page: 1,
|
||||
page_size: 5
|
||||
page_size: 5,
|
||||
});
|
||||
});
|
||||
|
||||
test('readResourceList properly adds rows to state', async () => {
|
||||
const selectedResourceRows = [
|
||||
{ id: 1, username: 'foo', url: 'item/1' }
|
||||
];
|
||||
const selectedResourceRows = [{ id: 1, username: 'foo', url: 'item/1' }];
|
||||
const handleSearch = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
count: 2,
|
||||
results: [
|
||||
{ id: 1, username: 'foo', url: 'item/1' },
|
||||
{ id: 2, username: 'bar', url: 'item/2' }
|
||||
]
|
||||
}
|
||||
{ id: 2, username: 'bar', url: 'item/2' },
|
||||
],
|
||||
},
|
||||
});
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/organizations/1/access?resource.page=1&resource.order_by=-username'],
|
||||
initialEntries: [
|
||||
'/organizations/1/access?resource.page=1&resource.order_by=-username',
|
||||
],
|
||||
});
|
||||
const wrapper = await mountWithContexts(
|
||||
<SelectResourceStep
|
||||
@ -74,7 +74,10 @@ describe('<SelectResourceStep />', () => {
|
||||
onSearch={handleSearch}
|
||||
selectedResourceRows={selectedResourceRows}
|
||||
sortedColumnKey="username"
|
||||
/>, { context: { router: { history, route: { location: history.location } } } }
|
||||
/>,
|
||||
{
|
||||
context: { router: { history, route: { location: history.location } } },
|
||||
}
|
||||
).find('SelectResourceStep');
|
||||
await wrapper.instance().readResourceList();
|
||||
expect(handleSearch).toHaveBeenCalledWith({
|
||||
@ -84,7 +87,7 @@ describe('<SelectResourceStep />', () => {
|
||||
});
|
||||
expect(wrapper.state('resources')).toEqual([
|
||||
{ id: 1, username: 'foo', url: 'item/1' },
|
||||
{ id: 2, username: 'bar', url: 'item/2' }
|
||||
{ id: 2, username: 'bar', url: 'item/2' },
|
||||
]);
|
||||
});
|
||||
|
||||
@ -94,8 +97,8 @@ describe('<SelectResourceStep />', () => {
|
||||
count: 2,
|
||||
results: [
|
||||
{ id: 1, username: 'foo', url: 'item/1' },
|
||||
{ id: 2, username: 'bar', url: 'item/2' }
|
||||
]
|
||||
{ id: 2, username: 'bar', url: 'item/2' },
|
||||
],
|
||||
};
|
||||
const wrapper = mountWithContexts(
|
||||
<SelectResourceStep
|
||||
@ -111,7 +114,9 @@ describe('<SelectResourceStep />', () => {
|
||||
wrapper.update();
|
||||
const checkboxListItemWrapper = wrapper.find('CheckboxListItem');
|
||||
expect(checkboxListItemWrapper.length).toBe(2);
|
||||
checkboxListItemWrapper.first().find('input[type="checkbox"]')
|
||||
checkboxListItemWrapper
|
||||
.first()
|
||||
.find('input[type="checkbox"]')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
expect(handleRowClick).toHaveBeenCalledWith(data.results[0]);
|
||||
});
|
||||
|
||||
@ -3,14 +3,21 @@ import { number, bool } from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import Chip from './Chip';
|
||||
|
||||
const ChipGroup = ({ children, className, showOverflowAfter, displayAll, ...props }) => {
|
||||
const ChipGroup = ({
|
||||
children,
|
||||
className,
|
||||
showOverflowAfter,
|
||||
displayAll,
|
||||
...props
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(!showOverflowAfter);
|
||||
const toggleIsOpen = () => setIsExpanded(!isExpanded);
|
||||
|
||||
const mappedChildren = React.Children.map(children, c => (
|
||||
const mappedChildren = React.Children.map(children, c =>
|
||||
React.cloneElement(c, { component: 'li' })
|
||||
));
|
||||
const showOverflowToggle = showOverflowAfter && children.length > showOverflowAfter;
|
||||
);
|
||||
const showOverflowToggle =
|
||||
showOverflowAfter && children.length > showOverflowAfter;
|
||||
const numToShow = isExpanded
|
||||
? children.length
|
||||
: Math.min(showOverflowAfter, children.length);
|
||||
@ -30,17 +37,17 @@ const ChipGroup = ({ children, className, showOverflowAfter, displayAll, ...prop
|
||||
};
|
||||
ChipGroup.propTypes = {
|
||||
showOverflowAfter: number,
|
||||
displayAll: bool
|
||||
displayAll: bool,
|
||||
};
|
||||
ChipGroup.defaultProps = {
|
||||
showOverflowAfter: null,
|
||||
displayAll: false
|
||||
displayAll: false,
|
||||
};
|
||||
|
||||
export default styled(ChipGroup)`
|
||||
--pf-c-chip-group--c-chip--MarginRight: 10px;
|
||||
--pf-c-chip-group--c-chip--MarginBottom: 10px;
|
||||
|
||||
|
||||
> .pf-c-chip.pf-m-overflow button {
|
||||
padding: 3px 8px;
|
||||
}
|
||||
|
||||
@ -13,7 +13,9 @@ describe('<DataListToolbar />', () => {
|
||||
});
|
||||
|
||||
test('it triggers the expected callbacks', () => {
|
||||
const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }];
|
||||
const columns = [
|
||||
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
|
||||
];
|
||||
|
||||
const search = 'button[aria-label="Search submit button"]';
|
||||
const searchTextInput = 'input[aria-label="Search text input"]';
|
||||
@ -59,14 +61,16 @@ describe('<DataListToolbar />', () => {
|
||||
test('dropdown items sortable/searchable columns work', () => {
|
||||
const sortDropdownToggleSelector = 'button[id="awx-sort"]';
|
||||
const searchDropdownToggleSelector = 'button[id="awx-search"]';
|
||||
const sortDropdownMenuItems = 'DropdownMenu > ul[aria-labelledby="awx-sort"]';
|
||||
const searchDropdownMenuItems = 'DropdownMenu > ul[aria-labelledby="awx-search"]';
|
||||
const sortDropdownMenuItems =
|
||||
'DropdownMenu > ul[aria-labelledby="awx-sort"]';
|
||||
const searchDropdownMenuItems =
|
||||
'DropdownMenu > ul[aria-labelledby="awx-search"]';
|
||||
|
||||
const multipleColumns = [
|
||||
{ name: 'Foo', key: 'foo', isSortable: true, isSearchable: true },
|
||||
{ name: 'Bar', key: 'bar', isSortable: true, isSearchable: true },
|
||||
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
||||
{ name: 'Baz', key: 'baz' }
|
||||
{ name: 'Baz', key: 'baz' },
|
||||
];
|
||||
|
||||
const onSort = jest.fn();
|
||||
@ -103,12 +107,16 @@ describe('<DataListToolbar />', () => {
|
||||
);
|
||||
toolbar.update();
|
||||
|
||||
const sortDropdownToggleDescending = toolbar.find(sortDropdownToggleSelector);
|
||||
const sortDropdownToggleDescending = toolbar.find(
|
||||
sortDropdownToggleSelector
|
||||
);
|
||||
expect(sortDropdownToggleDescending.length).toBe(1);
|
||||
sortDropdownToggleDescending.simulate('click');
|
||||
toolbar.update();
|
||||
|
||||
const sortDropdownItemsDescending = toolbar.find(sortDropdownMenuItems).children();
|
||||
const sortDropdownItemsDescending = toolbar
|
||||
.find(sortDropdownMenuItems)
|
||||
.children();
|
||||
expect(sortDropdownItemsDescending.length).toBe(2);
|
||||
sortDropdownToggleDescending.simulate('click'); // toggle close the sort dropdown
|
||||
|
||||
@ -134,8 +142,12 @@ describe('<DataListToolbar />', () => {
|
||||
const downAlphaIconSelector = 'SortAlphaDownIcon';
|
||||
const upAlphaIconSelector = 'SortAlphaUpIcon';
|
||||
|
||||
const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }];
|
||||
const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }];
|
||||
const numericColumns = [
|
||||
{ name: 'ID', key: 'id', isSortable: true, isNumeric: true },
|
||||
];
|
||||
const alphaColumns = [
|
||||
{ name: 'Name', key: 'name', isSortable: true, isNumeric: false },
|
||||
];
|
||||
|
||||
toolbar = mountWithContexts(
|
||||
<DataListToolbar
|
||||
@ -183,7 +195,9 @@ describe('<DataListToolbar />', () => {
|
||||
});
|
||||
|
||||
test('should render additionalControls', () => {
|
||||
const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }];
|
||||
const columns = [
|
||||
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
|
||||
];
|
||||
const onSearch = jest.fn();
|
||||
const onSort = jest.fn();
|
||||
const onSelectAll = jest.fn();
|
||||
@ -194,7 +208,11 @@ describe('<DataListToolbar />', () => {
|
||||
onSearch={onSearch}
|
||||
onSort={onSort}
|
||||
onSelectAll={onSelectAll}
|
||||
additionalControls={[<button key="1" id="test" type="button">click</button>]}
|
||||
additionalControls={[
|
||||
<button key="1" id="test" type="button">
|
||||
click
|
||||
</button>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@ -9,19 +9,19 @@ import { ChipGroup, Chip } from '@components/Chip';
|
||||
import VerticalSeparator from '@components/VerticalSeparator';
|
||||
|
||||
const FilterTagsRow = styled.div`
|
||||
display: flex;
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid #d2d2d2;
|
||||
font-size: 14px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid #d2d2d2;
|
||||
font-size: 14px;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ResultCount = styled.span`
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const FilterLabel = styled.span`
|
||||
padding-right: 20px;
|
||||
padding-right: 20px;
|
||||
`;
|
||||
|
||||
// remove non-default query params so they don't show up as filter tags
|
||||
@ -30,50 +30,59 @@ const filterDefaultParams = (paramsArr, config) => {
|
||||
return paramsArr.filter(key => defaultParamsKeys.indexOf(key) === -1);
|
||||
};
|
||||
|
||||
const FilterTags = ({ i18n, itemCount, qsConfig, location, onRemove, onRemoveAll }) => {
|
||||
const FilterTags = ({
|
||||
i18n,
|
||||
itemCount,
|
||||
qsConfig,
|
||||
location,
|
||||
onRemove,
|
||||
onRemoveAll,
|
||||
}) => {
|
||||
const queryParams = parseQueryString(qsConfig, location.search);
|
||||
const queryParamsArr = [];
|
||||
const displayAll = true;
|
||||
const nonDefaultParams = filterDefaultParams(Object.keys(queryParams), qsConfig);
|
||||
nonDefaultParams
|
||||
.forEach(key => {
|
||||
if (Array.isArray(queryParams[key])) {
|
||||
queryParams[key].forEach(val => queryParamsArr.push({ key, value: val }));
|
||||
} else {
|
||||
queryParamsArr.push({ key, value: queryParams[key] });
|
||||
}
|
||||
});
|
||||
const nonDefaultParams = filterDefaultParams(
|
||||
Object.keys(queryParams),
|
||||
qsConfig
|
||||
);
|
||||
nonDefaultParams.forEach(key => {
|
||||
if (Array.isArray(queryParams[key])) {
|
||||
queryParams[key].forEach(val => queryParamsArr.push({ key, value: val }));
|
||||
} else {
|
||||
queryParamsArr.push({ key, value: queryParams[key] });
|
||||
}
|
||||
});
|
||||
|
||||
return (queryParamsArr.length > 0) && (
|
||||
<FilterTagsRow>
|
||||
<ResultCount>
|
||||
{`${itemCount} results`}
|
||||
</ResultCount>
|
||||
<VerticalSeparator />
|
||||
<FilterLabel>{i18n._(t`Active Filters:`)}</FilterLabel>
|
||||
<ChipGroup displayAll={displayAll}>
|
||||
{queryParamsArr.map(({ key, value }) => (
|
||||
<Chip
|
||||
className="searchTagChip"
|
||||
key={`${key}__${value}`}
|
||||
isReadOnly={false}
|
||||
onClick={() => onRemove(key, value)}
|
||||
>
|
||||
{value}
|
||||
</Chip>
|
||||
))}
|
||||
<div className="pf-c-chip pf-m-overflow">
|
||||
<Button
|
||||
variant="plain"
|
||||
type="button"
|
||||
aria-label={i18n._(t`Clear all search filters`)}
|
||||
onClick={onRemoveAll}
|
||||
>
|
||||
<span className="pf-c-chip__text">{i18n._(t`Clear all`)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</ChipGroup>
|
||||
</FilterTagsRow>
|
||||
return (
|
||||
queryParamsArr.length > 0 && (
|
||||
<FilterTagsRow>
|
||||
<ResultCount>{`${itemCount} results`}</ResultCount>
|
||||
<VerticalSeparator />
|
||||
<FilterLabel>{i18n._(t`Active Filters:`)}</FilterLabel>
|
||||
<ChipGroup displayAll={displayAll}>
|
||||
{queryParamsArr.map(({ key, value }) => (
|
||||
<Chip
|
||||
className="searchTagChip"
|
||||
key={`${key}__${value}`}
|
||||
isReadOnly={false}
|
||||
onClick={() => onRemove(key, value)}
|
||||
>
|
||||
{value}
|
||||
</Chip>
|
||||
))}
|
||||
<div className="pf-c-chip pf-m-overflow">
|
||||
<Button
|
||||
variant="plain"
|
||||
type="button"
|
||||
aria-label={i18n._(t`Clear all search filters`)}
|
||||
onClick={onRemoveAll}
|
||||
>
|
||||
<span className="pf-c-chip__text">{i18n._(t`Clear all`)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</ChipGroup>
|
||||
</FilterTagsRow>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -26,14 +26,19 @@ describe('<ExpandCollapse />', () => {
|
||||
|
||||
test('renders non-default param tags based on location history', () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/foo?item.page=1&item.page_size=2&item.foo=bar&item.baz=bust'],
|
||||
initialEntries: [
|
||||
'/foo?item.page=1&item.page_size=2&item.foo=bar&item.baz=bust',
|
||||
],
|
||||
});
|
||||
const wrapper = mountWithContexts(
|
||||
<FilterTags
|
||||
qsConfig={qsConfig}
|
||||
onRemove={onRemoveFn}
|
||||
onRemoveAll={onRemoveAllFn}
|
||||
/>, { context: { router: { history, route: { location: history.location } } } }
|
||||
/>,
|
||||
{
|
||||
context: { router: { history, route: { location: history.location } } },
|
||||
}
|
||||
);
|
||||
const chips = wrapper.find('.pf-c-chip.searchTagChip');
|
||||
expect(chips.length).toBe(2);
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
encodeNonDefaultQueryString,
|
||||
parseQueryString,
|
||||
addParams,
|
||||
removeParams
|
||||
removeParams,
|
||||
} from '@util/qs';
|
||||
import { QSConfig } from '@types';
|
||||
|
||||
@ -21,12 +21,12 @@ const EmptyStateControlsWrapper = styled.div`
|
||||
margin-bottom: 20px;
|
||||
justify-content: flex-end;
|
||||
|
||||
& > :not(:first-child) {
|
||||
& > :not(:first-child) {
|
||||
margin-left: 20px;
|
||||
}
|
||||
`;
|
||||
class ListHeader extends React.Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleSearch = this.handleSearch.bind(this);
|
||||
@ -35,7 +35,7 @@ class ListHeader extends React.Component {
|
||||
this.handleRemoveAll = this.handleRemoveAll.bind(this);
|
||||
}
|
||||
|
||||
getSortOrder () {
|
||||
getSortOrder() {
|
||||
const { qsConfig, location } = this.props;
|
||||
const queryParams = parseQueryString(qsConfig, location.search);
|
||||
if (queryParams.order_by && queryParams.order_by.startsWith('-')) {
|
||||
@ -44,45 +44,47 @@ class ListHeader extends React.Component {
|
||||
return [queryParams.order_by, 'ascending'];
|
||||
}
|
||||
|
||||
handleSearch (key, value) {
|
||||
handleSearch(key, value) {
|
||||
const { history, qsConfig } = this.props;
|
||||
const { search } = history.location;
|
||||
this.pushHistoryState(addParams(qsConfig, search, { [key]: value }));
|
||||
}
|
||||
|
||||
handleRemove (key, value) {
|
||||
handleRemove(key, value) {
|
||||
const { history, qsConfig } = this.props;
|
||||
const { search } = history.location;
|
||||
this.pushHistoryState(removeParams(qsConfig, search, { [key]: value }));
|
||||
}
|
||||
|
||||
handleRemoveAll () {
|
||||
handleRemoveAll() {
|
||||
this.pushHistoryState(null);
|
||||
}
|
||||
|
||||
handleSort (key, order) {
|
||||
handleSort(key, order) {
|
||||
const { history, qsConfig } = this.props;
|
||||
const { search } = history.location;
|
||||
this.pushHistoryState(addParams(qsConfig, search, {
|
||||
order_by: order === 'ascending' ? key : `-${key}`,
|
||||
page: null,
|
||||
}));
|
||||
this.pushHistoryState(
|
||||
addParams(qsConfig, search, {
|
||||
order_by: order === 'ascending' ? key : `-${key}`,
|
||||
page: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
pushHistoryState (params) {
|
||||
pushHistoryState(params) {
|
||||
const { history, qsConfig } = this.props;
|
||||
const { pathname } = history.location;
|
||||
const encodedParams = encodeNonDefaultQueryString(qsConfig, params);
|
||||
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
emptyStateControls,
|
||||
itemCount,
|
||||
columns,
|
||||
renderToolbar,
|
||||
qsConfig
|
||||
qsConfig,
|
||||
} = this.props;
|
||||
const [orderBy, sortOrder] = this.getSortOrder();
|
||||
return (
|
||||
@ -124,17 +126,19 @@ class ListHeader extends React.Component {
|
||||
ListHeader.propTypes = {
|
||||
itemCount: PropTypes.number.isRequired,
|
||||
qsConfig: QSConfig.isRequired,
|
||||
columns: arrayOf(shape({
|
||||
name: string.isRequired,
|
||||
key: string.isRequired,
|
||||
isSortable: bool,
|
||||
isSearchable: bool
|
||||
})).isRequired,
|
||||
columns: arrayOf(
|
||||
shape({
|
||||
name: string.isRequired,
|
||||
key: string.isRequired,
|
||||
isSortable: bool,
|
||||
isSearchable: bool,
|
||||
})
|
||||
).isRequired,
|
||||
renderToolbar: PropTypes.func,
|
||||
};
|
||||
|
||||
ListHeader.defaultProps = {
|
||||
renderToolbar: (props) => (<DataListToolbar {...props} />),
|
||||
renderToolbar: props => <DataListToolbar {...props} />,
|
||||
};
|
||||
|
||||
export default withRouter(ListHeader);
|
||||
|
||||
@ -17,7 +17,9 @@ describe('ListHeader', () => {
|
||||
<ListHeader
|
||||
itemCount={50}
|
||||
qsConfig={qsConfig}
|
||||
columns={[{ name: 'foo', key: 'foo', isSearchable: true, isSortable: true }]}
|
||||
columns={[
|
||||
{ name: 'foo', key: 'foo', isSearchable: true, isSortable: true },
|
||||
]}
|
||||
renderToolbar={renderToolbarFn}
|
||||
/>
|
||||
);
|
||||
@ -33,8 +35,11 @@ describe('ListHeader', () => {
|
||||
<ListHeader
|
||||
itemCount={7}
|
||||
qsConfig={qsConfig}
|
||||
columns={[{ name: 'name', key: 'name', isSearchable: true, isSortable: true }]}
|
||||
/>, { context: { router: { history } } }
|
||||
columns={[
|
||||
{ name: 'name', key: 'name', isSearchable: true, isSortable: true },
|
||||
]}
|
||||
/>,
|
||||
{ context: { router: { history } } }
|
||||
);
|
||||
|
||||
const toolbar = wrapper.find('DataListToolbar');
|
||||
|
||||
@ -15,7 +15,7 @@ import DataListToolbar from '@components/DataListToolbar';
|
||||
import {
|
||||
encodeNonDefaultQueryString,
|
||||
parseQueryString,
|
||||
addParams
|
||||
addParams,
|
||||
} from '@util/qs';
|
||||
import { pluralize, ucFirst } from '@util/strings';
|
||||
|
||||
@ -24,32 +24,32 @@ import { QSConfig } from '@types';
|
||||
import PaginatedDataListItem from './PaginatedDataListItem';
|
||||
|
||||
class PaginatedDataList extends React.Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSetPage = this.handleSetPage.bind(this);
|
||||
this.handleSetPageSize = this.handleSetPageSize.bind(this);
|
||||
}
|
||||
|
||||
handleSetPage (event, pageNumber) {
|
||||
handleSetPage(event, pageNumber) {
|
||||
const { history, qsConfig } = this.props;
|
||||
const { search } = history.location;
|
||||
this.pushHistoryState(addParams(qsConfig, search, { page: pageNumber }));
|
||||
}
|
||||
|
||||
handleSetPageSize (event, pageSize) {
|
||||
handleSetPageSize(event, pageSize) {
|
||||
const { history, qsConfig } = this.props;
|
||||
const { search } = history.location;
|
||||
this.pushHistoryState(addParams(qsConfig, search, { page_size: pageSize }));
|
||||
}
|
||||
|
||||
pushHistoryState (params) {
|
||||
pushHistoryState(params) {
|
||||
const { history, qsConfig } = this.props;
|
||||
const { pathname } = history.location;
|
||||
const encodedParams = encodeNonDefaultQueryString(qsConfig, params);
|
||||
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
contentError,
|
||||
hasContentLoading,
|
||||
@ -66,25 +66,42 @@ class PaginatedDataList extends React.Component {
|
||||
i18n,
|
||||
renderToolbar,
|
||||
} = this.props;
|
||||
const columns = toolbarColumns.length ? toolbarColumns : [{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true }];
|
||||
const columns = toolbarColumns.length
|
||||
? toolbarColumns
|
||||
: [
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isSortable: true,
|
||||
isSearchable: true,
|
||||
},
|
||||
];
|
||||
const queryParams = parseQueryString(qsConfig, location.search);
|
||||
|
||||
const itemDisplayName = ucFirst(pluralize(itemName));
|
||||
const itemDisplayNamePlural = ucFirst(itemNamePlural || pluralize(itemName));
|
||||
const itemDisplayNamePlural = ucFirst(
|
||||
itemNamePlural || pluralize(itemName)
|
||||
);
|
||||
|
||||
const dataListLabel = i18n._(t`${itemDisplayName} List`);
|
||||
const emptyContentMessage = i18n._(t`Please add ${itemDisplayNamePlural} to populate this list `);
|
||||
const emptyContentMessage = i18n._(
|
||||
t`Please add ${itemDisplayNamePlural} to populate this list `
|
||||
);
|
||||
const emptyContentTitle = i18n._(t`No ${itemDisplayNamePlural} Found `);
|
||||
|
||||
let Content;
|
||||
if (hasContentLoading && items.length <= 0) {
|
||||
Content = (<ContentLoading />);
|
||||
Content = <ContentLoading />;
|
||||
} else if (contentError) {
|
||||
Content = (<ContentError error={contentError} />);
|
||||
Content = <ContentError error={contentError} />;
|
||||
} else if (items.length <= 0) {
|
||||
Content = (<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />);
|
||||
Content = (
|
||||
<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />
|
||||
);
|
||||
} else {
|
||||
Content = (<DataList aria-label={dataListLabel}>{items.map(renderItem)}</DataList>);
|
||||
Content = (
|
||||
<DataList aria-label={dataListLabel}>{items.map(renderItem)}</DataList>
|
||||
);
|
||||
}
|
||||
|
||||
if (items.length <= 0) {
|
||||
@ -115,12 +132,16 @@ class PaginatedDataList extends React.Component {
|
||||
itemCount={itemCount}
|
||||
page={queryParams.page || 1}
|
||||
perPage={queryParams.page_size}
|
||||
perPageOptions={showPageSizeOptions ? [
|
||||
{ title: '5', value: 5 },
|
||||
{ title: '10', value: 10 },
|
||||
{ title: '20', value: 20 },
|
||||
{ title: '50', value: 50 }
|
||||
] : []}
|
||||
perPageOptions={
|
||||
showPageSizeOptions
|
||||
? [
|
||||
{ title: '5', value: 5 },
|
||||
{ title: '10', value: 10 },
|
||||
{ title: '20', value: 20 },
|
||||
{ title: '50', value: 50 },
|
||||
]
|
||||
: []
|
||||
}
|
||||
onSetPage={this.handleSetPage}
|
||||
onPerPageSelect={this.handleSetPageSize}
|
||||
/>
|
||||
@ -142,11 +163,13 @@ PaginatedDataList.propTypes = {
|
||||
itemNamePlural: PropTypes.string,
|
||||
qsConfig: QSConfig.isRequired,
|
||||
renderItem: PropTypes.func,
|
||||
toolbarColumns: arrayOf(shape({
|
||||
name: string.isRequired,
|
||||
key: string.isRequired,
|
||||
isSortable: bool,
|
||||
})),
|
||||
toolbarColumns: arrayOf(
|
||||
shape({
|
||||
name: string.isRequired,
|
||||
key: string.isRequired,
|
||||
isSortable: bool,
|
||||
})
|
||||
),
|
||||
showPageSizeOptions: PropTypes.bool,
|
||||
renderToolbar: PropTypes.func,
|
||||
hasContentLoading: PropTypes.bool,
|
||||
@ -160,8 +183,8 @@ PaginatedDataList.defaultProps = {
|
||||
itemName: 'item',
|
||||
itemNamePlural: '',
|
||||
showPageSizeOptions: true,
|
||||
renderItem: (item) => (<PaginatedDataListItem key={item.id} item={item} />),
|
||||
renderToolbar: (props) => (<DataListToolbar {...props} />),
|
||||
renderItem: item => <PaginatedDataListItem key={item.id} item={item} />,
|
||||
renderToolbar: props => <DataListToolbar {...props} />,
|
||||
};
|
||||
|
||||
export { PaginatedDataList as _PaginatedDataList };
|
||||
|
||||
@ -51,7 +51,8 @@ describe('<PaginatedDataList />', () => {
|
||||
order_by: 'name',
|
||||
}}
|
||||
qsConfig={qsConfig}
|
||||
/>, { context: { router: { history } } }
|
||||
/>,
|
||||
{ context: { router: { history } } }
|
||||
);
|
||||
|
||||
const pagination = wrapper.find('Pagination');
|
||||
@ -77,7 +78,8 @@ describe('<PaginatedDataList />', () => {
|
||||
order_by: 'name',
|
||||
}}
|
||||
qsConfig={qsConfig}
|
||||
/>, { context: { router: { history } } }
|
||||
/>,
|
||||
{ context: { router: { history } } }
|
||||
);
|
||||
|
||||
const pagination = wrapper.find('Pagination');
|
||||
|
||||
@ -10,11 +10,9 @@ import {
|
||||
DropdownItem,
|
||||
Form,
|
||||
FormGroup,
|
||||
TextInput as PFTextInput
|
||||
TextInput as PFTextInput,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
SearchIcon
|
||||
} from '@patternfly/react-icons';
|
||||
import { SearchIcon } from '@patternfly/react-icons';
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
@ -29,7 +27,8 @@ const Button = styled(PFButton)`
|
||||
`;
|
||||
|
||||
const Dropdown = styled(PFDropdown)`
|
||||
&&& { /* Higher specificity required because we are selecting unclassed elements */
|
||||
&&& {
|
||||
/* Higher specificity required because we are selecting unclassed elements */
|
||||
> button {
|
||||
min-height: 30px;
|
||||
min-width: 70px;
|
||||
@ -37,17 +36,19 @@ const Dropdown = styled(PFDropdown)`
|
||||
padding: 0 10px;
|
||||
margin: 0px;
|
||||
|
||||
> span { /* text element */
|
||||
> span {
|
||||
/* text element */
|
||||
width: auto;
|
||||
}
|
||||
|
||||
> svg { /* caret icon */
|
||||
> svg {
|
||||
/* caret icon */
|
||||
margin: 0px;
|
||||
padding-top: 3px;
|
||||
padding-left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const NoOptionDropdown = styled.div`
|
||||
@ -61,7 +62,7 @@ const InputFormGroup = styled(FormGroup)`
|
||||
`;
|
||||
|
||||
class Search extends React.Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { sortedColumnKey } = this.props;
|
||||
@ -77,11 +78,11 @@ class Search extends React.Component {
|
||||
this.handleSearch = this.handleSearch.bind(this);
|
||||
}
|
||||
|
||||
handleDropdownToggle (isSearchDropdownOpen) {
|
||||
handleDropdownToggle(isSearchDropdownOpen) {
|
||||
this.setState({ isSearchDropdownOpen });
|
||||
}
|
||||
|
||||
handleDropdownSelect ({ target }) {
|
||||
handleDropdownSelect({ target }) {
|
||||
const { columns } = this.props;
|
||||
const { innerText } = target;
|
||||
|
||||
@ -89,7 +90,7 @@ class Search extends React.Component {
|
||||
this.setState({ isSearchDropdownOpen: false, searchKey });
|
||||
}
|
||||
|
||||
handleSearch (e) {
|
||||
handleSearch(e) {
|
||||
// keeps page from fully reloading
|
||||
e.preventDefault();
|
||||
|
||||
@ -102,22 +103,17 @@ class Search extends React.Component {
|
||||
this.setState({ searchValue: '' });
|
||||
}
|
||||
|
||||
handleSearchInputChange (searchValue) {
|
||||
handleSearchInputChange(searchValue) {
|
||||
this.setState({ searchValue });
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { up } = DropdownPosition;
|
||||
const {
|
||||
columns,
|
||||
i18n
|
||||
} = this.props;
|
||||
const {
|
||||
isSearchDropdownOpen,
|
||||
searchKey,
|
||||
searchValue,
|
||||
} = this.state;
|
||||
const { name: searchColumnName } = columns.find(({ key }) => key === searchKey);
|
||||
const { columns, i18n } = this.props;
|
||||
const { isSearchDropdownOpen, searchKey, searchValue } = this.state;
|
||||
const { name: searchColumnName } = columns.find(
|
||||
({ key }) => key === searchKey
|
||||
);
|
||||
|
||||
const searchDropdownItems = columns
|
||||
.filter(({ key, isSearchable }) => isSearchable && key !== searchKey)
|
||||
@ -133,32 +129,38 @@ class Search extends React.Component {
|
||||
{searchDropdownItems.length > 0 ? (
|
||||
<FormGroup
|
||||
fieldId="searchKeyDropdown"
|
||||
label={(<span className="pf-screen-reader">{i18n._(t`Search key dropdown`)}</span>)}
|
||||
label={
|
||||
<span className="pf-screen-reader">
|
||||
{i18n._(t`Search key dropdown`)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Dropdown
|
||||
onToggle={this.handleDropdownToggle}
|
||||
onSelect={this.handleDropdownSelect}
|
||||
direction={up}
|
||||
isOpen={isSearchDropdownOpen}
|
||||
toggle={(
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
id="awx-search"
|
||||
onToggle={this.handleDropdownToggle}
|
||||
>
|
||||
{searchColumnName}
|
||||
</DropdownToggle>
|
||||
)}
|
||||
}
|
||||
dropdownItems={searchDropdownItems}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : (
|
||||
<NoOptionDropdown>
|
||||
{searchColumnName}
|
||||
</NoOptionDropdown>
|
||||
<NoOptionDropdown>{searchColumnName}</NoOptionDropdown>
|
||||
)}
|
||||
<InputFormGroup
|
||||
fieldId="searchValueTextInput"
|
||||
label={(<span className="pf-screen-reader">{i18n._(t`Search value text input`)}</span>)}
|
||||
label={
|
||||
<span className="pf-screen-reader">
|
||||
{i18n._(t`Search value text input`)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<TextInput
|
||||
type="search"
|
||||
@ -190,7 +192,7 @@ Search.propTypes = {
|
||||
|
||||
Search.defaultProps = {
|
||||
onSearch: null,
|
||||
sortedColumnKey: 'name'
|
||||
sortedColumnKey: 'name',
|
||||
};
|
||||
|
||||
export default withI18n()(Search);
|
||||
|
||||
@ -12,7 +12,9 @@ describe('<Search />', () => {
|
||||
});
|
||||
|
||||
test('it triggers the expected callbacks', () => {
|
||||
const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }];
|
||||
const columns = [
|
||||
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
|
||||
];
|
||||
|
||||
const searchBtn = 'button[aria-label="Search submit button"]';
|
||||
const searchTextInput = 'input[aria-label="Search text input"]';
|
||||
@ -20,11 +22,7 @@ describe('<Search />', () => {
|
||||
const onSearch = jest.fn();
|
||||
|
||||
search = mountWithContexts(
|
||||
<Search
|
||||
sortedColumnKey="name"
|
||||
columns={columns}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
<Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
|
||||
);
|
||||
|
||||
search.find(searchTextInput).instance().value = 'test-321';
|
||||
@ -36,14 +34,12 @@ describe('<Search />', () => {
|
||||
});
|
||||
|
||||
test('handleDropdownToggle properly updates state', async () => {
|
||||
const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }];
|
||||
const columns = [
|
||||
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
|
||||
];
|
||||
const onSearch = jest.fn();
|
||||
const wrapper = mountWithContexts(
|
||||
<Search
|
||||
sortedColumnKey="name"
|
||||
columns={columns}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
<Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
|
||||
).find('Search');
|
||||
expect(wrapper.state('isSearchDropdownOpen')).toEqual(false);
|
||||
wrapper.instance().handleDropdownToggle(true);
|
||||
@ -53,18 +49,21 @@ describe('<Search />', () => {
|
||||
test('handleDropdownSelect properly updates state', async () => {
|
||||
const columns = [
|
||||
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
|
||||
{ name: 'Description', key: 'description', isSortable: true, isSearchable: true }
|
||||
{
|
||||
name: 'Description',
|
||||
key: 'description',
|
||||
isSortable: true,
|
||||
isSearchable: true,
|
||||
},
|
||||
];
|
||||
const onSearch = jest.fn();
|
||||
const wrapper = mountWithContexts(
|
||||
<Search
|
||||
sortedColumnKey="name"
|
||||
columns={columns}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
<Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
|
||||
).find('Search');
|
||||
expect(wrapper.state('searchKey')).toEqual('name');
|
||||
wrapper.instance().handleDropdownSelect({ target: { innerText: 'Description' } });
|
||||
wrapper
|
||||
.instance()
|
||||
.handleDropdownSelect({ target: { innerText: 'Description' } });
|
||||
expect(wrapper.state('searchKey')).toEqual('description');
|
||||
});
|
||||
});
|
||||
|
||||
@ -12,7 +12,9 @@ describe('<Sort />', () => {
|
||||
});
|
||||
|
||||
test('it triggers the expected callbacks', () => {
|
||||
const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }];
|
||||
const columns = [
|
||||
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
|
||||
];
|
||||
|
||||
const sortBtn = 'button[aria-label="Sort"]';
|
||||
|
||||
@ -38,7 +40,7 @@ describe('<Sort />', () => {
|
||||
{ name: 'Foo', key: 'foo', isSortable: true },
|
||||
{ name: 'Bar', key: 'bar', isSortable: true },
|
||||
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
||||
{ name: 'Baz', key: 'baz' }
|
||||
{ name: 'Baz', key: 'baz' },
|
||||
];
|
||||
|
||||
const onSort = jest.fn();
|
||||
@ -62,7 +64,7 @@ describe('<Sort />', () => {
|
||||
{ name: 'Foo', key: 'foo', isSortable: true },
|
||||
{ name: 'Bar', key: 'bar', isSortable: true },
|
||||
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
||||
{ name: 'Baz', key: 'baz' }
|
||||
{ name: 'Baz', key: 'baz' },
|
||||
];
|
||||
|
||||
const onSort = jest.fn();
|
||||
@ -86,7 +88,7 @@ describe('<Sort />', () => {
|
||||
{ name: 'Foo', key: 'foo', isSortable: true },
|
||||
{ name: 'Bar', key: 'bar', isSortable: true },
|
||||
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
||||
{ name: 'Baz', key: 'baz' }
|
||||
{ name: 'Baz', key: 'baz' },
|
||||
];
|
||||
|
||||
const onSort = jest.fn();
|
||||
@ -109,7 +111,7 @@ describe('<Sort />', () => {
|
||||
{ name: 'Foo', key: 'foo', isSortable: true },
|
||||
{ name: 'Bar', key: 'bar', isSortable: true },
|
||||
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
||||
{ name: 'Baz', key: 'baz' }
|
||||
{ name: 'Baz', key: 'baz' },
|
||||
];
|
||||
|
||||
const onSort = jest.fn();
|
||||
@ -133,8 +135,12 @@ describe('<Sort />', () => {
|
||||
const downAlphaIconSelector = 'SortAlphaDownIcon';
|
||||
const upAlphaIconSelector = 'SortAlphaUpIcon';
|
||||
|
||||
const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }];
|
||||
const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }];
|
||||
const numericColumns = [
|
||||
{ name: 'ID', key: 'id', isSortable: true, isNumeric: true },
|
||||
];
|
||||
const alphaColumns = [
|
||||
{ name: 'Name', key: 'name', isSortable: true, isNumeric: false },
|
||||
];
|
||||
const onSort = jest.fn();
|
||||
|
||||
sort = mountWithContexts(
|
||||
|
||||
@ -2,17 +2,13 @@ import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Card,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core';
|
||||
|
||||
import { UnifiedJobsAPI } from '@api';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import DatalistToolbar from '@components/DataListToolbar';
|
||||
import PaginatedDataList, {
|
||||
ToolbarDeleteButton
|
||||
ToolbarDeleteButton,
|
||||
} from '@components/PaginatedDataList';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
|
||||
@ -26,12 +22,12 @@ const QS_CONFIG = getQSConfig('job', {
|
||||
});
|
||||
|
||||
class JobList extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hasContentLoading: true,
|
||||
hasContentError: false,
|
||||
contentError: null,
|
||||
deletionError: false,
|
||||
selected: [],
|
||||
jobs: [],
|
||||
@ -44,28 +40,28 @@ class JobList extends Component {
|
||||
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.loadJobs();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location } = this.props;
|
||||
if (location !== prevProps.location) {
|
||||
this.loadJobs();
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteErrorClose () {
|
||||
handleDeleteErrorClose() {
|
||||
this.setState({ deletionError: false });
|
||||
}
|
||||
|
||||
handleSelectAll (isSelected) {
|
||||
handleSelectAll(isSelected) {
|
||||
const { jobs } = this.state;
|
||||
const selected = isSelected ? [...jobs] : [];
|
||||
this.setState({ selected });
|
||||
}
|
||||
|
||||
handleSelect (item) {
|
||||
handleSelect(item) {
|
||||
const { selected } = this.state;
|
||||
if (selected.some(s => s.id === item.id)) {
|
||||
this.setState({ selected: selected.filter(s => s.id !== item.id) });
|
||||
@ -74,7 +70,7 @@ class JobList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async handleDelete () {
|
||||
async handleDelete() {
|
||||
const { selected } = this.state;
|
||||
this.setState({ hasContentLoading: true, deletionError: false });
|
||||
try {
|
||||
@ -86,38 +82,37 @@ class JobList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async loadJobs () {
|
||||
async loadJobs() {
|
||||
const { location } = this.props;
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
|
||||
this.setState({ hasContentError: false, hasContentLoading: true });
|
||||
this.setState({ contentError: null, hasContentLoading: true });
|
||||
try {
|
||||
const { data: { count, results } } = await UnifiedJobsAPI.read(params);
|
||||
const {
|
||||
data: { count, results },
|
||||
} = await UnifiedJobsAPI.read(params);
|
||||
this.setState({
|
||||
itemCount: count,
|
||||
jobs: results,
|
||||
selected: [],
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({ hasContentError: true });
|
||||
this.setState({ contentError: err });
|
||||
} finally {
|
||||
this.setState({ hasContentLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
hasContentError,
|
||||
contentError,
|
||||
hasContentLoading,
|
||||
deletionError,
|
||||
jobs,
|
||||
itemCount,
|
||||
selected,
|
||||
} = this.state;
|
||||
const {
|
||||
match,
|
||||
i18n
|
||||
} = this.props;
|
||||
const { match, i18n } = this.props;
|
||||
const { medium } = PageSectionVariants;
|
||||
const isAllSelected = selected.length === jobs.length;
|
||||
const itemName = i18n._(t`Job`);
|
||||
@ -125,17 +120,27 @@ class JobList extends Component {
|
||||
<PageSection variant={medium}>
|
||||
<Card>
|
||||
<PaginatedDataList
|
||||
hasContentError={hasContentError}
|
||||
contentError={contentError}
|
||||
hasContentLoading={hasContentLoading}
|
||||
items={jobs}
|
||||
itemCount={itemCount}
|
||||
itemName={itemName}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarColumns={[
|
||||
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true },
|
||||
{ name: i18n._(t`Finished`), key: 'finished', isSortable: true, isNumeric: true },
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isSortable: true,
|
||||
isSearchable: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Finished`),
|
||||
key: 'finished',
|
||||
isSortable: true,
|
||||
isNumeric: true,
|
||||
},
|
||||
]}
|
||||
renderToolbar={(props) => (
|
||||
renderToolbar={props => (
|
||||
<DatalistToolbar
|
||||
{...props}
|
||||
showSelectAll
|
||||
@ -148,11 +153,11 @@ class JobList extends Component {
|
||||
onDelete={this.handleDelete}
|
||||
itemsToDelete={selected}
|
||||
itemName={itemName}
|
||||
/>
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
renderItem={(job) => (
|
||||
renderItem={job => (
|
||||
<JobListItem
|
||||
key={job.id}
|
||||
value={job.name}
|
||||
|
||||
@ -7,12 +7,10 @@ import { OrganizationsAPI, TeamsAPI, UsersAPI } from '@api';
|
||||
import AddResourceRole from '@components/AddRole/AddResourceRole';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import DataListToolbar from '@components/DataListToolbar';
|
||||
import PaginatedDataList, { ToolbarAddButton } from '@components/PaginatedDataList';
|
||||
import {
|
||||
getQSConfig,
|
||||
encodeQueryString,
|
||||
parseQueryString
|
||||
} from '@util/qs';
|
||||
import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
} from '@components/PaginatedDataList';
|
||||
import { getQSConfig, encodeQueryString, parseQueryString } from '@util/qs';
|
||||
import { Organization } from '@types';
|
||||
|
||||
import DeleteRoleConfirmationModal from './DeleteRoleConfirmationModal';
|
||||
@ -29,11 +27,11 @@ class OrganizationAccess extends React.Component {
|
||||
organization: Organization.isRequired,
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
accessRecords: [],
|
||||
hasContentError: false,
|
||||
contentError: null,
|
||||
hasContentLoading: true,
|
||||
hasDeletionError: false,
|
||||
deletionRecord: null,
|
||||
@ -51,11 +49,11 @@ class OrganizationAccess extends React.Component {
|
||||
this.handleDeleteOpen = this.handleDeleteOpen.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.loadAccessList();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location } = this.props;
|
||||
|
||||
const prevParams = parseQueryString(QS_CONFIG, prevProps.location.search);
|
||||
@ -66,43 +64,40 @@ class OrganizationAccess extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
async loadAccessList () {
|
||||
async loadAccessList() {
|
||||
const { organization, location } = this.props;
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
|
||||
this.setState({ hasContentError: false, hasContentLoading: true });
|
||||
this.setState({ contentError: null, hasContentLoading: true });
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
results: accessRecords = [],
|
||||
count: itemCount = 0
|
||||
}
|
||||
data: { results: accessRecords = [], count: itemCount = 0 },
|
||||
} = await OrganizationsAPI.readAccessList(organization.id, params);
|
||||
this.setState({ itemCount, accessRecords });
|
||||
} catch (error) {
|
||||
this.setState({ hasContentError: true });
|
||||
} catch (err) {
|
||||
this.setState({ cotentError: err });
|
||||
} finally {
|
||||
this.setState({ hasContentLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteOpen (deletionRole, deletionRecord) {
|
||||
handleDeleteOpen(deletionRole, deletionRecord) {
|
||||
this.setState({ deletionRole, deletionRecord });
|
||||
}
|
||||
|
||||
handleDeleteCancel () {
|
||||
handleDeleteCancel() {
|
||||
this.setState({ deletionRole: null, deletionRecord: null });
|
||||
}
|
||||
|
||||
handleDeleteErrorClose () {
|
||||
handleDeleteErrorClose() {
|
||||
this.setState({
|
||||
hasDeletionError: false,
|
||||
deletionRecord: null,
|
||||
deletionRole: null
|
||||
deletionRole: null,
|
||||
});
|
||||
}
|
||||
|
||||
async handleDeleteConfirm () {
|
||||
async handleDeleteConfirm() {
|
||||
const { deletionRole, deletionRecord } = this.state;
|
||||
|
||||
if (!deletionRole || !deletionRecord) {
|
||||
@ -111,7 +106,10 @@ class OrganizationAccess extends React.Component {
|
||||
|
||||
let promise;
|
||||
if (typeof deletionRole.team_id !== 'undefined') {
|
||||
promise = TeamsAPI.disassociateRole(deletionRole.team_id, deletionRole.id);
|
||||
promise = TeamsAPI.disassociateRole(
|
||||
deletionRole.team_id,
|
||||
deletionRole.id
|
||||
);
|
||||
} else {
|
||||
promise = UsersAPI.disassociateRole(deletionRecord.id, deletionRole.id);
|
||||
}
|
||||
@ -121,34 +119,34 @@ class OrganizationAccess extends React.Component {
|
||||
await promise.then(this.loadAccessList);
|
||||
this.setState({
|
||||
deletionRole: null,
|
||||
deletionRecord: null
|
||||
deletionRecord: null,
|
||||
});
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
hasContentLoading: false,
|
||||
hasDeletionError: true
|
||||
hasDeletionError: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleAddClose () {
|
||||
handleAddClose() {
|
||||
this.setState({ isAddModalOpen: false });
|
||||
}
|
||||
|
||||
handleAddOpen () {
|
||||
handleAddOpen() {
|
||||
this.setState({ isAddModalOpen: true });
|
||||
}
|
||||
|
||||
handleAddSuccess () {
|
||||
handleAddSuccess() {
|
||||
this.setState({ isAddModalOpen: false });
|
||||
this.loadAccessList();
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { organization, i18n } = this.props;
|
||||
const {
|
||||
accessRecords,
|
||||
hasContentError,
|
||||
contentError,
|
||||
hasContentLoading,
|
||||
deletionRole,
|
||||
deletionRecord,
|
||||
@ -157,28 +155,51 @@ class OrganizationAccess extends React.Component {
|
||||
isAddModalOpen,
|
||||
} = this.state;
|
||||
const canEdit = organization.summary_fields.user_capabilities.edit;
|
||||
const isDeleteModalOpen = !hasContentLoading && !hasDeletionError && deletionRole;
|
||||
const isDeleteModalOpen =
|
||||
!hasContentLoading && !hasDeletionError && deletionRole;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PaginatedDataList
|
||||
hasContentError={hasContentError}
|
||||
error={contentError}
|
||||
hasContentLoading={hasContentLoading}
|
||||
items={accessRecords}
|
||||
itemCount={itemCount}
|
||||
itemName="role"
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarColumns={[
|
||||
{ name: i18n._(t`First Name`), key: 'first_name', isSortable: true, isSearchable: true },
|
||||
{ name: i18n._(t`Username`), key: 'username', isSortable: true, isSearchable: true },
|
||||
{ name: i18n._(t`Last Name`), key: 'last_name', isSortable: true, isSearchable: true },
|
||||
{
|
||||
name: i18n._(t`First Name`),
|
||||
key: 'first_name',
|
||||
isSortable: true,
|
||||
isSearchable: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Username`),
|
||||
key: 'username',
|
||||
isSortable: true,
|
||||
isSearchable: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Last Name`),
|
||||
key: 'last_name',
|
||||
isSortable: true,
|
||||
isSearchable: true,
|
||||
},
|
||||
]}
|
||||
renderToolbar={(props) => (
|
||||
renderToolbar={props => (
|
||||
<DataListToolbar
|
||||
{...props}
|
||||
additionalControls={canEdit ? [
|
||||
<ToolbarAddButton key="add" onClick={this.handleAddOpen} />
|
||||
] : null}
|
||||
additionalControls={
|
||||
canEdit
|
||||
? [
|
||||
<ToolbarAddButton
|
||||
key="add"
|
||||
onClick={this.handleAddOpen}
|
||||
/>,
|
||||
]
|
||||
: null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
renderItem={accessRecord => (
|
||||
|
||||
@ -103,7 +103,7 @@ describe('<OrganizationAccess />', () => {
|
||||
expect(wrapper.find('OrganizationAccess').state('hasContentLoading')).toBe(
|
||||
false
|
||||
);
|
||||
expect(wrapper.find('OrganizationAccess').state('hasContentError')).toBe(false);
|
||||
expect(wrapper.find('OrganizationAccess').state('contentError')).toBe(null);
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
|
||||
}
|
||||
>
|
||||
<WithI18n
|
||||
hasContentError={false}
|
||||
error={null}
|
||||
hasContentLoading={true}
|
||||
itemCount={0}
|
||||
itemName="role"
|
||||
@ -83,7 +83,7 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
|
||||
withHash={true}
|
||||
>
|
||||
<withRouter(PaginatedDataList)
|
||||
hasContentError={false}
|
||||
error={null}
|
||||
hasContentLoading={true}
|
||||
i18n={"/i18n/"}
|
||||
itemCount={0}
|
||||
@ -130,7 +130,8 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
|
||||
>
|
||||
<Route>
|
||||
<PaginatedDataList
|
||||
hasContentError={false}
|
||||
contentError={null}
|
||||
error={null}
|
||||
hasContentLoading={true}
|
||||
history={"/history/"}
|
||||
i18n={"/i18n/"}
|
||||
|
||||
@ -2,11 +2,7 @@ import React, { Component, Fragment } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Card,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core';
|
||||
|
||||
import { OrganizationsAPI } from '@api';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
@ -26,12 +22,12 @@ const QS_CONFIG = getQSConfig('organization', {
|
||||
});
|
||||
|
||||
class OrganizationsList extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hasContentLoading: true,
|
||||
hasContentError: false,
|
||||
contentError: null,
|
||||
hasDeletionError: false,
|
||||
organizations: [],
|
||||
selected: [],
|
||||
@ -46,25 +42,25 @@ class OrganizationsList extends Component {
|
||||
this.loadOrganizations = this.loadOrganizations.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.loadOrganizations();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location } = this.props;
|
||||
if (location !== prevProps.location) {
|
||||
this.loadOrganizations();
|
||||
}
|
||||
}
|
||||
|
||||
handleSelectAll (isSelected) {
|
||||
handleSelectAll(isSelected) {
|
||||
const { organizations } = this.state;
|
||||
|
||||
const selected = isSelected ? [...organizations] : [];
|
||||
this.setState({ selected });
|
||||
}
|
||||
|
||||
handleSelect (row) {
|
||||
handleSelect(row) {
|
||||
const { selected } = this.state;
|
||||
|
||||
if (selected.some(s => s.id === row.id)) {
|
||||
@ -74,16 +70,16 @@ class OrganizationsList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteErrorClose () {
|
||||
handleDeleteErrorClose() {
|
||||
this.setState({ hasDeletionError: false });
|
||||
}
|
||||
|
||||
async handleOrgDelete () {
|
||||
async handleOrgDelete() {
|
||||
const { selected } = this.state;
|
||||
|
||||
this.setState({ hasContentLoading: true, hasDeletionError: false });
|
||||
try {
|
||||
await Promise.all(selected.map((org) => OrganizationsAPI.destroy(org.id)));
|
||||
await Promise.all(selected.map(org => OrganizationsAPI.destroy(org.id)));
|
||||
} catch (err) {
|
||||
this.setState({ hasDeletionError: true });
|
||||
} finally {
|
||||
@ -91,7 +87,7 @@ class OrganizationsList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async loadOrganizations () {
|
||||
async loadOrganizations() {
|
||||
const { location } = this.props;
|
||||
const { actions: cachedActions } = this.state;
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
@ -108,9 +104,16 @@ class OrganizationsList extends Component {
|
||||
optionsPromise,
|
||||
]);
|
||||
|
||||
this.setState({ hasContentError: false, hasContentLoading: true });
|
||||
this.setState({ contentError: null, hasContentLoading: true });
|
||||
try {
|
||||
const [{ data: { count, results } }, { data: { actions } }] = await promises;
|
||||
const [
|
||||
{
|
||||
data: { count, results },
|
||||
},
|
||||
{
|
||||
data: { actions },
|
||||
},
|
||||
] = await promises;
|
||||
this.setState({
|
||||
actions,
|
||||
itemCount: count,
|
||||
@ -118,20 +121,18 @@ class OrganizationsList extends Component {
|
||||
selected: [],
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState(({ hasContentError: true }));
|
||||
this.setState({ contentError: err });
|
||||
} finally {
|
||||
this.setState({ hasContentLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
medium,
|
||||
} = PageSectionVariants;
|
||||
render() {
|
||||
const { medium } = PageSectionVariants;
|
||||
const {
|
||||
actions,
|
||||
itemCount,
|
||||
hasContentError,
|
||||
contentError,
|
||||
hasContentLoading,
|
||||
hasDeletionError,
|
||||
selected,
|
||||
@ -139,7 +140,8 @@ class OrganizationsList extends Component {
|
||||
} = this.state;
|
||||
const { match, i18n } = this.props;
|
||||
|
||||
const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||
const canAdd =
|
||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||
const isAllSelected = selected.length === organizations.length;
|
||||
|
||||
return (
|
||||
@ -147,18 +149,33 @@ class OrganizationsList extends Component {
|
||||
<PageSection variant={medium}>
|
||||
<Card>
|
||||
<PaginatedDataList
|
||||
hasContentError={hasContentError}
|
||||
error={contentError}
|
||||
hasContentLoading={hasContentLoading}
|
||||
items={organizations}
|
||||
itemCount={itemCount}
|
||||
itemName="organization"
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarColumns={[
|
||||
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true },
|
||||
{ name: i18n._(t`Modified`), key: 'modified', isSortable: true, isNumeric: true },
|
||||
{ name: i18n._(t`Created`), key: 'created', isSortable: true, isNumeric: true },
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isSortable: true,
|
||||
isSearchable: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified`),
|
||||
key: 'modified',
|
||||
isSortable: true,
|
||||
isNumeric: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created`),
|
||||
key: 'created',
|
||||
isSortable: true,
|
||||
isNumeric: true,
|
||||
},
|
||||
]}
|
||||
renderToolbar={(props) => (
|
||||
renderToolbar={props => (
|
||||
<DataListToolbar
|
||||
{...props}
|
||||
showSelectAll
|
||||
@ -171,13 +188,13 @@ class OrganizationsList extends Component {
|
||||
itemsToDelete={selected}
|
||||
itemName="Organization"
|
||||
/>,
|
||||
canAdd
|
||||
? <ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
|
||||
: null,
|
||||
canAdd ? (
|
||||
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
|
||||
) : null,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
renderItem={(o) => (
|
||||
renderItem={o => (
|
||||
<OrganizationListItem
|
||||
key={o.id}
|
||||
organization={o}
|
||||
@ -187,8 +204,9 @@ class OrganizationsList extends Component {
|
||||
/>
|
||||
)}
|
||||
emptyStateControls={
|
||||
canAdd ? <ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
|
||||
: null
|
||||
canAdd ? (
|
||||
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@ -24,7 +24,7 @@ const COLUMNS = [
|
||||
];
|
||||
|
||||
class OrganizationNotifications extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
contentError: null,
|
||||
@ -38,22 +38,24 @@ class OrganizationNotifications extends Component {
|
||||
typeLabels: null,
|
||||
};
|
||||
this.handleNotificationToggle = this.handleNotificationToggle.bind(this);
|
||||
this.handleNotificationErrorClose = this.handleNotificationErrorClose.bind(this);
|
||||
this.handleNotificationErrorClose = this.handleNotificationErrorClose.bind(
|
||||
this
|
||||
);
|
||||
this.loadNotifications = this.loadNotifications.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.loadNotifications();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location } = this.props;
|
||||
if (location !== prevProps.location) {
|
||||
this.loadNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
async loadNotifications () {
|
||||
async loadNotifications() {
|
||||
const { id, location } = this.props;
|
||||
const { typeLabels } = this.state;
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
@ -67,13 +69,13 @@ class OrganizationNotifications extends Component {
|
||||
this.setState({ contentError: null, hasContentLoading: true });
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
count: itemCount = 0,
|
||||
results: notifications = [],
|
||||
}
|
||||
data: { count: itemCount = 0, results: notifications = [] },
|
||||
} = await OrganizationsAPI.readNotificationTemplates(id, params);
|
||||
|
||||
const optionsResponse = await OrganizationsAPI.readOptionsNotificationTemplates(id, params);
|
||||
const optionsResponse = await OrganizationsAPI.readOptionsNotificationTemplates(
|
||||
id,
|
||||
params
|
||||
);
|
||||
|
||||
let idMatchParams;
|
||||
if (notifications.length > 0) {
|
||||
@ -122,7 +124,7 @@ class OrganizationNotifications extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async handleNotificationToggle (notificationId, isCurrentlyOn, status) {
|
||||
async handleNotificationToggle(notificationId, isCurrentlyOn, status) {
|
||||
const { id } = this.props;
|
||||
|
||||
let stateArrayName;
|
||||
@ -135,13 +137,15 @@ class OrganizationNotifications extends Component {
|
||||
let stateUpdateFunction;
|
||||
if (isCurrentlyOn) {
|
||||
// when switching off, remove the toggled notification id from the array
|
||||
stateUpdateFunction = (prevState) => ({
|
||||
[stateArrayName]: prevState[stateArrayName].filter(i => i !== notificationId)
|
||||
stateUpdateFunction = prevState => ({
|
||||
[stateArrayName]: prevState[stateArrayName].filter(
|
||||
i => i !== notificationId
|
||||
),
|
||||
});
|
||||
} else {
|
||||
// when switching on, add the toggled notification id to the array
|
||||
stateUpdateFunction = (prevState) => ({
|
||||
[stateArrayName]: prevState[stateArrayName].concat(notificationId)
|
||||
stateUpdateFunction = prevState => ({
|
||||
[stateArrayName]: prevState[stateArrayName].concat(notificationId),
|
||||
});
|
||||
}
|
||||
|
||||
@ -161,11 +165,11 @@ class OrganizationNotifications extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleNotificationErrorClose () {
|
||||
handleNotificationErrorClose() {
|
||||
this.setState({ toggleError: false });
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { canToggleNotifications, i18n } = this.props;
|
||||
const {
|
||||
contentError,
|
||||
@ -176,7 +180,7 @@ class OrganizationNotifications extends Component {
|
||||
notifications,
|
||||
successTemplateIds,
|
||||
errorTemplateIds,
|
||||
typeLabels
|
||||
typeLabels,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
@ -189,7 +193,7 @@ class OrganizationNotifications extends Component {
|
||||
itemName="notification"
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarColumns={COLUMNS}
|
||||
renderItem={(notification) => (
|
||||
renderItem={notification => (
|
||||
<NotificationListItem
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
|
||||
@ -164,7 +164,6 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
|
||||
<Route>
|
||||
<PaginatedDataList
|
||||
contentError={null}
|
||||
hasContentError={false}
|
||||
hasContentLoading={false}
|
||||
history={"/history/"}
|
||||
i18n={"/i18n/"}
|
||||
|
||||
@ -8,30 +8,24 @@ import { QuestionCircleIcon } from '@patternfly/react-icons';
|
||||
import { InstanceGroupsAPI } from '@api';
|
||||
import Lookup from '@components/Lookup';
|
||||
|
||||
const getInstanceGroups = async (params) => InstanceGroupsAPI.read(params);
|
||||
const getInstanceGroups = async params => InstanceGroupsAPI.read(params);
|
||||
|
||||
class InstanceGroupsLookup extends React.Component {
|
||||
render () {
|
||||
render() {
|
||||
const { value, tooltip, onChange, i18n } = this.props;
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={(
|
||||
label={
|
||||
<Fragment>
|
||||
{i18n._(t`Instance Groups`)}
|
||||
{' '}
|
||||
{
|
||||
tooltip && (
|
||||
<Tooltip
|
||||
position="right"
|
||||
content={tooltip}
|
||||
>
|
||||
<QuestionCircleIcon />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
{i18n._(t`Instance Groups`)}{' '}
|
||||
{tooltip && (
|
||||
<Tooltip position="right" content={tooltip}>
|
||||
<QuestionCircleIcon />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
}
|
||||
fieldId="org-instance-groups"
|
||||
>
|
||||
<Lookup
|
||||
@ -43,9 +37,24 @@ class InstanceGroupsLookup extends React.Component {
|
||||
getItems={getInstanceGroups}
|
||||
multiple
|
||||
columns={[
|
||||
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true },
|
||||
{ name: i18n._(t`Modified`), key: 'modified', isSortable: false, isNumeric: true },
|
||||
{ name: i18n._(t`Created`), key: 'created', isSortable: false, isNumeric: true }
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isSortable: true,
|
||||
isSearchable: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified`),
|
||||
key: 'modified',
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created`),
|
||||
key: 'created',
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
]}
|
||||
sortedColumnKey="name"
|
||||
/>
|
||||
|
||||
@ -2,21 +2,17 @@ import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Card,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core';
|
||||
|
||||
import {
|
||||
JobTemplatesAPI,
|
||||
UnifiedJobTemplatesAPI,
|
||||
WorkflowJobTemplatesAPI
|
||||
WorkflowJobTemplatesAPI,
|
||||
} from '@api';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import DatalistToolbar from '@components/DataListToolbar';
|
||||
import PaginatedDataList, {
|
||||
ToolbarDeleteButton
|
||||
ToolbarDeleteButton,
|
||||
} from '@components/PaginatedDataList';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
|
||||
@ -28,16 +24,16 @@ const QS_CONFIG = getQSConfig('template', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
type: 'job_template,workflow_job_template'
|
||||
type: 'job_template,workflow_job_template',
|
||||
});
|
||||
|
||||
class TemplatesList extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hasContentLoading: true,
|
||||
hasContentError: false,
|
||||
contentError: null,
|
||||
hasDeletionError: false,
|
||||
selected: [],
|
||||
templates: [],
|
||||
@ -50,28 +46,28 @@ class TemplatesList extends Component {
|
||||
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.loadTemplates();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location } = this.props;
|
||||
if (location !== prevProps.location) {
|
||||
this.loadTemplates();
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteErrorClose () {
|
||||
handleDeleteErrorClose() {
|
||||
this.setState({ hasDeletionError: false });
|
||||
}
|
||||
|
||||
handleSelectAll (isSelected) {
|
||||
handleSelectAll(isSelected) {
|
||||
const { templates } = this.state;
|
||||
const selected = isSelected ? [...templates] : [];
|
||||
this.setState({ selected });
|
||||
}
|
||||
|
||||
handleSelect (template) {
|
||||
handleSelect(template) {
|
||||
const { selected } = this.state;
|
||||
if (selected.some(s => s.id === template.id)) {
|
||||
this.setState({ selected: selected.filter(s => s.id !== template.id) });
|
||||
@ -80,20 +76,22 @@ class TemplatesList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async handleTemplateDelete () {
|
||||
async handleTemplateDelete() {
|
||||
const { selected } = this.state;
|
||||
|
||||
this.setState({ hasContentLoading: true, hasDeletionError: false });
|
||||
try {
|
||||
await Promise.all(selected.map(({ type, id }) => {
|
||||
let deletePromise;
|
||||
if (type === 'job_template') {
|
||||
deletePromise = JobTemplatesAPI.destroy(id);
|
||||
} else if (type === 'workflow_job_template') {
|
||||
deletePromise = WorkflowJobTemplatesAPI.destroy(id);
|
||||
}
|
||||
return deletePromise;
|
||||
}));
|
||||
await Promise.all(
|
||||
selected.map(({ type, id }) => {
|
||||
let deletePromise;
|
||||
if (type === 'job_template') {
|
||||
deletePromise = JobTemplatesAPI.destroy(id);
|
||||
} else if (type === 'workflow_job_template') {
|
||||
deletePromise = WorkflowJobTemplatesAPI.destroy(id);
|
||||
}
|
||||
return deletePromise;
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
this.setState({ hasDeletionError: true });
|
||||
} finally {
|
||||
@ -101,56 +99,70 @@ class TemplatesList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async loadTemplates () {
|
||||
async loadTemplates() {
|
||||
const { location } = this.props;
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
|
||||
this.setState({ hasContentError: false, hasContentLoading: true });
|
||||
this.setState({ contentError: null, hasContentLoading: true });
|
||||
try {
|
||||
const { data: { count, results } } = await UnifiedJobTemplatesAPI.read(params);
|
||||
const {
|
||||
data: { count, results },
|
||||
} = await UnifiedJobTemplatesAPI.read(params);
|
||||
this.setState({
|
||||
itemCount: count,
|
||||
templates: results,
|
||||
selected: [],
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({ hasContentError: true });
|
||||
this.setState({ contentError: err });
|
||||
} finally {
|
||||
this.setState({ hasContentLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
hasContentError,
|
||||
contentError,
|
||||
hasContentLoading,
|
||||
hasDeletionError,
|
||||
templates,
|
||||
itemCount,
|
||||
selected,
|
||||
} = this.state;
|
||||
const {
|
||||
match,
|
||||
i18n
|
||||
} = this.props;
|
||||
const { match, i18n } = this.props;
|
||||
const isAllSelected = selected.length === templates.length;
|
||||
const { medium } = PageSectionVariants;
|
||||
return (
|
||||
<PageSection variant={medium}>
|
||||
<Card>
|
||||
<PaginatedDataList
|
||||
hasContentError={hasContentError}
|
||||
err={contentError}
|
||||
hasContentLoading={hasContentLoading}
|
||||
items={templates}
|
||||
itemCount={itemCount}
|
||||
itemName={i18n._(t`Template`)}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarColumns={[
|
||||
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true },
|
||||
{ name: i18n._(t`Modified`), key: 'modified', isSortable: true, isNumeric: true },
|
||||
{ name: i18n._(t`Created`), key: 'created', isSortable: true, isNumeric: true },
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isSortable: true,
|
||||
isSearchable: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified`),
|
||||
key: 'modified',
|
||||
isSortable: true,
|
||||
isNumeric: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created`),
|
||||
key: 'created',
|
||||
isSortable: true,
|
||||
isNumeric: true,
|
||||
},
|
||||
]}
|
||||
renderToolbar={(props) => (
|
||||
renderToolbar={props => (
|
||||
<DatalistToolbar
|
||||
{...props}
|
||||
showSelectAll
|
||||
@ -163,11 +175,11 @@ class TemplatesList extends Component {
|
||||
onDelete={this.handleTemplateDelete}
|
||||
itemsToDelete={selected}
|
||||
itemName={i18n._(t`Template`)}
|
||||
/>
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
renderItem={(template) => (
|
||||
renderItem={template => (
|
||||
<TemplateListItem
|
||||
key={template.id}
|
||||
value={template.name}
|
||||
|
||||
@ -4,16 +4,17 @@
|
||||
* @param {array} array in the format [ [ key, value ], ...]
|
||||
* @return {object} object in the forms { key: value, ... }
|
||||
*/
|
||||
const toObject = (entriesArr) => entriesArr.reduce((acc, [key, value]) => {
|
||||
if (acc[key] && Array.isArray(acc[key])) {
|
||||
acc[key].push(value);
|
||||
} else if (acc[key]) {
|
||||
acc[key] = [acc[key], value];
|
||||
} else {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
const toObject = entriesArr =>
|
||||
entriesArr.reduce((acc, [key, value]) => {
|
||||
if (acc[key] && Array.isArray(acc[key])) {
|
||||
acc[key].push(value);
|
||||
} else if (acc[key]) {
|
||||
acc[key] = [acc[key], value];
|
||||
} else {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* helper function to namespace params object
|
||||
@ -30,7 +31,7 @@ const namespaceParams = (namespace, params = {}) => {
|
||||
});
|
||||
|
||||
return namespaced || {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* helper function to remove namespace from params object
|
||||
@ -47,7 +48,7 @@ const denamespaceParams = (namespace, params = {}) => {
|
||||
});
|
||||
|
||||
return denamespaced || {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* helper function to check the namespace of a param is what you expec
|
||||
@ -59,7 +60,7 @@ const namespaceMatches = (namespace, fieldname) => {
|
||||
if (!namespace) return !fieldname.includes('.');
|
||||
|
||||
return fieldname.startsWith(`${namespace}.`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* helper function to check the value of a param is equal to another
|
||||
@ -72,13 +73,15 @@ const paramValueIsEqual = (one, two) => {
|
||||
|
||||
if (Array.isArray(one) && Array.isArray(two)) {
|
||||
isEqual = one.filter(val => two.indexOf(val) > -1).length === 0;
|
||||
} else if ((typeof(one) === "string" && typeof(two) === "string") ||
|
||||
(typeof(one) === "number" && typeof(two) === "number")){
|
||||
} else if (
|
||||
(typeof one === 'string' && typeof two === 'string') ||
|
||||
(typeof one === 'number' && typeof two === 'number')
|
||||
) {
|
||||
isEqual = one === two;
|
||||
}
|
||||
|
||||
return isEqual;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert query param object to url query string
|
||||
@ -87,17 +90,19 @@ const paramValueIsEqual = (one, two) => {
|
||||
* @param {object} query param object
|
||||
* @return {string} url query string
|
||||
*/
|
||||
export const encodeQueryString = (params) => {
|
||||
export const encodeQueryString = params => {
|
||||
if (!params) return '';
|
||||
|
||||
return Object.keys(params)
|
||||
.sort()
|
||||
.filter(key => params[key] !== null)
|
||||
.map(key => ([key, params[key]]))
|
||||
.map(key => [key, params[key]])
|
||||
.map(([key, value]) => {
|
||||
// if value is array, should return more than one key value pair
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
|
||||
return value
|
||||
.map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
|
||||
.join('&');
|
||||
}
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
})
|
||||
@ -115,22 +120,31 @@ export const encodeNonDefaultQueryString = (config, params) => {
|
||||
if (!params) return '';
|
||||
|
||||
const namespacedParams = namespaceParams(config.namespace, params);
|
||||
const namespacedDefaults = namespaceParams(config.namespace, config.defaultParams);
|
||||
const namespacedDefaults = namespaceParams(
|
||||
config.namespace,
|
||||
config.defaultParams
|
||||
);
|
||||
const namespacedDefaultKeys = Object.keys(namespacedDefaults);
|
||||
const namespacedParamsWithoutDefaultsKeys = Object.keys(namespacedParams)
|
||||
.filter(key => namespacedDefaultKeys.indexOf(key) === -1 ||
|
||||
!paramValueIsEqual(namespacedParams[key], namespacedDefaults[key]));
|
||||
const namespacedParamsWithoutDefaultsKeys = Object.keys(
|
||||
namespacedParams
|
||||
).filter(
|
||||
key =>
|
||||
namespacedDefaultKeys.indexOf(key) === -1 ||
|
||||
!paramValueIsEqual(namespacedParams[key], namespacedDefaults[key])
|
||||
);
|
||||
|
||||
return namespacedParamsWithoutDefaultsKeys
|
||||
.sort()
|
||||
.filter(key => namespacedParams[key] !== null)
|
||||
.map(key => {
|
||||
return ([key, namespacedParams[key]])
|
||||
return [key, namespacedParams[key]];
|
||||
})
|
||||
.map(([key, value]) => {
|
||||
// if value is array, should return more than one key value pair
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
|
||||
return value
|
||||
.map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
|
||||
.join('&');
|
||||
}
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
})
|
||||
@ -144,7 +158,7 @@ export const encodeNonDefaultQueryString = (config, params) => {
|
||||
* @param {array} params that are number fields
|
||||
* @return {object} query param object
|
||||
*/
|
||||
export function getQSConfig (
|
||||
export function getQSConfig(
|
||||
namespace,
|
||||
defaultParams = { page: 1, page_size: 5, order_by: 'name' },
|
||||
integerFields = ['page', 'page_size']
|
||||
@ -165,12 +179,16 @@ export function getQSConfig (
|
||||
* @param {string} url query string
|
||||
* @return {object} query param object
|
||||
*/
|
||||
export function parseQueryString (config, queryString) {
|
||||
export function parseQueryString(config, queryString) {
|
||||
if (!queryString) return config.defaultParams;
|
||||
|
||||
const namespacedIntegerFields = config.integerFields.map(f => config.namespace ? `${config.namespace}.${f}` : f);
|
||||
const namespacedIntegerFields = config.integerFields.map(f =>
|
||||
config.namespace ? `${config.namespace}.${f}` : f
|
||||
);
|
||||
|
||||
const keyValuePairs = queryString.replace(/^\?/, '').split('&')
|
||||
const keyValuePairs = queryString
|
||||
.replace(/^\?/, '')
|
||||
.split('&')
|
||||
.map(s => s.split('='))
|
||||
.map(([key, value]) => {
|
||||
if (namespacedIntegerFields.includes(key)) {
|
||||
@ -185,37 +203,38 @@ export function parseQueryString (config, queryString) {
|
||||
// needs to return array for duplicate keys
|
||||
// ie [[k1, v1], [k1, v2], [k2, v3]]
|
||||
// -> [[k1, [v1, v2]], [k2, v3]]
|
||||
const dedupedKeyValuePairs = Object.keys(keyValueObject)
|
||||
.map(key => {
|
||||
const values = keyValuePairs
|
||||
.filter(([k]) => k === key)
|
||||
.map(([, v]) => v);
|
||||
const dedupedKeyValuePairs = Object.keys(keyValueObject).map(key => {
|
||||
const values = keyValuePairs.filter(([k]) => k === key).map(([, v]) => v);
|
||||
|
||||
if (values.length === 1) {
|
||||
return [key, values[0]];
|
||||
}
|
||||
if (values.length === 1) {
|
||||
return [key, values[0]];
|
||||
}
|
||||
|
||||
return [key, values];
|
||||
});
|
||||
return [key, values];
|
||||
});
|
||||
|
||||
const parsed = Object.assign(...dedupedKeyValuePairs.map(([k, v]) => ({
|
||||
[k]: v
|
||||
})));
|
||||
const parsed = Object.assign(
|
||||
...dedupedKeyValuePairs.map(([k, v]) => ({
|
||||
[k]: v,
|
||||
}))
|
||||
);
|
||||
|
||||
const namespacedParams = {};
|
||||
|
||||
Object.keys(parsed)
|
||||
.forEach(field => {
|
||||
if (namespaceMatches(config.namespace, field)) {
|
||||
let fieldname = field;
|
||||
if (config.namespace) {
|
||||
fieldname = field.substr(config.namespace.length + 1);
|
||||
}
|
||||
namespacedParams[fieldname] = parsed[field];
|
||||
Object.keys(parsed).forEach(field => {
|
||||
if (namespaceMatches(config.namespace, field)) {
|
||||
let fieldname = field;
|
||||
if (config.namespace) {
|
||||
fieldname = field.substr(config.namespace.length + 1);
|
||||
}
|
||||
});
|
||||
namespacedParams[fieldname] = parsed[field];
|
||||
}
|
||||
});
|
||||
|
||||
const namespacedDefaults = namespaceParams(config.namespace, config.defaultParams);
|
||||
const namespacedDefaults = namespaceParams(
|
||||
config.namespace,
|
||||
config.defaultParams
|
||||
);
|
||||
|
||||
Object.keys(namespacedDefaults)
|
||||
.filter(key => Object.keys(parsed).indexOf(key) === -1)
|
||||
@ -238,11 +257,12 @@ export function parseQueryString (config, queryString) {
|
||||
* @param {object} namespaced params object of default params
|
||||
* @return {object} namespaced params object of only defaults
|
||||
*/
|
||||
const getDefaultParams = (params, defaults) => toObject(
|
||||
Object.keys(params)
|
||||
.filter(key => Object.keys(defaults).indexOf(key) > -1)
|
||||
.map(key => [key, params[key]])
|
||||
);
|
||||
const getDefaultParams = (params, defaults) =>
|
||||
toObject(
|
||||
Object.keys(params)
|
||||
.filter(key => Object.keys(defaults).indexOf(key) > -1)
|
||||
.map(key => [key, params[key]])
|
||||
);
|
||||
|
||||
/**
|
||||
* helper function to get params that are not defaults
|
||||
@ -250,11 +270,12 @@ const getDefaultParams = (params, defaults) => toObject(
|
||||
* @param {object} namespaced params object of default params
|
||||
* @return {object} namespaced params object of non-defaults
|
||||
*/
|
||||
const getNonDefaultParams = (params, defaults) => toObject(
|
||||
Object.keys(params)
|
||||
.filter(key => Object.keys(defaults).indexOf(key) === -1)
|
||||
.map(key => [key, params[key]])
|
||||
);
|
||||
const getNonDefaultParams = (params, defaults) =>
|
||||
toObject(
|
||||
Object.keys(params)
|
||||
.filter(key => Object.keys(defaults).indexOf(key) === -1)
|
||||
.map(key => [key, params[key]])
|
||||
);
|
||||
|
||||
/**
|
||||
* helper function to merge old and new params together
|
||||
@ -262,9 +283,9 @@ const getNonDefaultParams = (params, defaults) => toObject(
|
||||
* @param {object} namespaced params object of new params
|
||||
* @return {object} merged namespaced params object
|
||||
*/
|
||||
const getMergedParams = (oldParams, newParams) => toObject(
|
||||
Object.keys(oldParams)
|
||||
.map(key => {
|
||||
const getMergedParams = (oldParams, newParams) =>
|
||||
toObject(
|
||||
Object.keys(oldParams).map(key => {
|
||||
let oldVal = oldParams[key];
|
||||
const newVal = newParams[key];
|
||||
if (newVal) {
|
||||
@ -276,7 +297,7 @@ const getMergedParams = (oldParams, newParams) => toObject(
|
||||
}
|
||||
return [key, oldVal];
|
||||
})
|
||||
);
|
||||
);
|
||||
|
||||
/**
|
||||
* helper function to get new params that are not in merged params
|
||||
@ -284,11 +305,12 @@ const getMergedParams = (oldParams, newParams) => toObject(
|
||||
* @param {object} namespaced params object of new params
|
||||
* @return {object} remaining new namespaced params object
|
||||
*/
|
||||
const getRemainingNewParams = (mergedParams, newParams) => toObject(
|
||||
Object.keys(newParams)
|
||||
.filter(key => Object.keys(mergedParams).indexOf(key) === -1)
|
||||
.map(key => [key, newParams[key]])
|
||||
);
|
||||
const getRemainingNewParams = (mergedParams, newParams) =>
|
||||
toObject(
|
||||
Object.keys(newParams)
|
||||
.filter(key => Object.keys(mergedParams).indexOf(key) === -1)
|
||||
.map(key => [key, newParams[key]])
|
||||
);
|
||||
|
||||
/**
|
||||
* Merges existing params of search string with new ones and returns the updated list of params
|
||||
@ -297,13 +319,16 @@ const getRemainingNewParams = (mergedParams, newParams) => toObject(
|
||||
* @param {object} object with new params to add
|
||||
* @return {object} query param object
|
||||
*/
|
||||
export function addParams (config, searchString, newParams) {
|
||||
export function addParams(config, searchString, newParams) {
|
||||
const namespacedOldParams = namespaceParams(
|
||||
config.namespace,
|
||||
parseQueryString(config, searchString)
|
||||
);
|
||||
const namespacedNewParams = namespaceParams(config.namespace, newParams);
|
||||
const namespacedDefaultParams = namespaceParams(config.namespace, config.defaultParams);
|
||||
const namespacedDefaultParams = namespaceParams(
|
||||
config.namespace,
|
||||
config.defaultParams
|
||||
);
|
||||
|
||||
const namespacedOldParamsNotDefaults = getNonDefaultParams(
|
||||
namespacedOldParams,
|
||||
@ -320,7 +345,7 @@ export function addParams (config, searchString, newParams) {
|
||||
return denamespaceParams(config.namespace, {
|
||||
...getDefaultParams(namespacedOldParams, namespacedDefaultParams),
|
||||
...namespacedMergedParams,
|
||||
...getRemainingNewParams(namespacedMergedParams, namespacedNewParams)
|
||||
...getRemainingNewParams(namespacedMergedParams, namespacedNewParams),
|
||||
});
|
||||
}
|
||||
|
||||
@ -331,26 +356,29 @@ export function addParams (config, searchString, newParams) {
|
||||
* @param {object} object with new params to remove
|
||||
* @return {object} query param object
|
||||
*/
|
||||
export function removeParams (config, searchString, paramsToRemove) {
|
||||
export function removeParams(config, searchString, paramsToRemove) {
|
||||
const oldParams = parseQueryString(config, searchString);
|
||||
const paramsEntries = [];
|
||||
Object.entries(oldParams)
|
||||
.forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(val => {
|
||||
paramsEntries.push([key, val]);
|
||||
})
|
||||
} else {
|
||||
paramsEntries.push([key, value]);
|
||||
}
|
||||
})
|
||||
Object.entries(oldParams).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(val => {
|
||||
paramsEntries.push([key, val]);
|
||||
});
|
||||
} else {
|
||||
paramsEntries.push([key, value]);
|
||||
}
|
||||
});
|
||||
const paramsToRemoveEntries = Object.entries(paramsToRemove);
|
||||
const remainingEntries = paramsEntries
|
||||
.filter(([key, value]) => paramsToRemoveEntries
|
||||
.filter(([newKey, newValue]) => key === newKey && value === newValue).length === 0);
|
||||
const remainingEntries = paramsEntries.filter(
|
||||
([key, value]) =>
|
||||
paramsToRemoveEntries.filter(
|
||||
([newKey, newValue]) => key === newKey && value === newValue
|
||||
).length === 0
|
||||
);
|
||||
const remainingObject = toObject(remainingEntries);
|
||||
const defaultEntriesLeftover = Object.entries(config.defaultParams)
|
||||
.filter(([key]) => !remainingObject[key]);
|
||||
const defaultEntriesLeftover = Object.entries(config.defaultParams).filter(
|
||||
([key]) => !remainingObject[key]
|
||||
);
|
||||
const finalParamsEntries = remainingEntries;
|
||||
defaultEntriesLeftover.forEach(value => {
|
||||
finalParamsEntries.push(value);
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
parseQueryString,
|
||||
getQSConfig,
|
||||
addParams,
|
||||
removeParams
|
||||
removeParams,
|
||||
} from './qs';
|
||||
|
||||
describe('qs (qs.js)', () => {
|
||||
@ -13,14 +13,19 @@ describe('qs (qs.js)', () => {
|
||||
[
|
||||
[null, ''],
|
||||
[{}, ''],
|
||||
[{ order_by: 'name', page: 1, page_size: 5 }, 'order_by=name&page=1&page_size=5'],
|
||||
[{ '-order_by': 'name', page: '1', page_size: 5 }, '-order_by=name&page=1&page_size=5'],
|
||||
]
|
||||
.forEach(([params, expectedQueryString]) => {
|
||||
const actualQueryString = encodeQueryString(params);
|
||||
[
|
||||
{ order_by: 'name', page: 1, page_size: 5 },
|
||||
'order_by=name&page=1&page_size=5',
|
||||
],
|
||||
[
|
||||
{ '-order_by': 'name', page: '1', page_size: 5 },
|
||||
'-order_by=name&page=1&page_size=5',
|
||||
],
|
||||
].forEach(([params, expectedQueryString]) => {
|
||||
const actualQueryString = encodeQueryString(params);
|
||||
|
||||
expect(actualQueryString).toEqual(expectedQueryString);
|
||||
});
|
||||
expect(actualQueryString).toEqual(expectedQueryString);
|
||||
});
|
||||
});
|
||||
|
||||
test('encodeQueryString omits null values', () => {
|
||||
@ -35,7 +40,7 @@ describe('qs (qs.js)', () => {
|
||||
describe('encodeNonDefaultQueryString', () => {
|
||||
const config = {
|
||||
namespace: null,
|
||||
defaultParams: { page: 1, page_size: 5, order_by: 'name'},
|
||||
defaultParams: { page: 1, page_size: 5, order_by: 'name' },
|
||||
integerFields: ['page'],
|
||||
};
|
||||
|
||||
@ -45,14 +50,19 @@ describe('qs (qs.js)', () => {
|
||||
[{}, ''],
|
||||
[{ order_by: 'name', page: 1, page_size: 5 }, ''],
|
||||
[{ order_by: '-name', page: 1, page_size: 5 }, 'order_by=-name'],
|
||||
[{ order_by: '-name', page: 3, page_size: 10 }, 'order_by=-name&page=3&page_size=10'],
|
||||
[{ order_by: '-name', page: 3, page_size: 10, foo: 'bar' }, 'foo=bar&order_by=-name&page=3&page_size=10'],
|
||||
]
|
||||
.forEach(([params, expectedQueryString]) => {
|
||||
const actualQueryString = encodeNonDefaultQueryString(config, params);
|
||||
[
|
||||
{ order_by: '-name', page: 3, page_size: 10 },
|
||||
'order_by=-name&page=3&page_size=10',
|
||||
],
|
||||
[
|
||||
{ order_by: '-name', page: 3, page_size: 10, foo: 'bar' },
|
||||
'foo=bar&order_by=-name&page=3&page_size=10',
|
||||
],
|
||||
].forEach(([params, expectedQueryString]) => {
|
||||
const actualQueryString = encodeNonDefaultQueryString(config, params);
|
||||
|
||||
expect(actualQueryString).toEqual(expectedQueryString);
|
||||
});
|
||||
expect(actualQueryString).toEqual(expectedQueryString);
|
||||
});
|
||||
});
|
||||
|
||||
test('encodeNonDefaultQueryString omits null values', () => {
|
||||
@ -114,7 +124,7 @@ describe('qs (qs.js)', () => {
|
||||
const query = '';
|
||||
expect(parseQueryString(config, query)).toEqual({
|
||||
page: 1,
|
||||
page_size: 15
|
||||
page_size: 15,
|
||||
});
|
||||
});
|
||||
|
||||
@ -222,7 +232,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?baz=bar&page=3';
|
||||
const newParams = { bag: 'boom' }
|
||||
const newParams = { bag: 'boom' };
|
||||
expect(addParams(config, query, newParams)).toEqual({
|
||||
baz: 'bar',
|
||||
bag: 'boom',
|
||||
@ -238,7 +248,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?baz=bar&baz=bang&page=3';
|
||||
const newParams = { baz: 'boom' }
|
||||
const newParams = { baz: 'boom' };
|
||||
expect(addParams(config, query, newParams)).toEqual({
|
||||
baz: ['bar', 'bang', 'boom'],
|
||||
page: 3,
|
||||
@ -253,7 +263,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?baz=bar&baz=bang&page=3';
|
||||
const newParams = { page: 5 }
|
||||
const newParams = { page: 5 };
|
||||
expect(addParams(config, query, newParams)).toEqual({
|
||||
baz: ['bar', 'bang'],
|
||||
page: 5,
|
||||
@ -268,7 +278,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?baz=bar&baz=bang&page=3';
|
||||
const newParams = { baz: 'bust', pat: 'pal' }
|
||||
const newParams = { baz: 'bust', pat: 'pal' };
|
||||
expect(addParams(config, query, newParams)).toEqual({
|
||||
baz: ['bar', 'bang', 'bust'],
|
||||
pat: 'pal',
|
||||
@ -284,7 +294,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?item.baz=bar&item.page=3';
|
||||
const newParams = { bag: 'boom' }
|
||||
const newParams = { bag: 'boom' };
|
||||
expect(addParams(config, query, newParams)).toEqual({
|
||||
baz: 'bar',
|
||||
bag: 'boom',
|
||||
@ -300,7 +310,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?item.baz=bar&foo.page=3';
|
||||
const newParams = { bag: 'boom' }
|
||||
const newParams = { bag: 'boom' };
|
||||
expect(addParams(config, query, newParams)).toEqual({
|
||||
baz: 'bar',
|
||||
bag: 'boom',
|
||||
@ -316,7 +326,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?item.baz=bar&item.baz=bang&item.page=3';
|
||||
const newParams = { baz: 'boom' }
|
||||
const newParams = { baz: 'boom' };
|
||||
expect(addParams(config, query, newParams)).toEqual({
|
||||
baz: ['bar', 'bang', 'boom'],
|
||||
page: 3,
|
||||
@ -331,7 +341,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?item.baz=bar&item.baz=bang&item.page=3';
|
||||
const newParams = { page: 5 }
|
||||
const newParams = { page: 5 };
|
||||
expect(addParams(config, query, newParams)).toEqual({
|
||||
baz: ['bar', 'bang'],
|
||||
page: 5,
|
||||
@ -346,7 +356,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?item.baz=bar&item.baz=bang&item.page=3';
|
||||
const newParams = { baz: 'bust', pat: 'pal' }
|
||||
const newParams = { baz: 'bust', pat: 'pal' };
|
||||
expect(addParams(config, query, newParams)).toEqual({
|
||||
baz: ['bar', 'bang', 'bust'],
|
||||
pat: 'pal',
|
||||
@ -364,7 +374,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?baz=bar&page=3&bag=boom';
|
||||
const newParams = { bag: 'boom' }
|
||||
const newParams = { bag: 'boom' };
|
||||
expect(removeParams(config, query, newParams)).toEqual({
|
||||
baz: 'bar',
|
||||
page: 3,
|
||||
@ -379,7 +389,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?baz=bar&baz=bang&page=3';
|
||||
const newParams = { baz: 'bar' }
|
||||
const newParams = { baz: 'bar' };
|
||||
expect(removeParams(config, query, newParams)).toEqual({
|
||||
baz: 'bang',
|
||||
page: 3,
|
||||
@ -394,7 +404,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?baz=bar&baz=bang&baz=bust&page=3';
|
||||
const newParams = { baz: 'bar' }
|
||||
const newParams = { baz: 'bar' };
|
||||
expect(removeParams(config, query, newParams)).toEqual({
|
||||
baz: ['bang', 'bust'],
|
||||
page: 3,
|
||||
@ -409,7 +419,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?baz=bar&baz=bang&page=3';
|
||||
const newParams = { page: 3 }
|
||||
const newParams = { page: 3 };
|
||||
expect(removeParams(config, query, newParams)).toEqual({
|
||||
baz: ['bar', 'bang'],
|
||||
page: 1,
|
||||
@ -424,7 +434,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?baz=bar&baz=bang&baz=bust&pat=pal&page=3';
|
||||
const newParams = { baz: 'bust', pat: 'pal' }
|
||||
const newParams = { baz: 'bust', pat: 'pal' };
|
||||
expect(removeParams(config, query, newParams)).toEqual({
|
||||
baz: ['bar', 'bang'],
|
||||
page: 3,
|
||||
@ -439,7 +449,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?item.baz=bar&item.page=3';
|
||||
const newParams = { baz: 'bar' }
|
||||
const newParams = { baz: 'bar' };
|
||||
expect(removeParams(config, query, newParams)).toEqual({
|
||||
page: 3,
|
||||
page_size: 15,
|
||||
@ -453,7 +463,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?item.baz=bar&foo.page=3';
|
||||
const newParams = { baz: 'bar' }
|
||||
const newParams = { baz: 'bar' };
|
||||
expect(removeParams(config, query, newParams)).toEqual({
|
||||
page: 1,
|
||||
page_size: 15,
|
||||
@ -467,7 +477,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?item.baz=bar&item.baz=bang&item.page=3';
|
||||
const newParams = { baz: 'bar' }
|
||||
const newParams = { baz: 'bar' };
|
||||
expect(removeParams(config, query, newParams)).toEqual({
|
||||
baz: 'bang',
|
||||
page: 3,
|
||||
@ -482,7 +492,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?item.baz=bar&item.baz=bang&item.baz=bust&item.page=3';
|
||||
const newParams = { baz: 'bar' }
|
||||
const newParams = { baz: 'bar' };
|
||||
expect(removeParams(config, query, newParams)).toEqual({
|
||||
baz: ['bang', 'bust'],
|
||||
page: 3,
|
||||
@ -497,7 +507,7 @@ describe('qs (qs.js)', () => {
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?item.baz=bar&item.baz=bang&item.page=3';
|
||||
const newParams = { page: 3 }
|
||||
const newParams = { page: 3 };
|
||||
expect(removeParams(config, query, newParams)).toEqual({
|
||||
baz: ['bar', 'bang'],
|
||||
page: 1,
|
||||
@ -511,8 +521,9 @@ describe('qs (qs.js)', () => {
|
||||
defaultParams: { page: 1, page_size: 15 },
|
||||
integerFields: ['page', 'page_size'],
|
||||
};
|
||||
const query = '?item.baz=bar&item.baz=bang&item.baz=bust&item.pat=pal&item.page=3';
|
||||
const newParams = { baz: 'bust', pat: 'pal' }
|
||||
const query =
|
||||
'?item.baz=bar&item.baz=bang&item.baz=bust&item.pat=pal&item.page=3';
|
||||
const newParams = { baz: 'bust', pat: 'pal' };
|
||||
expect(removeParams(config, query, newParams)).toEqual({
|
||||
baz: ['bar', 'bang'],
|
||||
page: 3,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user