updates based on pr feedback

run prettier
update hasContentError to contentError in all the places
function naming updates
This commit is contained in:
John Mitchell
2019-07-26 10:03:46 -04:00
parent 357887417c
commit bdfeb2cb9c
31 changed files with 929 additions and 664 deletions

View File

@@ -1,48 +1,46 @@
import axios from 'axios'; import axios from 'axios';
import { import { encodeQueryString } from '@util/qs';
encodeQueryString
} from '@util/qs';
const defaultHttp = axios.create({ const defaultHttp = axios.create({
xsrfCookieName: 'csrftoken', xsrfCookieName: 'csrftoken',
xsrfHeaderName: 'X-CSRFToken', xsrfHeaderName: 'X-CSRFToken',
paramsSerializer(params) { paramsSerializer(params) {
return encodeQueryString(params); return encodeQueryString(params);
} },
}); });
class Base { class Base {
constructor (http = defaultHttp, baseURL) { constructor(http = defaultHttp, baseURL) {
this.http = http; this.http = http;
this.baseUrl = baseURL; this.baseUrl = baseURL;
} }
create (data) { create(data) {
return this.http.post(this.baseUrl, data); return this.http.post(this.baseUrl, data);
} }
destroy (id) { destroy(id) {
return this.http.delete(`${this.baseUrl}${id}/`); return this.http.delete(`${this.baseUrl}${id}/`);
} }
read (params) { read(params) {
return this.http.get(this.baseUrl, { params }); return this.http.get(this.baseUrl, { params });
} }
readDetail (id) { readDetail(id) {
return this.http.get(`${this.baseUrl}${id}/`); return this.http.get(`${this.baseUrl}${id}/`);
} }
readOptions () { readOptions() {
return this.http.options(this.baseUrl); return this.http.options(this.baseUrl);
} }
replace (id, data) { replace(id, data) {
return this.http.put(`${this.baseUrl}${id}/`, data); return this.http.put(`${this.baseUrl}${id}/`, data);
} }
update (id, data) { update(id, data) {
return this.http.patch(`${this.baseUrl}${id}/`, data); return this.http.patch(`${this.baseUrl}${id}/`, data);
} }
} }

View File

@@ -3,14 +3,14 @@ import Base from './Base';
describe('Base', () => { describe('Base', () => {
const createPromise = () => Promise.resolve(); const createPromise = () => Promise.resolve();
const mockBaseURL = '/api/v2/organizations/'; const mockBaseURL = '/api/v2/organizations/';
const mockHttp = ({ const mockHttp = {
delete: jest.fn(createPromise), delete: jest.fn(createPromise),
get: jest.fn(createPromise), get: jest.fn(createPromise),
options: jest.fn(createPromise), options: jest.fn(createPromise),
patch: jest.fn(createPromise), patch: jest.fn(createPromise),
post: jest.fn(createPromise), post: jest.fn(createPromise),
put: jest.fn(createPromise) put: jest.fn(createPromise),
}); };
const BaseAPI = new Base(mockHttp, mockBaseURL); const BaseAPI = new Base(mockHttp, mockBaseURL);
@@ -18,7 +18,7 @@ describe('Base', () => {
jest.clearAllMocks(); 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 ' }; const data = { name: 'test ' };
await BaseAPI.create(data); await BaseAPI.create(data);
@@ -28,19 +28,21 @@ describe('Base', () => {
done(); done();
}); });
test('destroy calls http method with expected data', async (done) => { test('destroy calls http method with expected data', async done => {
const resourceId = 1; const resourceId = 1;
await BaseAPI.destroy(resourceId); await BaseAPI.destroy(resourceId);
expect(mockHttp.delete).toHaveBeenCalledTimes(1); 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(); 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 testParams = { foo: 'bar' };
const testParamsDuplicates = { foo: ['bar', 'baz']}; const testParamsDuplicates = { foo: ['bar', 'baz'] };
await BaseAPI.read(testParams); await BaseAPI.read(testParams);
await BaseAPI.read(); await BaseAPI.read();
@@ -48,25 +50,29 @@ describe('Base', () => {
expect(mockHttp.get).toHaveBeenCalledTimes(3); expect(mockHttp.get).toHaveBeenCalledTimes(3);
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`); 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][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][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(); done();
}); });
test('readDetail calls http method with expected data', async (done) => { test('readDetail calls http method with expected data', async done => {
const resourceId = 1; const resourceId = 1;
await BaseAPI.readDetail(resourceId); await BaseAPI.readDetail(resourceId);
expect(mockHttp.get).toHaveBeenCalledTimes(1); 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(); done();
}); });
test('readOptions calls http method with expected data', async (done) => { test('readOptions calls http method with expected data', async done => {
await BaseAPI.readOptions(); await BaseAPI.readOptions();
expect(mockHttp.options).toHaveBeenCalledTimes(1); expect(mockHttp.options).toHaveBeenCalledTimes(1);
@@ -74,27 +80,31 @@ describe('Base', () => {
done(); 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 resourceId = 1;
const data = { name: 'test ' }; const data = { name: 'test ' };
await BaseAPI.replace(resourceId, data); await BaseAPI.replace(resourceId, data);
expect(mockHttp.put).toHaveBeenCalledTimes(1); 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); expect(mockHttp.put.mock.calls[0][1]).toEqual(data);
done(); 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 resourceId = 1;
const data = { name: 'test ' }; const data = { name: 'test ' };
await BaseAPI.update(resourceId, data); await BaseAPI.update(resourceId, data);
expect(mockHttp.patch).toHaveBeenCalledTimes(1); 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); expect(mockHttp.patch.mock.calls[0][1]).toEqual(data);
done(); done();

View File

@@ -1,15 +1,24 @@
const InstanceGroupsMixin = (parent) => class extends parent { const InstanceGroupsMixin = parent =>
readInstanceGroups (resourceId, params) { class extends parent {
return this.http.get(`${this.baseUrl}${resourceId}/instance_groups/`, params); readInstanceGroups(resourceId, params) {
} return this.http.get(
`${this.baseUrl}${resourceId}/instance_groups/`,
params
);
}
associateInstanceGroup (resourceId, instanceGroupId) { associateInstanceGroup(resourceId, instanceGroupId) {
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId }); return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, {
} id: instanceGroupId,
});
}
disassociateInstanceGroup (resourceId, instanceGroupId) { disassociateInstanceGroup(resourceId, instanceGroupId) {
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId, disassociate: true }); return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, {
} id: instanceGroupId,
}; disassociate: true,
});
}
};
export default InstanceGroupsMixin; export default InstanceGroupsMixin;

View File

@@ -1,65 +1,106 @@
const NotificationsMixin = (parent) => class extends parent { const NotificationsMixin = parent =>
readOptionsNotificationTemplates(id) { class extends parent {
return this.http.options(`${this.baseUrl}${id}/notification_templates/`); 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);
} }
if (notificationType === 'success' && associationState === false) { readNotificationTemplates(id, params) {
return this.disassociateNotificationTemplatesSuccess(resourceId, notificationId); return this.http.get(
`${this.baseUrl}${id}/notification_templates/`,
params
);
} }
if (notificationType === 'error' && associationState === true) { readNotificationTemplatesSuccess(id, params) {
return this.associateNotificationTemplatesError(resourceId, notificationId); return this.http.get(
`${this.baseUrl}${id}/notification_templates_success/`,
params
);
} }
if (notificationType === 'error' && associationState === false) { readNotificationTemplatesError(id, params) {
return this.disassociateNotificationTemplatesError(resourceId, notificationId); 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; export default NotificationsMixin;

View File

@@ -3,16 +3,16 @@ import NotificationsMixin from '../mixins/Notifications.mixin';
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin'; import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
constructor (http) { constructor(http) {
super(http); super(http);
this.baseUrl = '/api/v2/organizations/'; this.baseUrl = '/api/v2/organizations/';
} }
readAccessList (id, params) { readAccessList(id, params) {
return this.http.get(`${this.baseUrl}${id}/access_list/`, { 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 }); return this.http.get(`${this.baseUrl}${id}/teams/`, { params });
} }
} }

View File

@@ -4,7 +4,7 @@ import { describeNotificationMixin } from '../../../testUtils/apiReusable';
describe('OrganizationsAPI', () => { describe('OrganizationsAPI', () => {
const orgId = 1; const orgId = 1;
const createPromise = () => Promise.resolve(); const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise) }); const mockHttp = { get: jest.fn(createPromise) };
const OrganizationsAPI = new Organizations(mockHttp); const OrganizationsAPI = new Organizations(mockHttp);
@@ -12,9 +12,9 @@ describe('OrganizationsAPI', () => {
jest.clearAllMocks(); 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 testParams = { foo: 'bar' };
const testParamsDuplicates = { foo: ['bar', 'baz']}; const testParamsDuplicates = { foo: ['bar', 'baz'] };
const mockBaseURL = `/api/v2/organizations/${orgId}/access_list/`; const mockBaseURL = `/api/v2/organizations/${orgId}/access_list/`;
@@ -24,17 +24,19 @@ describe('OrganizationsAPI', () => {
expect(mockHttp.get).toHaveBeenCalledTimes(3); expect(mockHttp.get).toHaveBeenCalledTimes(3);
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`); 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][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][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(); 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 testParams = { foo: 'bar' };
const testParamsDuplicates = { foo: ['bar', 'baz']}; const testParamsDuplicates = { foo: ['bar', 'baz'] };
const mockBaseURL = `/api/v2/organizations/${orgId}/teams/`; const mockBaseURL = `/api/v2/organizations/${orgId}/teams/`;
@@ -44,11 +46,13 @@ describe('OrganizationsAPI', () => {
expect(mockHttp.get).toHaveBeenCalledTimes(3); expect(mockHttp.get).toHaveBeenCalledTimes(3);
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`); 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][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][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(); done();
}); });
}); });

View File

@@ -8,14 +8,13 @@ import SelectRoleStep from './SelectRoleStep';
import SelectableCard from './SelectableCard'; import SelectableCard from './SelectableCard';
import { TeamsAPI, UsersAPI } from '../../api'; import { TeamsAPI, UsersAPI } from '../../api';
const readUsers = async (queryParams) => UsersAPI.read( const readUsers = async queryParams =>
Object.assign(queryParams, { is_superuser: false }) 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 { class AddResourceRole extends React.Component {
constructor (props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@@ -25,67 +24,69 @@ class AddResourceRole extends React.Component {
currentStepId: 1, currentStepId: 1,
}; };
this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind(this); this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind(
this
);
this.handleResourceSelect = this.handleResourceSelect.bind(this); this.handleResourceSelect = this.handleResourceSelect.bind(this);
this.handleRoleCheckboxClick = this.handleRoleCheckboxClick.bind(this); this.handleRoleCheckboxClick = this.handleRoleCheckboxClick.bind(this);
this.handleWizardNext = this.handleWizardNext.bind(this); this.handleWizardNext = this.handleWizardNext.bind(this);
this.handleWizardSave = this.handleWizardSave.bind(this); this.handleWizardSave = this.handleWizardSave.bind(this);
} }
handleResourceCheckboxClick (user) { handleResourceCheckboxClick(user) {
const { selectedResourceRows } = this.state; const { selectedResourceRows } = this.state;
const selectedIndex = selectedResourceRows const selectedIndex = selectedResourceRows.findIndex(
.findIndex(selectedRow => selectedRow.id === user.id); selectedRow => selectedRow.id === user.id
);
if (selectedIndex > -1) { if (selectedIndex > -1) {
selectedResourceRows.splice(selectedIndex, 1); selectedResourceRows.splice(selectedIndex, 1);
this.setState({ selectedResourceRows }); this.setState({ selectedResourceRows });
} else { } else {
this.setState(prevState => ({ this.setState(prevState => ({
selectedResourceRows: [...prevState.selectedResourceRows, user] selectedResourceRows: [...prevState.selectedResourceRows, user],
})); }));
} }
} }
handleRoleCheckboxClick (role) { handleRoleCheckboxClick(role) {
const { selectedRoleRows } = this.state; const { selectedRoleRows } = this.state;
const selectedIndex = selectedRoleRows const selectedIndex = selectedRoleRows.findIndex(
.findIndex(selectedRow => selectedRow.id === role.id); selectedRow => selectedRow.id === role.id
);
if (selectedIndex > -1) { if (selectedIndex > -1) {
selectedRoleRows.splice(selectedIndex, 1); selectedRoleRows.splice(selectedIndex, 1);
this.setState({ selectedRoleRows }); this.setState({ selectedRoleRows });
} else { } else {
this.setState(prevState => ({ this.setState(prevState => ({
selectedRoleRows: [...prevState.selectedRoleRows, role] selectedRoleRows: [...prevState.selectedRoleRows, role],
})); }));
} }
} }
handleResourceSelect (resourceType) { handleResourceSelect(resourceType) {
this.setState({ this.setState({
selectedResource: resourceType, selectedResource: resourceType,
selectedResourceRows: [], selectedResourceRows: [],
selectedRoleRows: [] selectedRoleRows: [],
}); });
} }
handleWizardNext (step) { handleWizardNext(step) {
this.setState({ this.setState({
currentStepId: step.id, currentStepId: step.id,
}); });
} }
async handleWizardSave () { async handleWizardSave() {
const { const { onSave } = this.props;
onSave
} = this.props;
const { const {
selectedResourceRows, selectedResourceRows,
selectedRoleRows, selectedRoleRows,
selectedResource selectedResource,
} = this.state; } = this.state;
try { try {
@@ -95,11 +96,17 @@ class AddResourceRole extends React.Component {
for (let j = 0; j < selectedRoleRows.length; j++) { for (let j = 0; j < selectedRoleRows.length; j++) {
if (selectedResource === 'users') { if (selectedResource === 'users') {
roleRequests.push( roleRequests.push(
UsersAPI.associateRole(selectedResourceRows[i].id, selectedRoleRows[j].id) UsersAPI.associateRole(
selectedResourceRows[i].id,
selectedRoleRows[j].id
)
); );
} else if (selectedResource === 'teams') { } else if (selectedResource === 'teams') {
roleRequests.push( 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 { const {
selectedResource, selectedResource,
selectedResourceRows, selectedResourceRows,
selectedRoleRows, selectedRoleRows,
currentStepId, currentStepId,
} = this.state; } = this.state;
const { const { onClose, roles, i18n } = this.props;
onClose,
roles,
i18n
} = this.props;
const userColumns = [ const userColumns = [
{ name: i18n._(t`Username`), key: 'username', isSortable: true, isSearchable: true } {
name: i18n._(t`Username`),
key: 'username',
isSortable: true,
isSearchable: true,
},
]; ];
const teamColumns = [ const teamColumns = [
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true } {
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
]; ];
let wizardTitle = ''; let wizardTitle = '';
@@ -164,7 +177,7 @@ class AddResourceRole extends React.Component {
/> />
</div> </div>
), ),
enableNext: selectedResource !== null enableNext: selectedResource !== null,
}, },
{ {
id: 2, id: 2,
@@ -195,7 +208,7 @@ class AddResourceRole extends React.Component {
)} )}
</Fragment> </Fragment>
), ),
enableNext: selectedResourceRows.length > 0 enableNext: selectedResourceRows.length > 0,
}, },
{ {
id: 3, id: 3,
@@ -211,8 +224,8 @@ class AddResourceRole extends React.Component {
/> />
), ),
nextButtonText: i18n._(t`Save`), nextButtonText: i18n._(t`Save`),
enableNext: selectedRoleRows.length > 0 enableNext: selectedRoleRows.length > 0,
} },
]; ];
const currentStep = steps.find(step => step.id === currentStepId); const currentStep = steps.find(step => step.id === currentStepId);
@@ -236,11 +249,11 @@ class AddResourceRole extends React.Component {
AddResourceRole.propTypes = { AddResourceRole.propTypes = {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired, onSave: PropTypes.func.isRequired,
roles: PropTypes.shape() roles: PropTypes.shape(),
}; };
AddResourceRole.defaultProps = { AddResourceRole.defaultProps = {
roles: {} roles: {},
}; };
export { AddResourceRole as _AddResourceRole }; export { AddResourceRole as _AddResourceRole };

View File

@@ -40,10 +40,7 @@ class SelectResourceStep extends React.Component {
async readResourceList() { async readResourceList() {
const { onSearch, location } = this.props; const { onSearch, location } = this.props;
const queryParams = parseQueryString( const queryParams = parseQueryString(this.qsConfig, location.search);
this.qsConfig,
location.search
);
this.setState({ this.setState({
isLoading: true, isLoading: true,

View File

@@ -7,7 +7,7 @@ import SelectResourceStep from './SelectResourceStep';
describe('<SelectResourceStep />', () => { describe('<SelectResourceStep />', () => {
const columns = [ const columns = [
{ name: 'Username', key: 'username', isSortable: true, isSearchable: true } { name: 'Username', key: 'username', isSortable: true, isSearchable: true },
]; ];
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); jest.restoreAllMocks();
@@ -30,9 +30,9 @@ describe('<SelectResourceStep />', () => {
count: 2, count: 2,
results: [ results: [
{ id: 1, username: 'foo', url: 'item/1' }, { id: 1, username: 'foo', url: 'item/1' },
{ id: 2, username: 'bar', url: 'item/2' } { id: 2, username: 'bar', url: 'item/2' },
] ],
} },
}); });
mountWithContexts( mountWithContexts(
<SelectResourceStep <SelectResourceStep
@@ -46,25 +46,25 @@ describe('<SelectResourceStep />', () => {
expect(handleSearch).toHaveBeenCalledWith({ expect(handleSearch).toHaveBeenCalledWith({
order_by: 'username', order_by: 'username',
page: 1, page: 1,
page_size: 5 page_size: 5,
}); });
}); });
test('readResourceList properly adds rows to state', async () => { test('readResourceList properly adds rows to state', async () => {
const selectedResourceRows = [ const selectedResourceRows = [{ id: 1, username: 'foo', url: 'item/1' }];
{ id: 1, username: 'foo', url: 'item/1' }
];
const handleSearch = jest.fn().mockResolvedValue({ const handleSearch = jest.fn().mockResolvedValue({
data: { data: {
count: 2, count: 2,
results: [ results: [
{ id: 1, username: 'foo', url: 'item/1' }, { id: 1, username: 'foo', url: 'item/1' },
{ id: 2, username: 'bar', url: 'item/2' } { id: 2, username: 'bar', url: 'item/2' },
] ],
} },
}); });
const history = createMemoryHistory({ 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( const wrapper = await mountWithContexts(
<SelectResourceStep <SelectResourceStep
@@ -74,7 +74,10 @@ describe('<SelectResourceStep />', () => {
onSearch={handleSearch} onSearch={handleSearch}
selectedResourceRows={selectedResourceRows} selectedResourceRows={selectedResourceRows}
sortedColumnKey="username" sortedColumnKey="username"
/>, { context: { router: { history, route: { location: history.location } } } } />,
{
context: { router: { history, route: { location: history.location } } },
}
).find('SelectResourceStep'); ).find('SelectResourceStep');
await wrapper.instance().readResourceList(); await wrapper.instance().readResourceList();
expect(handleSearch).toHaveBeenCalledWith({ expect(handleSearch).toHaveBeenCalledWith({
@@ -84,7 +87,7 @@ describe('<SelectResourceStep />', () => {
}); });
expect(wrapper.state('resources')).toEqual([ expect(wrapper.state('resources')).toEqual([
{ id: 1, username: 'foo', url: 'item/1' }, { 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, count: 2,
results: [ results: [
{ id: 1, username: 'foo', url: 'item/1' }, { id: 1, username: 'foo', url: 'item/1' },
{ id: 2, username: 'bar', url: 'item/2' } { id: 2, username: 'bar', url: 'item/2' },
] ],
}; };
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<SelectResourceStep <SelectResourceStep
@@ -111,7 +114,9 @@ describe('<SelectResourceStep />', () => {
wrapper.update(); wrapper.update();
const checkboxListItemWrapper = wrapper.find('CheckboxListItem'); const checkboxListItemWrapper = wrapper.find('CheckboxListItem');
expect(checkboxListItemWrapper.length).toBe(2); expect(checkboxListItemWrapper.length).toBe(2);
checkboxListItemWrapper.first().find('input[type="checkbox"]') checkboxListItemWrapper
.first()
.find('input[type="checkbox"]')
.simulate('change', { target: { checked: true } }); .simulate('change', { target: { checked: true } });
expect(handleRowClick).toHaveBeenCalledWith(data.results[0]); expect(handleRowClick).toHaveBeenCalledWith(data.results[0]);
}); });

View File

@@ -3,14 +3,21 @@ import { number, bool } from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import Chip from './Chip'; import Chip from './Chip';
const ChipGroup = ({ children, className, showOverflowAfter, displayAll, ...props }) => { const ChipGroup = ({
children,
className,
showOverflowAfter,
displayAll,
...props
}) => {
const [isExpanded, setIsExpanded] = useState(!showOverflowAfter); const [isExpanded, setIsExpanded] = useState(!showOverflowAfter);
const toggleIsOpen = () => setIsExpanded(!isExpanded); const toggleIsOpen = () => setIsExpanded(!isExpanded);
const mappedChildren = React.Children.map(children, c => ( const mappedChildren = React.Children.map(children, c =>
React.cloneElement(c, { component: 'li' }) React.cloneElement(c, { component: 'li' })
)); );
const showOverflowToggle = showOverflowAfter && children.length > showOverflowAfter; const showOverflowToggle =
showOverflowAfter && children.length > showOverflowAfter;
const numToShow = isExpanded const numToShow = isExpanded
? children.length ? children.length
: Math.min(showOverflowAfter, children.length); : Math.min(showOverflowAfter, children.length);
@@ -30,17 +37,17 @@ const ChipGroup = ({ children, className, showOverflowAfter, displayAll, ...prop
}; };
ChipGroup.propTypes = { ChipGroup.propTypes = {
showOverflowAfter: number, showOverflowAfter: number,
displayAll: bool displayAll: bool,
}; };
ChipGroup.defaultProps = { ChipGroup.defaultProps = {
showOverflowAfter: null, showOverflowAfter: null,
displayAll: false displayAll: false,
}; };
export default styled(ChipGroup)` export default styled(ChipGroup)`
--pf-c-chip-group--c-chip--MarginRight: 10px; --pf-c-chip-group--c-chip--MarginRight: 10px;
--pf-c-chip-group--c-chip--MarginBottom: 10px; --pf-c-chip-group--c-chip--MarginBottom: 10px;
> .pf-c-chip.pf-m-overflow button { > .pf-c-chip.pf-m-overflow button {
padding: 3px 8px; padding: 3px 8px;
} }

View File

@@ -13,7 +13,9 @@ describe('<DataListToolbar />', () => {
}); });
test('it triggers the expected callbacks', () => { 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 search = 'button[aria-label="Search submit button"]';
const searchTextInput = 'input[aria-label="Search text input"]'; const searchTextInput = 'input[aria-label="Search text input"]';
@@ -59,14 +61,16 @@ describe('<DataListToolbar />', () => {
test('dropdown items sortable/searchable columns work', () => { test('dropdown items sortable/searchable columns work', () => {
const sortDropdownToggleSelector = 'button[id="awx-sort"]'; const sortDropdownToggleSelector = 'button[id="awx-sort"]';
const searchDropdownToggleSelector = 'button[id="awx-search"]'; const searchDropdownToggleSelector = 'button[id="awx-search"]';
const sortDropdownMenuItems = 'DropdownMenu > ul[aria-labelledby="awx-sort"]'; const sortDropdownMenuItems =
const searchDropdownMenuItems = 'DropdownMenu > ul[aria-labelledby="awx-search"]'; 'DropdownMenu > ul[aria-labelledby="awx-sort"]';
const searchDropdownMenuItems =
'DropdownMenu > ul[aria-labelledby="awx-search"]';
const multipleColumns = [ const multipleColumns = [
{ name: 'Foo', key: 'foo', isSortable: true, isSearchable: true }, { name: 'Foo', key: 'foo', isSortable: true, isSearchable: true },
{ name: 'Bar', key: 'bar', isSortable: true, isSearchable: true }, { name: 'Bar', key: 'bar', isSortable: true, isSearchable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true }, { name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' } { name: 'Baz', key: 'baz' },
]; ];
const onSort = jest.fn(); const onSort = jest.fn();
@@ -103,12 +107,16 @@ describe('<DataListToolbar />', () => {
); );
toolbar.update(); toolbar.update();
const sortDropdownToggleDescending = toolbar.find(sortDropdownToggleSelector); const sortDropdownToggleDescending = toolbar.find(
sortDropdownToggleSelector
);
expect(sortDropdownToggleDescending.length).toBe(1); expect(sortDropdownToggleDescending.length).toBe(1);
sortDropdownToggleDescending.simulate('click'); sortDropdownToggleDescending.simulate('click');
toolbar.update(); toolbar.update();
const sortDropdownItemsDescending = toolbar.find(sortDropdownMenuItems).children(); const sortDropdownItemsDescending = toolbar
.find(sortDropdownMenuItems)
.children();
expect(sortDropdownItemsDescending.length).toBe(2); expect(sortDropdownItemsDescending.length).toBe(2);
sortDropdownToggleDescending.simulate('click'); // toggle close the sort dropdown sortDropdownToggleDescending.simulate('click'); // toggle close the sort dropdown
@@ -134,8 +142,12 @@ describe('<DataListToolbar />', () => {
const downAlphaIconSelector = 'SortAlphaDownIcon'; const downAlphaIconSelector = 'SortAlphaDownIcon';
const upAlphaIconSelector = 'SortAlphaUpIcon'; const upAlphaIconSelector = 'SortAlphaUpIcon';
const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }]; const numericColumns = [
const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }]; { name: 'ID', key: 'id', isSortable: true, isNumeric: true },
];
const alphaColumns = [
{ name: 'Name', key: 'name', isSortable: true, isNumeric: false },
];
toolbar = mountWithContexts( toolbar = mountWithContexts(
<DataListToolbar <DataListToolbar
@@ -183,7 +195,9 @@ describe('<DataListToolbar />', () => {
}); });
test('should render additionalControls', () => { 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 onSearch = jest.fn();
const onSort = jest.fn(); const onSort = jest.fn();
const onSelectAll = jest.fn(); const onSelectAll = jest.fn();
@@ -194,7 +208,11 @@ describe('<DataListToolbar />', () => {
onSearch={onSearch} onSearch={onSearch}
onSort={onSort} onSort={onSort}
onSelectAll={onSelectAll} onSelectAll={onSelectAll}
additionalControls={[<button key="1" id="test" type="button">click</button>]} additionalControls={[
<button key="1" id="test" type="button">
click
</button>,
]}
/> />
); );

View File

@@ -9,19 +9,19 @@ import { ChipGroup, Chip } from '@components/Chip';
import VerticalSeparator from '@components/VerticalSeparator'; import VerticalSeparator from '@components/VerticalSeparator';
const FilterTagsRow = styled.div` const FilterTagsRow = styled.div`
display: flex; display: flex;
padding: 15px 20px; padding: 15px 20px;
border-top: 1px solid #d2d2d2; border-top: 1px solid #d2d2d2;
font-size: 14px; font-size: 14px;
align-items: center; align-items: center;
`; `;
const ResultCount = styled.span` const ResultCount = styled.span`
font-weight: bold; font-weight: bold;
`; `;
const FilterLabel = styled.span` const FilterLabel = styled.span`
padding-right: 20px; padding-right: 20px;
`; `;
// remove non-default query params so they don't show up as filter tags // 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); 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 queryParams = parseQueryString(qsConfig, location.search);
const queryParamsArr = []; const queryParamsArr = [];
const displayAll = true; const displayAll = true;
const nonDefaultParams = filterDefaultParams(Object.keys(queryParams), qsConfig); const nonDefaultParams = filterDefaultParams(
nonDefaultParams Object.keys(queryParams),
.forEach(key => { qsConfig
if (Array.isArray(queryParams[key])) { );
queryParams[key].forEach(val => queryParamsArr.push({ key, value: val })); nonDefaultParams.forEach(key => {
} else { if (Array.isArray(queryParams[key])) {
queryParamsArr.push({ key, value: queryParams[key] }); queryParams[key].forEach(val => queryParamsArr.push({ key, value: val }));
} } else {
}); queryParamsArr.push({ key, value: queryParams[key] });
}
});
return (queryParamsArr.length > 0) && ( return (
<FilterTagsRow> queryParamsArr.length > 0 && (
<ResultCount> <FilterTagsRow>
{`${itemCount} results`} <ResultCount>{`${itemCount} results`}</ResultCount>
</ResultCount> <VerticalSeparator />
<VerticalSeparator /> <FilterLabel>{i18n._(t`Active Filters:`)}</FilterLabel>
<FilterLabel>{i18n._(t`Active Filters:`)}</FilterLabel> <ChipGroup displayAll={displayAll}>
<ChipGroup displayAll={displayAll}> {queryParamsArr.map(({ key, value }) => (
{queryParamsArr.map(({ key, value }) => ( <Chip
<Chip className="searchTagChip"
className="searchTagChip" key={`${key}__${value}`}
key={`${key}__${value}`} isReadOnly={false}
isReadOnly={false} onClick={() => onRemove(key, value)}
onClick={() => onRemove(key, value)} >
> {value}
{value} </Chip>
</Chip> ))}
))} <div className="pf-c-chip pf-m-overflow">
<div className="pf-c-chip pf-m-overflow"> <Button
<Button variant="plain"
variant="plain" type="button"
type="button" aria-label={i18n._(t`Clear all search filters`)}
aria-label={i18n._(t`Clear all search filters`)} onClick={onRemoveAll}
onClick={onRemoveAll} >
> <span className="pf-c-chip__text">{i18n._(t`Clear all`)}</span>
<span className="pf-c-chip__text">{i18n._(t`Clear all`)}</span> </Button>
</Button> </div>
</div> </ChipGroup>
</ChipGroup> </FilterTagsRow>
</FilterTagsRow> )
); );
}; };

View File

@@ -26,14 +26,19 @@ describe('<ExpandCollapse />', () => {
test('renders non-default param tags based on location history', () => { test('renders non-default param tags based on location history', () => {
const history = createMemoryHistory({ 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( const wrapper = mountWithContexts(
<FilterTags <FilterTags
qsConfig={qsConfig} qsConfig={qsConfig}
onRemove={onRemoveFn} onRemove={onRemoveFn}
onRemoveAll={onRemoveAllFn} onRemoveAll={onRemoveAllFn}
/>, { context: { router: { history, route: { location: history.location } } } } />,
{
context: { router: { history, route: { location: history.location } } },
}
); );
const chips = wrapper.find('.pf-c-chip.searchTagChip'); const chips = wrapper.find('.pf-c-chip.searchTagChip');
expect(chips.length).toBe(2); expect(chips.length).toBe(2);

View File

@@ -10,7 +10,7 @@ import {
encodeNonDefaultQueryString, encodeNonDefaultQueryString,
parseQueryString, parseQueryString,
addParams, addParams,
removeParams removeParams,
} from '@util/qs'; } from '@util/qs';
import { QSConfig } from '@types'; import { QSConfig } from '@types';
@@ -21,12 +21,12 @@ const EmptyStateControlsWrapper = styled.div`
margin-bottom: 20px; margin-bottom: 20px;
justify-content: flex-end; justify-content: flex-end;
& > :not(:first-child) { & > :not(:first-child) {
margin-left: 20px; margin-left: 20px;
} }
`; `;
class ListHeader extends React.Component { class ListHeader extends React.Component {
constructor (props) { constructor(props) {
super(props); super(props);
this.handleSearch = this.handleSearch.bind(this); this.handleSearch = this.handleSearch.bind(this);
@@ -35,7 +35,7 @@ class ListHeader extends React.Component {
this.handleRemoveAll = this.handleRemoveAll.bind(this); this.handleRemoveAll = this.handleRemoveAll.bind(this);
} }
getSortOrder () { getSortOrder() {
const { qsConfig, location } = this.props; const { qsConfig, location } = this.props;
const queryParams = parseQueryString(qsConfig, location.search); const queryParams = parseQueryString(qsConfig, location.search);
if (queryParams.order_by && queryParams.order_by.startsWith('-')) { if (queryParams.order_by && queryParams.order_by.startsWith('-')) {
@@ -44,45 +44,47 @@ class ListHeader extends React.Component {
return [queryParams.order_by, 'ascending']; return [queryParams.order_by, 'ascending'];
} }
handleSearch (key, value) { handleSearch(key, value) {
const { history, qsConfig } = this.props; const { history, qsConfig } = this.props;
const { search } = history.location; const { search } = history.location;
this.pushHistoryState(addParams(qsConfig, search, { [key]: value })); this.pushHistoryState(addParams(qsConfig, search, { [key]: value }));
} }
handleRemove (key, value) { handleRemove(key, value) {
const { history, qsConfig } = this.props; const { history, qsConfig } = this.props;
const { search } = history.location; const { search } = history.location;
this.pushHistoryState(removeParams(qsConfig, search, { [key]: value })); this.pushHistoryState(removeParams(qsConfig, search, { [key]: value }));
} }
handleRemoveAll () { handleRemoveAll() {
this.pushHistoryState(null); this.pushHistoryState(null);
} }
handleSort (key, order) { handleSort(key, order) {
const { history, qsConfig } = this.props; const { history, qsConfig } = this.props;
const { search } = history.location; const { search } = history.location;
this.pushHistoryState(addParams(qsConfig, search, { this.pushHistoryState(
order_by: order === 'ascending' ? key : `-${key}`, addParams(qsConfig, search, {
page: null, order_by: order === 'ascending' ? key : `-${key}`,
})); page: null,
})
);
} }
pushHistoryState (params) { pushHistoryState(params) {
const { history, qsConfig } = this.props; const { history, qsConfig } = this.props;
const { pathname } = history.location; const { pathname } = history.location;
const encodedParams = encodeNonDefaultQueryString(qsConfig, params); const encodedParams = encodeNonDefaultQueryString(qsConfig, params);
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname); history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
} }
render () { render() {
const { const {
emptyStateControls, emptyStateControls,
itemCount, itemCount,
columns, columns,
renderToolbar, renderToolbar,
qsConfig qsConfig,
} = this.props; } = this.props;
const [orderBy, sortOrder] = this.getSortOrder(); const [orderBy, sortOrder] = this.getSortOrder();
return ( return (
@@ -124,17 +126,19 @@ class ListHeader extends React.Component {
ListHeader.propTypes = { ListHeader.propTypes = {
itemCount: PropTypes.number.isRequired, itemCount: PropTypes.number.isRequired,
qsConfig: QSConfig.isRequired, qsConfig: QSConfig.isRequired,
columns: arrayOf(shape({ columns: arrayOf(
name: string.isRequired, shape({
key: string.isRequired, name: string.isRequired,
isSortable: bool, key: string.isRequired,
isSearchable: bool isSortable: bool,
})).isRequired, isSearchable: bool,
})
).isRequired,
renderToolbar: PropTypes.func, renderToolbar: PropTypes.func,
}; };
ListHeader.defaultProps = { ListHeader.defaultProps = {
renderToolbar: (props) => (<DataListToolbar {...props} />), renderToolbar: props => <DataListToolbar {...props} />,
}; };
export default withRouter(ListHeader); export default withRouter(ListHeader);

View File

@@ -17,7 +17,9 @@ describe('ListHeader', () => {
<ListHeader <ListHeader
itemCount={50} itemCount={50}
qsConfig={qsConfig} qsConfig={qsConfig}
columns={[{ name: 'foo', key: 'foo', isSearchable: true, isSortable: true }]} columns={[
{ name: 'foo', key: 'foo', isSearchable: true, isSortable: true },
]}
renderToolbar={renderToolbarFn} renderToolbar={renderToolbarFn}
/> />
); );
@@ -33,8 +35,11 @@ describe('ListHeader', () => {
<ListHeader <ListHeader
itemCount={7} itemCount={7}
qsConfig={qsConfig} qsConfig={qsConfig}
columns={[{ name: 'name', key: 'name', isSearchable: true, isSortable: true }]} columns={[
/>, { context: { router: { history } } } { name: 'name', key: 'name', isSearchable: true, isSortable: true },
]}
/>,
{ context: { router: { history } } }
); );
const toolbar = wrapper.find('DataListToolbar'); const toolbar = wrapper.find('DataListToolbar');

View File

@@ -15,7 +15,7 @@ import DataListToolbar from '@components/DataListToolbar';
import { import {
encodeNonDefaultQueryString, encodeNonDefaultQueryString,
parseQueryString, parseQueryString,
addParams addParams,
} from '@util/qs'; } from '@util/qs';
import { pluralize, ucFirst } from '@util/strings'; import { pluralize, ucFirst } from '@util/strings';
@@ -24,32 +24,32 @@ import { QSConfig } from '@types';
import PaginatedDataListItem from './PaginatedDataListItem'; import PaginatedDataListItem from './PaginatedDataListItem';
class PaginatedDataList extends React.Component { class PaginatedDataList extends React.Component {
constructor (props) { constructor(props) {
super(props); super(props);
this.handleSetPage = this.handleSetPage.bind(this); this.handleSetPage = this.handleSetPage.bind(this);
this.handleSetPageSize = this.handleSetPageSize.bind(this); this.handleSetPageSize = this.handleSetPageSize.bind(this);
} }
handleSetPage (event, pageNumber) { handleSetPage(event, pageNumber) {
const { history, qsConfig } = this.props; const { history, qsConfig } = this.props;
const { search } = history.location; const { search } = history.location;
this.pushHistoryState(addParams(qsConfig, search, { page: pageNumber })); this.pushHistoryState(addParams(qsConfig, search, { page: pageNumber }));
} }
handleSetPageSize (event, pageSize) { handleSetPageSize(event, pageSize) {
const { history, qsConfig } = this.props; const { history, qsConfig } = this.props;
const { search } = history.location; const { search } = history.location;
this.pushHistoryState(addParams(qsConfig, search, { page_size: pageSize })); this.pushHistoryState(addParams(qsConfig, search, { page_size: pageSize }));
} }
pushHistoryState (params) { pushHistoryState(params) {
const { history, qsConfig } = this.props; const { history, qsConfig } = this.props;
const { pathname } = history.location; const { pathname } = history.location;
const encodedParams = encodeNonDefaultQueryString(qsConfig, params); const encodedParams = encodeNonDefaultQueryString(qsConfig, params);
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname); history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
} }
render () { render() {
const { const {
contentError, contentError,
hasContentLoading, hasContentLoading,
@@ -66,25 +66,42 @@ class PaginatedDataList extends React.Component {
i18n, i18n,
renderToolbar, renderToolbar,
} = this.props; } = 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 queryParams = parseQueryString(qsConfig, location.search);
const itemDisplayName = ucFirst(pluralize(itemName)); const itemDisplayName = ucFirst(pluralize(itemName));
const itemDisplayNamePlural = ucFirst(itemNamePlural || pluralize(itemName)); const itemDisplayNamePlural = ucFirst(
itemNamePlural || pluralize(itemName)
);
const dataListLabel = i18n._(t`${itemDisplayName} List`); 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 `); const emptyContentTitle = i18n._(t`No ${itemDisplayNamePlural} Found `);
let Content; let Content;
if (hasContentLoading && items.length <= 0) { if (hasContentLoading && items.length <= 0) {
Content = (<ContentLoading />); Content = <ContentLoading />;
} else if (contentError) { } else if (contentError) {
Content = (<ContentError error={contentError} />); Content = <ContentError error={contentError} />;
} else if (items.length <= 0) { } else if (items.length <= 0) {
Content = (<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />); Content = (
<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />
);
} else { } else {
Content = (<DataList aria-label={dataListLabel}>{items.map(renderItem)}</DataList>); Content = (
<DataList aria-label={dataListLabel}>{items.map(renderItem)}</DataList>
);
} }
if (items.length <= 0) { if (items.length <= 0) {
@@ -115,12 +132,16 @@ class PaginatedDataList extends React.Component {
itemCount={itemCount} itemCount={itemCount}
page={queryParams.page || 1} page={queryParams.page || 1}
perPage={queryParams.page_size} perPage={queryParams.page_size}
perPageOptions={showPageSizeOptions ? [ perPageOptions={
{ title: '5', value: 5 }, showPageSizeOptions
{ title: '10', value: 10 }, ? [
{ title: '20', value: 20 }, { title: '5', value: 5 },
{ title: '50', value: 50 } { title: '10', value: 10 },
] : []} { title: '20', value: 20 },
{ title: '50', value: 50 },
]
: []
}
onSetPage={this.handleSetPage} onSetPage={this.handleSetPage}
onPerPageSelect={this.handleSetPageSize} onPerPageSelect={this.handleSetPageSize}
/> />
@@ -142,11 +163,13 @@ PaginatedDataList.propTypes = {
itemNamePlural: PropTypes.string, itemNamePlural: PropTypes.string,
qsConfig: QSConfig.isRequired, qsConfig: QSConfig.isRequired,
renderItem: PropTypes.func, renderItem: PropTypes.func,
toolbarColumns: arrayOf(shape({ toolbarColumns: arrayOf(
name: string.isRequired, shape({
key: string.isRequired, name: string.isRequired,
isSortable: bool, key: string.isRequired,
})), isSortable: bool,
})
),
showPageSizeOptions: PropTypes.bool, showPageSizeOptions: PropTypes.bool,
renderToolbar: PropTypes.func, renderToolbar: PropTypes.func,
hasContentLoading: PropTypes.bool, hasContentLoading: PropTypes.bool,
@@ -160,8 +183,8 @@ PaginatedDataList.defaultProps = {
itemName: 'item', itemName: 'item',
itemNamePlural: '', itemNamePlural: '',
showPageSizeOptions: true, showPageSizeOptions: true,
renderItem: (item) => (<PaginatedDataListItem key={item.id} item={item} />), renderItem: item => <PaginatedDataListItem key={item.id} item={item} />,
renderToolbar: (props) => (<DataListToolbar {...props} />), renderToolbar: props => <DataListToolbar {...props} />,
}; };
export { PaginatedDataList as _PaginatedDataList }; export { PaginatedDataList as _PaginatedDataList };

View File

@@ -51,7 +51,8 @@ describe('<PaginatedDataList />', () => {
order_by: 'name', order_by: 'name',
}} }}
qsConfig={qsConfig} qsConfig={qsConfig}
/>, { context: { router: { history } } } />,
{ context: { router: { history } } }
); );
const pagination = wrapper.find('Pagination'); const pagination = wrapper.find('Pagination');
@@ -77,7 +78,8 @@ describe('<PaginatedDataList />', () => {
order_by: 'name', order_by: 'name',
}} }}
qsConfig={qsConfig} qsConfig={qsConfig}
/>, { context: { router: { history } } } />,
{ context: { router: { history } } }
); );
const pagination = wrapper.find('Pagination'); const pagination = wrapper.find('Pagination');

View File

@@ -10,11 +10,9 @@ import {
DropdownItem, DropdownItem,
Form, Form,
FormGroup, FormGroup,
TextInput as PFTextInput TextInput as PFTextInput,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { import { SearchIcon } from '@patternfly/react-icons';
SearchIcon
} from '@patternfly/react-icons';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -29,7 +27,8 @@ const Button = styled(PFButton)`
`; `;
const Dropdown = styled(PFDropdown)` const Dropdown = styled(PFDropdown)`
&&& { /* Higher specificity required because we are selecting unclassed elements */ &&& {
/* Higher specificity required because we are selecting unclassed elements */
> button { > button {
min-height: 30px; min-height: 30px;
min-width: 70px; min-width: 70px;
@@ -37,17 +36,19 @@ const Dropdown = styled(PFDropdown)`
padding: 0 10px; padding: 0 10px;
margin: 0px; margin: 0px;
> span { /* text element */ > span {
/* text element */
width: auto; width: auto;
} }
> svg { /* caret icon */ > svg {
/* caret icon */
margin: 0px; margin: 0px;
padding-top: 3px; padding-top: 3px;
padding-left: 3px; padding-left: 3px;
} }
} }
} }
`; `;
const NoOptionDropdown = styled.div` const NoOptionDropdown = styled.div`
@@ -61,7 +62,7 @@ const InputFormGroup = styled(FormGroup)`
`; `;
class Search extends React.Component { class Search extends React.Component {
constructor (props) { constructor(props) {
super(props); super(props);
const { sortedColumnKey } = this.props; const { sortedColumnKey } = this.props;
@@ -77,11 +78,11 @@ class Search extends React.Component {
this.handleSearch = this.handleSearch.bind(this); this.handleSearch = this.handleSearch.bind(this);
} }
handleDropdownToggle (isSearchDropdownOpen) { handleDropdownToggle(isSearchDropdownOpen) {
this.setState({ isSearchDropdownOpen }); this.setState({ isSearchDropdownOpen });
} }
handleDropdownSelect ({ target }) { handleDropdownSelect({ target }) {
const { columns } = this.props; const { columns } = this.props;
const { innerText } = target; const { innerText } = target;
@@ -89,7 +90,7 @@ class Search extends React.Component {
this.setState({ isSearchDropdownOpen: false, searchKey }); this.setState({ isSearchDropdownOpen: false, searchKey });
} }
handleSearch (e) { handleSearch(e) {
// keeps page from fully reloading // keeps page from fully reloading
e.preventDefault(); e.preventDefault();
@@ -102,22 +103,17 @@ class Search extends React.Component {
this.setState({ searchValue: '' }); this.setState({ searchValue: '' });
} }
handleSearchInputChange (searchValue) { handleSearchInputChange(searchValue) {
this.setState({ searchValue }); this.setState({ searchValue });
} }
render () { render() {
const { up } = DropdownPosition; const { up } = DropdownPosition;
const { const { columns, i18n } = this.props;
columns, const { isSearchDropdownOpen, searchKey, searchValue } = this.state;
i18n const { name: searchColumnName } = columns.find(
} = this.props; ({ key }) => key === searchKey
const { );
isSearchDropdownOpen,
searchKey,
searchValue,
} = this.state;
const { name: searchColumnName } = columns.find(({ key }) => key === searchKey);
const searchDropdownItems = columns const searchDropdownItems = columns
.filter(({ key, isSearchable }) => isSearchable && key !== searchKey) .filter(({ key, isSearchable }) => isSearchable && key !== searchKey)
@@ -133,32 +129,38 @@ class Search extends React.Component {
{searchDropdownItems.length > 0 ? ( {searchDropdownItems.length > 0 ? (
<FormGroup <FormGroup
fieldId="searchKeyDropdown" 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 <Dropdown
onToggle={this.handleDropdownToggle} onToggle={this.handleDropdownToggle}
onSelect={this.handleDropdownSelect} onSelect={this.handleDropdownSelect}
direction={up} direction={up}
isOpen={isSearchDropdownOpen} isOpen={isSearchDropdownOpen}
toggle={( toggle={
<DropdownToggle <DropdownToggle
id="awx-search" id="awx-search"
onToggle={this.handleDropdownToggle} onToggle={this.handleDropdownToggle}
> >
{searchColumnName} {searchColumnName}
</DropdownToggle> </DropdownToggle>
)} }
dropdownItems={searchDropdownItems} dropdownItems={searchDropdownItems}
/> />
</FormGroup> </FormGroup>
) : ( ) : (
<NoOptionDropdown> <NoOptionDropdown>{searchColumnName}</NoOptionDropdown>
{searchColumnName}
</NoOptionDropdown>
)} )}
<InputFormGroup <InputFormGroup
fieldId="searchValueTextInput" 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 <TextInput
type="search" type="search"
@@ -190,7 +192,7 @@ Search.propTypes = {
Search.defaultProps = { Search.defaultProps = {
onSearch: null, onSearch: null,
sortedColumnKey: 'name' sortedColumnKey: 'name',
}; };
export default withI18n()(Search); export default withI18n()(Search);

View File

@@ -12,7 +12,9 @@ describe('<Search />', () => {
}); });
test('it triggers the expected callbacks', () => { 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 searchBtn = 'button[aria-label="Search submit button"]';
const searchTextInput = 'input[aria-label="Search text input"]'; const searchTextInput = 'input[aria-label="Search text input"]';
@@ -20,11 +22,7 @@ describe('<Search />', () => {
const onSearch = jest.fn(); const onSearch = jest.fn();
search = mountWithContexts( search = mountWithContexts(
<Search <Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
); );
search.find(searchTextInput).instance().value = 'test-321'; search.find(searchTextInput).instance().value = 'test-321';
@@ -36,14 +34,12 @@ describe('<Search />', () => {
}); });
test('handleDropdownToggle properly updates state', async () => { 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 onSearch = jest.fn();
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<Search <Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
).find('Search'); ).find('Search');
expect(wrapper.state('isSearchDropdownOpen')).toEqual(false); expect(wrapper.state('isSearchDropdownOpen')).toEqual(false);
wrapper.instance().handleDropdownToggle(true); wrapper.instance().handleDropdownToggle(true);
@@ -53,18 +49,21 @@ describe('<Search />', () => {
test('handleDropdownSelect properly updates state', async () => { test('handleDropdownSelect properly updates state', async () => {
const columns = [ const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }, { 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 onSearch = jest.fn();
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<Search <Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
).find('Search'); ).find('Search');
expect(wrapper.state('searchKey')).toEqual('name'); expect(wrapper.state('searchKey')).toEqual('name');
wrapper.instance().handleDropdownSelect({ target: { innerText: 'Description' } }); wrapper
.instance()
.handleDropdownSelect({ target: { innerText: 'Description' } });
expect(wrapper.state('searchKey')).toEqual('description'); expect(wrapper.state('searchKey')).toEqual('description');
}); });
}); });

View File

@@ -12,7 +12,9 @@ describe('<Sort />', () => {
}); });
test('it triggers the expected callbacks', () => { 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"]'; const sortBtn = 'button[aria-label="Sort"]';
@@ -38,7 +40,7 @@ describe('<Sort />', () => {
{ name: 'Foo', key: 'foo', isSortable: true }, { name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true }, { name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true }, { name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' } { name: 'Baz', key: 'baz' },
]; ];
const onSort = jest.fn(); const onSort = jest.fn();
@@ -62,7 +64,7 @@ describe('<Sort />', () => {
{ name: 'Foo', key: 'foo', isSortable: true }, { name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true }, { name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true }, { name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' } { name: 'Baz', key: 'baz' },
]; ];
const onSort = jest.fn(); const onSort = jest.fn();
@@ -86,7 +88,7 @@ describe('<Sort />', () => {
{ name: 'Foo', key: 'foo', isSortable: true }, { name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true }, { name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true }, { name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' } { name: 'Baz', key: 'baz' },
]; ];
const onSort = jest.fn(); const onSort = jest.fn();
@@ -109,7 +111,7 @@ describe('<Sort />', () => {
{ name: 'Foo', key: 'foo', isSortable: true }, { name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true }, { name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true }, { name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' } { name: 'Baz', key: 'baz' },
]; ];
const onSort = jest.fn(); const onSort = jest.fn();
@@ -133,8 +135,12 @@ describe('<Sort />', () => {
const downAlphaIconSelector = 'SortAlphaDownIcon'; const downAlphaIconSelector = 'SortAlphaDownIcon';
const upAlphaIconSelector = 'SortAlphaUpIcon'; const upAlphaIconSelector = 'SortAlphaUpIcon';
const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }]; const numericColumns = [
const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }]; { name: 'ID', key: 'id', isSortable: true, isNumeric: true },
];
const alphaColumns = [
{ name: 'Name', key: 'name', isSortable: true, isNumeric: false },
];
const onSort = jest.fn(); const onSort = jest.fn();
sort = mountWithContexts( sort = mountWithContexts(

View File

@@ -2,17 +2,13 @@ import React, { Component } from 'react';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core';
Card,
PageSection,
PageSectionVariants,
} from '@patternfly/react-core';
import { UnifiedJobsAPI } from '@api'; import { UnifiedJobsAPI } from '@api';
import AlertModal from '@components/AlertModal'; import AlertModal from '@components/AlertModal';
import DatalistToolbar from '@components/DataListToolbar'; import DatalistToolbar from '@components/DataListToolbar';
import PaginatedDataList, { import PaginatedDataList, {
ToolbarDeleteButton ToolbarDeleteButton,
} from '@components/PaginatedDataList'; } from '@components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '@util/qs'; import { getQSConfig, parseQueryString } from '@util/qs';
@@ -26,12 +22,12 @@ const QS_CONFIG = getQSConfig('job', {
}); });
class JobList extends Component { class JobList extends Component {
constructor (props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
hasContentLoading: true, hasContentLoading: true,
hasContentError: false, contentError: null,
deletionError: false, deletionError: false,
selected: [], selected: [],
jobs: [], jobs: [],
@@ -44,28 +40,28 @@ class JobList extends Component {
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this); this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
} }
componentDidMount () { componentDidMount() {
this.loadJobs(); this.loadJobs();
} }
componentDidUpdate (prevProps) { componentDidUpdate(prevProps) {
const { location } = this.props; const { location } = this.props;
if (location !== prevProps.location) { if (location !== prevProps.location) {
this.loadJobs(); this.loadJobs();
} }
} }
handleDeleteErrorClose () { handleDeleteErrorClose() {
this.setState({ deletionError: false }); this.setState({ deletionError: false });
} }
handleSelectAll (isSelected) { handleSelectAll(isSelected) {
const { jobs } = this.state; const { jobs } = this.state;
const selected = isSelected ? [...jobs] : []; const selected = isSelected ? [...jobs] : [];
this.setState({ selected }); this.setState({ selected });
} }
handleSelect (item) { handleSelect(item) {
const { selected } = this.state; const { selected } = this.state;
if (selected.some(s => s.id === item.id)) { if (selected.some(s => s.id === item.id)) {
this.setState({ selected: selected.filter(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; const { selected } = this.state;
this.setState({ hasContentLoading: true, deletionError: false }); this.setState({ hasContentLoading: true, deletionError: false });
try { try {
@@ -86,38 +82,37 @@ class JobList extends Component {
} }
} }
async loadJobs () { async loadJobs() {
const { location } = this.props; const { location } = this.props;
const params = parseQueryString(QS_CONFIG, location.search); const params = parseQueryString(QS_CONFIG, location.search);
this.setState({ hasContentError: false, hasContentLoading: true }); this.setState({ contentError: null, hasContentLoading: true });
try { try {
const { data: { count, results } } = await UnifiedJobsAPI.read(params); const {
data: { count, results },
} = await UnifiedJobsAPI.read(params);
this.setState({ this.setState({
itemCount: count, itemCount: count,
jobs: results, jobs: results,
selected: [], selected: [],
}); });
} catch (err) { } catch (err) {
this.setState({ hasContentError: true }); this.setState({ contentError: err });
} finally { } finally {
this.setState({ hasContentLoading: false }); this.setState({ hasContentLoading: false });
} }
} }
render () { render() {
const { const {
hasContentError, contentError,
hasContentLoading, hasContentLoading,
deletionError, deletionError,
jobs, jobs,
itemCount, itemCount,
selected, selected,
} = this.state; } = this.state;
const { const { match, i18n } = this.props;
match,
i18n
} = this.props;
const { medium } = PageSectionVariants; const { medium } = PageSectionVariants;
const isAllSelected = selected.length === jobs.length; const isAllSelected = selected.length === jobs.length;
const itemName = i18n._(t`Job`); const itemName = i18n._(t`Job`);
@@ -125,17 +120,27 @@ class JobList extends Component {
<PageSection variant={medium}> <PageSection variant={medium}>
<Card> <Card>
<PaginatedDataList <PaginatedDataList
hasContentError={hasContentError} contentError={contentError}
hasContentLoading={hasContentLoading} hasContentLoading={hasContentLoading}
items={jobs} items={jobs}
itemCount={itemCount} itemCount={itemCount}
itemName={itemName} itemName={itemName}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
toolbarColumns={[ 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 <DatalistToolbar
{...props} {...props}
showSelectAll showSelectAll
@@ -148,11 +153,11 @@ class JobList extends Component {
onDelete={this.handleDelete} onDelete={this.handleDelete}
itemsToDelete={selected} itemsToDelete={selected}
itemName={itemName} itemName={itemName}
/> />,
]} ]}
/> />
)} )}
renderItem={(job) => ( renderItem={job => (
<JobListItem <JobListItem
key={job.id} key={job.id}
value={job.name} value={job.name}

View File

@@ -7,12 +7,10 @@ import { OrganizationsAPI, TeamsAPI, UsersAPI } from '@api';
import AddResourceRole from '@components/AddRole/AddResourceRole'; import AddResourceRole from '@components/AddRole/AddResourceRole';
import AlertModal from '@components/AlertModal'; import AlertModal from '@components/AlertModal';
import DataListToolbar from '@components/DataListToolbar'; import DataListToolbar from '@components/DataListToolbar';
import PaginatedDataList, { ToolbarAddButton } from '@components/PaginatedDataList'; import PaginatedDataList, {
import { ToolbarAddButton,
getQSConfig, } from '@components/PaginatedDataList';
encodeQueryString, import { getQSConfig, encodeQueryString, parseQueryString } from '@util/qs';
parseQueryString
} from '@util/qs';
import { Organization } from '@types'; import { Organization } from '@types';
import DeleteRoleConfirmationModal from './DeleteRoleConfirmationModal'; import DeleteRoleConfirmationModal from './DeleteRoleConfirmationModal';
@@ -29,11 +27,11 @@ class OrganizationAccess extends React.Component {
organization: Organization.isRequired, organization: Organization.isRequired,
}; };
constructor (props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
accessRecords: [], accessRecords: [],
hasContentError: false, contentError: null,
hasContentLoading: true, hasContentLoading: true,
hasDeletionError: false, hasDeletionError: false,
deletionRecord: null, deletionRecord: null,
@@ -51,11 +49,11 @@ class OrganizationAccess extends React.Component {
this.handleDeleteOpen = this.handleDeleteOpen.bind(this); this.handleDeleteOpen = this.handleDeleteOpen.bind(this);
} }
componentDidMount () { componentDidMount() {
this.loadAccessList(); this.loadAccessList();
} }
componentDidUpdate (prevProps) { componentDidUpdate(prevProps) {
const { location } = this.props; const { location } = this.props;
const prevParams = parseQueryString(QS_CONFIG, prevProps.location.search); 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 { organization, location } = this.props;
const params = parseQueryString(QS_CONFIG, location.search); const params = parseQueryString(QS_CONFIG, location.search);
this.setState({ hasContentError: false, hasContentLoading: true }); this.setState({ contentError: null, hasContentLoading: true });
try { try {
const { const {
data: { data: { results: accessRecords = [], count: itemCount = 0 },
results: accessRecords = [],
count: itemCount = 0
}
} = await OrganizationsAPI.readAccessList(organization.id, params); } = await OrganizationsAPI.readAccessList(organization.id, params);
this.setState({ itemCount, accessRecords }); this.setState({ itemCount, accessRecords });
} catch (error) { } catch (err) {
this.setState({ hasContentError: true }); this.setState({ cotentError: err });
} finally { } finally {
this.setState({ hasContentLoading: false }); this.setState({ hasContentLoading: false });
} }
} }
handleDeleteOpen (deletionRole, deletionRecord) { handleDeleteOpen(deletionRole, deletionRecord) {
this.setState({ deletionRole, deletionRecord }); this.setState({ deletionRole, deletionRecord });
} }
handleDeleteCancel () { handleDeleteCancel() {
this.setState({ deletionRole: null, deletionRecord: null }); this.setState({ deletionRole: null, deletionRecord: null });
} }
handleDeleteErrorClose () { handleDeleteErrorClose() {
this.setState({ this.setState({
hasDeletionError: false, hasDeletionError: false,
deletionRecord: null, deletionRecord: null,
deletionRole: null deletionRole: null,
}); });
} }
async handleDeleteConfirm () { async handleDeleteConfirm() {
const { deletionRole, deletionRecord } = this.state; const { deletionRole, deletionRecord } = this.state;
if (!deletionRole || !deletionRecord) { if (!deletionRole || !deletionRecord) {
@@ -111,7 +106,10 @@ class OrganizationAccess extends React.Component {
let promise; let promise;
if (typeof deletionRole.team_id !== 'undefined') { if (typeof deletionRole.team_id !== 'undefined') {
promise = TeamsAPI.disassociateRole(deletionRole.team_id, deletionRole.id); promise = TeamsAPI.disassociateRole(
deletionRole.team_id,
deletionRole.id
);
} else { } else {
promise = UsersAPI.disassociateRole(deletionRecord.id, deletionRole.id); promise = UsersAPI.disassociateRole(deletionRecord.id, deletionRole.id);
} }
@@ -121,34 +119,34 @@ class OrganizationAccess extends React.Component {
await promise.then(this.loadAccessList); await promise.then(this.loadAccessList);
this.setState({ this.setState({
deletionRole: null, deletionRole: null,
deletionRecord: null deletionRecord: null,
}); });
} catch (error) { } catch (error) {
this.setState({ this.setState({
hasContentLoading: false, hasContentLoading: false,
hasDeletionError: true hasDeletionError: true,
}); });
} }
} }
handleAddClose () { handleAddClose() {
this.setState({ isAddModalOpen: false }); this.setState({ isAddModalOpen: false });
} }
handleAddOpen () { handleAddOpen() {
this.setState({ isAddModalOpen: true }); this.setState({ isAddModalOpen: true });
} }
handleAddSuccess () { handleAddSuccess() {
this.setState({ isAddModalOpen: false }); this.setState({ isAddModalOpen: false });
this.loadAccessList(); this.loadAccessList();
} }
render () { render() {
const { organization, i18n } = this.props; const { organization, i18n } = this.props;
const { const {
accessRecords, accessRecords,
hasContentError, contentError,
hasContentLoading, hasContentLoading,
deletionRole, deletionRole,
deletionRecord, deletionRecord,
@@ -157,28 +155,51 @@ class OrganizationAccess extends React.Component {
isAddModalOpen, isAddModalOpen,
} = this.state; } = this.state;
const canEdit = organization.summary_fields.user_capabilities.edit; const canEdit = organization.summary_fields.user_capabilities.edit;
const isDeleteModalOpen = !hasContentLoading && !hasDeletionError && deletionRole; const isDeleteModalOpen =
!hasContentLoading && !hasDeletionError && deletionRole;
return ( return (
<Fragment> <Fragment>
<PaginatedDataList <PaginatedDataList
hasContentError={hasContentError} error={contentError}
hasContentLoading={hasContentLoading} hasContentLoading={hasContentLoading}
items={accessRecords} items={accessRecords}
itemCount={itemCount} itemCount={itemCount}
itemName="role" itemName="role"
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
toolbarColumns={[ 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`First Name`),
{ name: i18n._(t`Last Name`), key: 'last_name', isSortable: true, isSearchable: true }, 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 <DataListToolbar
{...props} {...props}
additionalControls={canEdit ? [ additionalControls={
<ToolbarAddButton key="add" onClick={this.handleAddOpen} /> canEdit
] : null} ? [
<ToolbarAddButton
key="add"
onClick={this.handleAddOpen}
/>,
]
: null
}
/> />
)} )}
renderItem={accessRecord => ( renderItem={accessRecord => (

View File

@@ -103,7 +103,7 @@ describe('<OrganizationAccess />', () => {
expect(wrapper.find('OrganizationAccess').state('hasContentLoading')).toBe( expect(wrapper.find('OrganizationAccess').state('hasContentLoading')).toBe(
false false
); );
expect(wrapper.find('OrganizationAccess').state('hasContentError')).toBe(false); expect(wrapper.find('OrganizationAccess').state('contentError')).toBe(null);
done(); done();
}); });

View File

@@ -34,7 +34,7 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
} }
> >
<WithI18n <WithI18n
hasContentError={false} error={null}
hasContentLoading={true} hasContentLoading={true}
itemCount={0} itemCount={0}
itemName="role" itemName="role"
@@ -83,7 +83,7 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
withHash={true} withHash={true}
> >
<withRouter(PaginatedDataList) <withRouter(PaginatedDataList)
hasContentError={false} error={null}
hasContentLoading={true} hasContentLoading={true}
i18n={"/i18n/"} i18n={"/i18n/"}
itemCount={0} itemCount={0}
@@ -130,7 +130,8 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
> >
<Route> <Route>
<PaginatedDataList <PaginatedDataList
hasContentError={false} contentError={null}
error={null}
hasContentLoading={true} hasContentLoading={true}
history={"/history/"} history={"/history/"}
i18n={"/i18n/"} i18n={"/i18n/"}

View File

@@ -2,11 +2,7 @@ import React, { Component, Fragment } from 'react';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core';
Card,
PageSection,
PageSectionVariants,
} from '@patternfly/react-core';
import { OrganizationsAPI } from '@api'; import { OrganizationsAPI } from '@api';
import AlertModal from '@components/AlertModal'; import AlertModal from '@components/AlertModal';
@@ -26,12 +22,12 @@ const QS_CONFIG = getQSConfig('organization', {
}); });
class OrganizationsList extends Component { class OrganizationsList extends Component {
constructor (props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
hasContentLoading: true, hasContentLoading: true,
hasContentError: false, contentError: null,
hasDeletionError: false, hasDeletionError: false,
organizations: [], organizations: [],
selected: [], selected: [],
@@ -46,25 +42,25 @@ class OrganizationsList extends Component {
this.loadOrganizations = this.loadOrganizations.bind(this); this.loadOrganizations = this.loadOrganizations.bind(this);
} }
componentDidMount () { componentDidMount() {
this.loadOrganizations(); this.loadOrganizations();
} }
componentDidUpdate (prevProps) { componentDidUpdate(prevProps) {
const { location } = this.props; const { location } = this.props;
if (location !== prevProps.location) { if (location !== prevProps.location) {
this.loadOrganizations(); this.loadOrganizations();
} }
} }
handleSelectAll (isSelected) { handleSelectAll(isSelected) {
const { organizations } = this.state; const { organizations } = this.state;
const selected = isSelected ? [...organizations] : []; const selected = isSelected ? [...organizations] : [];
this.setState({ selected }); this.setState({ selected });
} }
handleSelect (row) { handleSelect(row) {
const { selected } = this.state; const { selected } = this.state;
if (selected.some(s => s.id === row.id)) { if (selected.some(s => s.id === row.id)) {
@@ -74,16 +70,16 @@ class OrganizationsList extends Component {
} }
} }
handleDeleteErrorClose () { handleDeleteErrorClose() {
this.setState({ hasDeletionError: false }); this.setState({ hasDeletionError: false });
} }
async handleOrgDelete () { async handleOrgDelete() {
const { selected } = this.state; const { selected } = this.state;
this.setState({ hasContentLoading: true, hasDeletionError: false }); this.setState({ hasContentLoading: true, hasDeletionError: false });
try { try {
await Promise.all(selected.map((org) => OrganizationsAPI.destroy(org.id))); await Promise.all(selected.map(org => OrganizationsAPI.destroy(org.id)));
} catch (err) { } catch (err) {
this.setState({ hasDeletionError: true }); this.setState({ hasDeletionError: true });
} finally { } finally {
@@ -91,7 +87,7 @@ class OrganizationsList extends Component {
} }
} }
async loadOrganizations () { async loadOrganizations() {
const { location } = this.props; const { location } = this.props;
const { actions: cachedActions } = this.state; const { actions: cachedActions } = this.state;
const params = parseQueryString(QS_CONFIG, location.search); const params = parseQueryString(QS_CONFIG, location.search);
@@ -108,9 +104,16 @@ class OrganizationsList extends Component {
optionsPromise, optionsPromise,
]); ]);
this.setState({ hasContentError: false, hasContentLoading: true }); this.setState({ contentError: null, hasContentLoading: true });
try { try {
const [{ data: { count, results } }, { data: { actions } }] = await promises; const [
{
data: { count, results },
},
{
data: { actions },
},
] = await promises;
this.setState({ this.setState({
actions, actions,
itemCount: count, itemCount: count,
@@ -118,20 +121,18 @@ class OrganizationsList extends Component {
selected: [], selected: [],
}); });
} catch (err) { } catch (err) {
this.setState(({ hasContentError: true })); this.setState({ contentError: err });
} finally { } finally {
this.setState({ hasContentLoading: false }); this.setState({ hasContentLoading: false });
} }
} }
render () { render() {
const { const { medium } = PageSectionVariants;
medium,
} = PageSectionVariants;
const { const {
actions, actions,
itemCount, itemCount,
hasContentError, contentError,
hasContentLoading, hasContentLoading,
hasDeletionError, hasDeletionError,
selected, selected,
@@ -139,7 +140,8 @@ class OrganizationsList extends Component {
} = this.state; } = this.state;
const { match, i18n } = this.props; 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; const isAllSelected = selected.length === organizations.length;
return ( return (
@@ -147,18 +149,33 @@ class OrganizationsList extends Component {
<PageSection variant={medium}> <PageSection variant={medium}>
<Card> <Card>
<PaginatedDataList <PaginatedDataList
hasContentError={hasContentError} error={contentError}
hasContentLoading={hasContentLoading} hasContentLoading={hasContentLoading}
items={organizations} items={organizations}
itemCount={itemCount} itemCount={itemCount}
itemName="organization" itemName="organization"
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
toolbarColumns={[ toolbarColumns={[
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true }, {
{ name: i18n._(t`Modified`), key: 'modified', isSortable: true, isNumeric: true }, name: i18n._(t`Name`),
{ name: i18n._(t`Created`), key: 'created', isSortable: true, isNumeric: true }, 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 <DataListToolbar
{...props} {...props}
showSelectAll showSelectAll
@@ -171,13 +188,13 @@ class OrganizationsList extends Component {
itemsToDelete={selected} itemsToDelete={selected}
itemName="Organization" itemName="Organization"
/>, />,
canAdd canAdd ? (
? <ToolbarAddButton key="add" linkTo={`${match.url}/add`} /> <ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
: null, ) : null,
]} ]}
/> />
)} )}
renderItem={(o) => ( renderItem={o => (
<OrganizationListItem <OrganizationListItem
key={o.id} key={o.id}
organization={o} organization={o}
@@ -187,8 +204,9 @@ class OrganizationsList extends Component {
/> />
)} )}
emptyStateControls={ emptyStateControls={
canAdd ? <ToolbarAddButton key="add" linkTo={`${match.url}/add`} /> canAdd ? (
: null <ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
) : null
} }
/> />
</Card> </Card>

View File

@@ -24,7 +24,7 @@ const COLUMNS = [
]; ];
class OrganizationNotifications extends Component { class OrganizationNotifications extends Component {
constructor (props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
contentError: null, contentError: null,
@@ -38,22 +38,24 @@ class OrganizationNotifications extends Component {
typeLabels: null, typeLabels: null,
}; };
this.handleNotificationToggle = this.handleNotificationToggle.bind(this); this.handleNotificationToggle = this.handleNotificationToggle.bind(this);
this.handleNotificationErrorClose = this.handleNotificationErrorClose.bind(this); this.handleNotificationErrorClose = this.handleNotificationErrorClose.bind(
this
);
this.loadNotifications = this.loadNotifications.bind(this); this.loadNotifications = this.loadNotifications.bind(this);
} }
componentDidMount () { componentDidMount() {
this.loadNotifications(); this.loadNotifications();
} }
componentDidUpdate (prevProps) { componentDidUpdate(prevProps) {
const { location } = this.props; const { location } = this.props;
if (location !== prevProps.location) { if (location !== prevProps.location) {
this.loadNotifications(); this.loadNotifications();
} }
} }
async loadNotifications () { async loadNotifications() {
const { id, location } = this.props; const { id, location } = this.props;
const { typeLabels } = this.state; const { typeLabels } = this.state;
const params = parseQueryString(QS_CONFIG, location.search); const params = parseQueryString(QS_CONFIG, location.search);
@@ -67,13 +69,13 @@ class OrganizationNotifications extends Component {
this.setState({ contentError: null, hasContentLoading: true }); this.setState({ contentError: null, hasContentLoading: true });
try { try {
const { const {
data: { data: { count: itemCount = 0, results: notifications = [] },
count: itemCount = 0,
results: notifications = [],
}
} = await OrganizationsAPI.readNotificationTemplates(id, params); } = await OrganizationsAPI.readNotificationTemplates(id, params);
const optionsResponse = await OrganizationsAPI.readOptionsNotificationTemplates(id, params); const optionsResponse = await OrganizationsAPI.readOptionsNotificationTemplates(
id,
params
);
let idMatchParams; let idMatchParams;
if (notifications.length > 0) { 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; const { id } = this.props;
let stateArrayName; let stateArrayName;
@@ -135,13 +137,15 @@ class OrganizationNotifications extends Component {
let stateUpdateFunction; let stateUpdateFunction;
if (isCurrentlyOn) { if (isCurrentlyOn) {
// when switching off, remove the toggled notification id from the array // when switching off, remove the toggled notification id from the array
stateUpdateFunction = (prevState) => ({ stateUpdateFunction = prevState => ({
[stateArrayName]: prevState[stateArrayName].filter(i => i !== notificationId) [stateArrayName]: prevState[stateArrayName].filter(
i => i !== notificationId
),
}); });
} else { } else {
// when switching on, add the toggled notification id to the array // when switching on, add the toggled notification id to the array
stateUpdateFunction = (prevState) => ({ stateUpdateFunction = prevState => ({
[stateArrayName]: prevState[stateArrayName].concat(notificationId) [stateArrayName]: prevState[stateArrayName].concat(notificationId),
}); });
} }
@@ -161,11 +165,11 @@ class OrganizationNotifications extends Component {
} }
} }
handleNotificationErrorClose () { handleNotificationErrorClose() {
this.setState({ toggleError: false }); this.setState({ toggleError: false });
} }
render () { render() {
const { canToggleNotifications, i18n } = this.props; const { canToggleNotifications, i18n } = this.props;
const { const {
contentError, contentError,
@@ -176,7 +180,7 @@ class OrganizationNotifications extends Component {
notifications, notifications,
successTemplateIds, successTemplateIds,
errorTemplateIds, errorTemplateIds,
typeLabels typeLabels,
} = this.state; } = this.state;
return ( return (
@@ -189,7 +193,7 @@ class OrganizationNotifications extends Component {
itemName="notification" itemName="notification"
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
toolbarColumns={COLUMNS} toolbarColumns={COLUMNS}
renderItem={(notification) => ( renderItem={notification => (
<NotificationListItem <NotificationListItem
key={notification.id} key={notification.id}
notification={notification} notification={notification}

View File

@@ -164,7 +164,6 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
<Route> <Route>
<PaginatedDataList <PaginatedDataList
contentError={null} contentError={null}
hasContentError={false}
hasContentLoading={false} hasContentLoading={false}
history={"/history/"} history={"/history/"}
i18n={"/i18n/"} i18n={"/i18n/"}

View File

@@ -8,30 +8,24 @@ import { QuestionCircleIcon } from '@patternfly/react-icons';
import { InstanceGroupsAPI } from '@api'; import { InstanceGroupsAPI } from '@api';
import Lookup from '@components/Lookup'; import Lookup from '@components/Lookup';
const getInstanceGroups = async (params) => InstanceGroupsAPI.read(params); const getInstanceGroups = async params => InstanceGroupsAPI.read(params);
class InstanceGroupsLookup extends React.Component { class InstanceGroupsLookup extends React.Component {
render () { render() {
const { value, tooltip, onChange, i18n } = this.props; const { value, tooltip, onChange, i18n } = this.props;
return ( return (
<FormGroup <FormGroup
label={( label={
<Fragment> <Fragment>
{i18n._(t`Instance Groups`)} {i18n._(t`Instance Groups`)}{' '}
{' '} {tooltip && (
{ <Tooltip position="right" content={tooltip}>
tooltip && ( <QuestionCircleIcon />
<Tooltip </Tooltip>
position="right" )}
content={tooltip}
>
<QuestionCircleIcon />
</Tooltip>
)
}
</Fragment> </Fragment>
)} }
fieldId="org-instance-groups" fieldId="org-instance-groups"
> >
<Lookup <Lookup
@@ -43,9 +37,24 @@ class InstanceGroupsLookup extends React.Component {
getItems={getInstanceGroups} getItems={getInstanceGroups}
multiple multiple
columns={[ columns={[
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true }, {
{ name: i18n._(t`Modified`), key: 'modified', isSortable: false, isNumeric: true }, name: i18n._(t`Name`),
{ name: i18n._(t`Created`), key: 'created', isSortable: false, isNumeric: true } 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" sortedColumnKey="name"
/> />

View File

@@ -2,21 +2,17 @@ import React, { Component } from 'react';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core';
Card,
PageSection,
PageSectionVariants,
} from '@patternfly/react-core';
import { import {
JobTemplatesAPI, JobTemplatesAPI,
UnifiedJobTemplatesAPI, UnifiedJobTemplatesAPI,
WorkflowJobTemplatesAPI WorkflowJobTemplatesAPI,
} from '@api'; } from '@api';
import AlertModal from '@components/AlertModal'; import AlertModal from '@components/AlertModal';
import DatalistToolbar from '@components/DataListToolbar'; import DatalistToolbar from '@components/DataListToolbar';
import PaginatedDataList, { import PaginatedDataList, {
ToolbarDeleteButton ToolbarDeleteButton,
} from '@components/PaginatedDataList'; } from '@components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '@util/qs'; import { getQSConfig, parseQueryString } from '@util/qs';
@@ -28,16 +24,16 @@ const QS_CONFIG = getQSConfig('template', {
page: 1, page: 1,
page_size: 5, page_size: 5,
order_by: 'name', order_by: 'name',
type: 'job_template,workflow_job_template' type: 'job_template,workflow_job_template',
}); });
class TemplatesList extends Component { class TemplatesList extends Component {
constructor (props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
hasContentLoading: true, hasContentLoading: true,
hasContentError: false, contentError: null,
hasDeletionError: false, hasDeletionError: false,
selected: [], selected: [],
templates: [], templates: [],
@@ -50,28 +46,28 @@ class TemplatesList extends Component {
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this); this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
} }
componentDidMount () { componentDidMount() {
this.loadTemplates(); this.loadTemplates();
} }
componentDidUpdate (prevProps) { componentDidUpdate(prevProps) {
const { location } = this.props; const { location } = this.props;
if (location !== prevProps.location) { if (location !== prevProps.location) {
this.loadTemplates(); this.loadTemplates();
} }
} }
handleDeleteErrorClose () { handleDeleteErrorClose() {
this.setState({ hasDeletionError: false }); this.setState({ hasDeletionError: false });
} }
handleSelectAll (isSelected) { handleSelectAll(isSelected) {
const { templates } = this.state; const { templates } = this.state;
const selected = isSelected ? [...templates] : []; const selected = isSelected ? [...templates] : [];
this.setState({ selected }); this.setState({ selected });
} }
handleSelect (template) { handleSelect(template) {
const { selected } = this.state; const { selected } = this.state;
if (selected.some(s => s.id === template.id)) { if (selected.some(s => s.id === template.id)) {
this.setState({ selected: selected.filter(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; const { selected } = this.state;
this.setState({ hasContentLoading: true, hasDeletionError: false }); this.setState({ hasContentLoading: true, hasDeletionError: false });
try { try {
await Promise.all(selected.map(({ type, id }) => { await Promise.all(
let deletePromise; selected.map(({ type, id }) => {
if (type === 'job_template') { let deletePromise;
deletePromise = JobTemplatesAPI.destroy(id); if (type === 'job_template') {
} else if (type === 'workflow_job_template') { deletePromise = JobTemplatesAPI.destroy(id);
deletePromise = WorkflowJobTemplatesAPI.destroy(id); } else if (type === 'workflow_job_template') {
} deletePromise = WorkflowJobTemplatesAPI.destroy(id);
return deletePromise; }
})); return deletePromise;
})
);
} catch (err) { } catch (err) {
this.setState({ hasDeletionError: true }); this.setState({ hasDeletionError: true });
} finally { } finally {
@@ -101,56 +99,70 @@ class TemplatesList extends Component {
} }
} }
async loadTemplates () { async loadTemplates() {
const { location } = this.props; const { location } = this.props;
const params = parseQueryString(QS_CONFIG, location.search); const params = parseQueryString(QS_CONFIG, location.search);
this.setState({ hasContentError: false, hasContentLoading: true }); this.setState({ contentError: null, hasContentLoading: true });
try { try {
const { data: { count, results } } = await UnifiedJobTemplatesAPI.read(params); const {
data: { count, results },
} = await UnifiedJobTemplatesAPI.read(params);
this.setState({ this.setState({
itemCount: count, itemCount: count,
templates: results, templates: results,
selected: [], selected: [],
}); });
} catch (err) { } catch (err) {
this.setState({ hasContentError: true }); this.setState({ contentError: err });
} finally { } finally {
this.setState({ hasContentLoading: false }); this.setState({ hasContentLoading: false });
} }
} }
render () { render() {
const { const {
hasContentError, contentError,
hasContentLoading, hasContentLoading,
hasDeletionError, hasDeletionError,
templates, templates,
itemCount, itemCount,
selected, selected,
} = this.state; } = this.state;
const { const { match, i18n } = this.props;
match,
i18n
} = this.props;
const isAllSelected = selected.length === templates.length; const isAllSelected = selected.length === templates.length;
const { medium } = PageSectionVariants; const { medium } = PageSectionVariants;
return ( return (
<PageSection variant={medium}> <PageSection variant={medium}>
<Card> <Card>
<PaginatedDataList <PaginatedDataList
hasContentError={hasContentError} err={contentError}
hasContentLoading={hasContentLoading} hasContentLoading={hasContentLoading}
items={templates} items={templates}
itemCount={itemCount} itemCount={itemCount}
itemName={i18n._(t`Template`)} itemName={i18n._(t`Template`)}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
toolbarColumns={[ toolbarColumns={[
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true }, {
{ name: i18n._(t`Modified`), key: 'modified', isSortable: true, isNumeric: true }, name: i18n._(t`Name`),
{ name: i18n._(t`Created`), key: 'created', isSortable: true, isNumeric: true }, 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 <DatalistToolbar
{...props} {...props}
showSelectAll showSelectAll
@@ -163,11 +175,11 @@ class TemplatesList extends Component {
onDelete={this.handleTemplateDelete} onDelete={this.handleTemplateDelete}
itemsToDelete={selected} itemsToDelete={selected}
itemName={i18n._(t`Template`)} itemName={i18n._(t`Template`)}
/> />,
]} ]}
/> />
)} )}
renderItem={(template) => ( renderItem={template => (
<TemplateListItem <TemplateListItem
key={template.id} key={template.id}
value={template.name} value={template.name}

View File

@@ -4,16 +4,17 @@
* @param {array} array in the format [ [ key, value ], ...] * @param {array} array in the format [ [ key, value ], ...]
* @return {object} object in the forms { key: value, ... } * @return {object} object in the forms { key: value, ... }
*/ */
const toObject = (entriesArr) => entriesArr.reduce((acc, [key, value]) => { const toObject = entriesArr =>
if (acc[key] && Array.isArray(acc[key])) { entriesArr.reduce((acc, [key, value]) => {
acc[key].push(value); if (acc[key] && Array.isArray(acc[key])) {
} else if (acc[key]) { acc[key].push(value);
acc[key] = [acc[key], value]; } else if (acc[key]) {
} else { acc[key] = [acc[key], value];
acc[key] = value; } else {
} acc[key] = value;
return acc; }
}, {}); return acc;
}, {});
/** /**
* helper function to namespace params object * helper function to namespace params object
@@ -30,7 +31,7 @@ const namespaceParams = (namespace, params = {}) => {
}); });
return namespaced || {}; return namespaced || {};
} };
/** /**
* helper function to remove namespace from params object * helper function to remove namespace from params object
@@ -47,7 +48,7 @@ const denamespaceParams = (namespace, params = {}) => {
}); });
return denamespaced || {}; return denamespaced || {};
} };
/** /**
* helper function to check the namespace of a param is what you expec * 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('.'); if (!namespace) return !fieldname.includes('.');
return fieldname.startsWith(`${namespace}.`); return fieldname.startsWith(`${namespace}.`);
} };
/** /**
* helper function to check the value of a param is equal to another * 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)) { if (Array.isArray(one) && Array.isArray(two)) {
isEqual = one.filter(val => two.indexOf(val) > -1).length === 0; isEqual = one.filter(val => two.indexOf(val) > -1).length === 0;
} else if ((typeof(one) === "string" && typeof(two) === "string") || } else if (
(typeof(one) === "number" && typeof(two) === "number")){ (typeof one === 'string' && typeof two === 'string') ||
(typeof one === 'number' && typeof two === 'number')
) {
isEqual = one === two; isEqual = one === two;
} }
return isEqual; return isEqual;
} };
/** /**
* Convert query param object to url query string * Convert query param object to url query string
@@ -87,17 +90,19 @@ const paramValueIsEqual = (one, two) => {
* @param {object} query param object * @param {object} query param object
* @return {string} url query string * @return {string} url query string
*/ */
export const encodeQueryString = (params) => { export const encodeQueryString = params => {
if (!params) return ''; if (!params) return '';
return Object.keys(params) return Object.keys(params)
.sort() .sort()
.filter(key => params[key] !== null) .filter(key => params[key] !== null)
.map(key => ([key, params[key]])) .map(key => [key, params[key]])
.map(([key, value]) => { .map(([key, value]) => {
// if value is array, should return more than one key value pair // if value is array, should return more than one key value pair
if (Array.isArray(value)) { 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)}`; return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
}) })
@@ -115,22 +120,31 @@ export const encodeNonDefaultQueryString = (config, params) => {
if (!params) return ''; if (!params) return '';
const namespacedParams = namespaceParams(config.namespace, params); 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 namespacedDefaultKeys = Object.keys(namespacedDefaults);
const namespacedParamsWithoutDefaultsKeys = Object.keys(namespacedParams) const namespacedParamsWithoutDefaultsKeys = Object.keys(
.filter(key => namespacedDefaultKeys.indexOf(key) === -1 || namespacedParams
!paramValueIsEqual(namespacedParams[key], namespacedDefaults[key])); ).filter(
key =>
namespacedDefaultKeys.indexOf(key) === -1 ||
!paramValueIsEqual(namespacedParams[key], namespacedDefaults[key])
);
return namespacedParamsWithoutDefaultsKeys return namespacedParamsWithoutDefaultsKeys
.sort() .sort()
.filter(key => namespacedParams[key] !== null) .filter(key => namespacedParams[key] !== null)
.map(key => { .map(key => {
return ([key, namespacedParams[key]]) return [key, namespacedParams[key]];
}) })
.map(([key, value]) => { .map(([key, value]) => {
// if value is array, should return more than one key value pair // if value is array, should return more than one key value pair
if (Array.isArray(value)) { 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)}`; return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
}) })
@@ -144,7 +158,7 @@ export const encodeNonDefaultQueryString = (config, params) => {
* @param {array} params that are number fields * @param {array} params that are number fields
* @return {object} query param object * @return {object} query param object
*/ */
export function getQSConfig ( export function getQSConfig(
namespace, namespace,
defaultParams = { page: 1, page_size: 5, order_by: 'name' }, defaultParams = { page: 1, page_size: 5, order_by: 'name' },
integerFields = ['page', 'page_size'] integerFields = ['page', 'page_size']
@@ -165,12 +179,16 @@ export function getQSConfig (
* @param {string} url query string * @param {string} url query string
* @return {object} query param object * @return {object} query param object
*/ */
export function parseQueryString (config, queryString) { export function parseQueryString(config, queryString) {
if (!queryString) return config.defaultParams; 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(s => s.split('='))
.map(([key, value]) => { .map(([key, value]) => {
if (namespacedIntegerFields.includes(key)) { if (namespacedIntegerFields.includes(key)) {
@@ -185,37 +203,38 @@ export function parseQueryString (config, queryString) {
// needs to return array for duplicate keys // needs to return array for duplicate keys
// ie [[k1, v1], [k1, v2], [k2, v3]] // ie [[k1, v1], [k1, v2], [k2, v3]]
// -> [[k1, [v1, v2]], [k2, v3]] // -> [[k1, [v1, v2]], [k2, v3]]
const dedupedKeyValuePairs = Object.keys(keyValueObject) const dedupedKeyValuePairs = Object.keys(keyValueObject).map(key => {
.map(key => { const values = keyValuePairs.filter(([k]) => k === key).map(([, v]) => v);
const values = keyValuePairs
.filter(([k]) => k === key)
.map(([, v]) => v);
if (values.length === 1) { if (values.length === 1) {
return [key, values[0]]; return [key, values[0]];
} }
return [key, values]; return [key, values];
}); });
const parsed = Object.assign(...dedupedKeyValuePairs.map(([k, v]) => ({ const parsed = Object.assign(
[k]: v ...dedupedKeyValuePairs.map(([k, v]) => ({
}))); [k]: v,
}))
);
const namespacedParams = {}; const namespacedParams = {};
Object.keys(parsed) Object.keys(parsed).forEach(field => {
.forEach(field => { if (namespaceMatches(config.namespace, field)) {
if (namespaceMatches(config.namespace, field)) { let fieldname = field;
let fieldname = field; if (config.namespace) {
if (config.namespace) { fieldname = field.substr(config.namespace.length + 1);
fieldname = field.substr(config.namespace.length + 1);
}
namespacedParams[fieldname] = parsed[field];
} }
}); namespacedParams[fieldname] = parsed[field];
}
});
const namespacedDefaults = namespaceParams(config.namespace, config.defaultParams); const namespacedDefaults = namespaceParams(
config.namespace,
config.defaultParams
);
Object.keys(namespacedDefaults) Object.keys(namespacedDefaults)
.filter(key => Object.keys(parsed).indexOf(key) === -1) .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 * @param {object} namespaced params object of default params
* @return {object} namespaced params object of only defaults * @return {object} namespaced params object of only defaults
*/ */
const getDefaultParams = (params, defaults) => toObject( const getDefaultParams = (params, defaults) =>
Object.keys(params) toObject(
.filter(key => Object.keys(defaults).indexOf(key) > -1) Object.keys(params)
.map(key => [key, params[key]]) .filter(key => Object.keys(defaults).indexOf(key) > -1)
); .map(key => [key, params[key]])
);
/** /**
* helper function to get params that are not defaults * 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 * @param {object} namespaced params object of default params
* @return {object} namespaced params object of non-defaults * @return {object} namespaced params object of non-defaults
*/ */
const getNonDefaultParams = (params, defaults) => toObject( const getNonDefaultParams = (params, defaults) =>
Object.keys(params) toObject(
.filter(key => Object.keys(defaults).indexOf(key) === -1) Object.keys(params)
.map(key => [key, params[key]]) .filter(key => Object.keys(defaults).indexOf(key) === -1)
); .map(key => [key, params[key]])
);
/** /**
* helper function to merge old and new params together * 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 * @param {object} namespaced params object of new params
* @return {object} merged namespaced params object * @return {object} merged namespaced params object
*/ */
const getMergedParams = (oldParams, newParams) => toObject( const getMergedParams = (oldParams, newParams) =>
Object.keys(oldParams) toObject(
.map(key => { Object.keys(oldParams).map(key => {
let oldVal = oldParams[key]; let oldVal = oldParams[key];
const newVal = newParams[key]; const newVal = newParams[key];
if (newVal) { if (newVal) {
@@ -276,7 +297,7 @@ const getMergedParams = (oldParams, newParams) => toObject(
} }
return [key, oldVal]; return [key, oldVal];
}) })
); );
/** /**
* helper function to get new params that are not in merged params * 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 * @param {object} namespaced params object of new params
* @return {object} remaining new namespaced params object * @return {object} remaining new namespaced params object
*/ */
const getRemainingNewParams = (mergedParams, newParams) => toObject( const getRemainingNewParams = (mergedParams, newParams) =>
Object.keys(newParams) toObject(
.filter(key => Object.keys(mergedParams).indexOf(key) === -1) Object.keys(newParams)
.map(key => [key, newParams[key]]) .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 * 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 * @param {object} object with new params to add
* @return {object} query param object * @return {object} query param object
*/ */
export function addParams (config, searchString, newParams) { export function addParams(config, searchString, newParams) {
const namespacedOldParams = namespaceParams( const namespacedOldParams = namespaceParams(
config.namespace, config.namespace,
parseQueryString(config, searchString) parseQueryString(config, searchString)
); );
const namespacedNewParams = namespaceParams(config.namespace, newParams); const namespacedNewParams = namespaceParams(config.namespace, newParams);
const namespacedDefaultParams = namespaceParams(config.namespace, config.defaultParams); const namespacedDefaultParams = namespaceParams(
config.namespace,
config.defaultParams
);
const namespacedOldParamsNotDefaults = getNonDefaultParams( const namespacedOldParamsNotDefaults = getNonDefaultParams(
namespacedOldParams, namespacedOldParams,
@@ -320,7 +345,7 @@ export function addParams (config, searchString, newParams) {
return denamespaceParams(config.namespace, { return denamespaceParams(config.namespace, {
...getDefaultParams(namespacedOldParams, namespacedDefaultParams), ...getDefaultParams(namespacedOldParams, namespacedDefaultParams),
...namespacedMergedParams, ...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 * @param {object} object with new params to remove
* @return {object} query param object * @return {object} query param object
*/ */
export function removeParams (config, searchString, paramsToRemove) { export function removeParams(config, searchString, paramsToRemove) {
const oldParams = parseQueryString(config, searchString); const oldParams = parseQueryString(config, searchString);
const paramsEntries = []; const paramsEntries = [];
Object.entries(oldParams) Object.entries(oldParams).forEach(([key, value]) => {
.forEach(([key, value]) => { if (Array.isArray(value)) {
if (Array.isArray(value)) { value.forEach(val => {
value.forEach(val => { paramsEntries.push([key, val]);
paramsEntries.push([key, val]); });
}) } else {
} else { paramsEntries.push([key, value]);
paramsEntries.push([key, value]); }
} });
})
const paramsToRemoveEntries = Object.entries(paramsToRemove); const paramsToRemoveEntries = Object.entries(paramsToRemove);
const remainingEntries = paramsEntries const remainingEntries = paramsEntries.filter(
.filter(([key, value]) => paramsToRemoveEntries ([key, value]) =>
.filter(([newKey, newValue]) => key === newKey && value === newValue).length === 0); paramsToRemoveEntries.filter(
([newKey, newValue]) => key === newKey && value === newValue
).length === 0
);
const remainingObject = toObject(remainingEntries); const remainingObject = toObject(remainingEntries);
const defaultEntriesLeftover = Object.entries(config.defaultParams) const defaultEntriesLeftover = Object.entries(config.defaultParams).filter(
.filter(([key]) => !remainingObject[key]); ([key]) => !remainingObject[key]
);
const finalParamsEntries = remainingEntries; const finalParamsEntries = remainingEntries;
defaultEntriesLeftover.forEach(value => { defaultEntriesLeftover.forEach(value => {
finalParamsEntries.push(value); finalParamsEntries.push(value);

View File

@@ -4,7 +4,7 @@ import {
parseQueryString, parseQueryString,
getQSConfig, getQSConfig,
addParams, addParams,
removeParams removeParams,
} from './qs'; } from './qs';
describe('qs (qs.js)', () => { describe('qs (qs.js)', () => {
@@ -13,14 +13,19 @@ describe('qs (qs.js)', () => {
[ [
[null, ''], [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'], { 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',
],
].forEach(([params, expectedQueryString]) => {
const actualQueryString = encodeQueryString(params);
expect(actualQueryString).toEqual(expectedQueryString); expect(actualQueryString).toEqual(expectedQueryString);
}); });
}); });
test('encodeQueryString omits null values', () => { test('encodeQueryString omits null values', () => {
@@ -35,7 +40,7 @@ describe('qs (qs.js)', () => {
describe('encodeNonDefaultQueryString', () => { describe('encodeNonDefaultQueryString', () => {
const config = { const config = {
namespace: null, namespace: null,
defaultParams: { page: 1, page_size: 5, order_by: 'name'}, defaultParams: { page: 1, page_size: 5, order_by: 'name' },
integerFields: ['page'], 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', page: 1, page_size: 5 }, 'order_by=-name'], [{ 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'], { order_by: '-name', page: 3, page_size: 10 },
] 'order_by=-name&page=3&page_size=10',
.forEach(([params, expectedQueryString]) => { ],
const actualQueryString = encodeNonDefaultQueryString(config, params); [
{ 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', () => { test('encodeNonDefaultQueryString omits null values', () => {
@@ -114,7 +124,7 @@ describe('qs (qs.js)', () => {
const query = ''; const query = '';
expect(parseQueryString(config, query)).toEqual({ expect(parseQueryString(config, query)).toEqual({
page: 1, page: 1,
page_size: 15 page_size: 15,
}); });
}); });
@@ -222,7 +232,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?baz=bar&page=3'; const query = '?baz=bar&page=3';
const newParams = { bag: 'boom' } const newParams = { bag: 'boom' };
expect(addParams(config, query, newParams)).toEqual({ expect(addParams(config, query, newParams)).toEqual({
baz: 'bar', baz: 'bar',
bag: 'boom', bag: 'boom',
@@ -238,7 +248,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?baz=bar&baz=bang&page=3'; const query = '?baz=bar&baz=bang&page=3';
const newParams = { baz: 'boom' } const newParams = { baz: 'boom' };
expect(addParams(config, query, newParams)).toEqual({ expect(addParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang', 'boom'], baz: ['bar', 'bang', 'boom'],
page: 3, page: 3,
@@ -253,7 +263,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?baz=bar&baz=bang&page=3'; const query = '?baz=bar&baz=bang&page=3';
const newParams = { page: 5 } const newParams = { page: 5 };
expect(addParams(config, query, newParams)).toEqual({ expect(addParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang'], baz: ['bar', 'bang'],
page: 5, page: 5,
@@ -268,7 +278,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?baz=bar&baz=bang&page=3'; 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({ expect(addParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang', 'bust'], baz: ['bar', 'bang', 'bust'],
pat: 'pal', pat: 'pal',
@@ -284,7 +294,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?item.baz=bar&item.page=3'; const query = '?item.baz=bar&item.page=3';
const newParams = { bag: 'boom' } const newParams = { bag: 'boom' };
expect(addParams(config, query, newParams)).toEqual({ expect(addParams(config, query, newParams)).toEqual({
baz: 'bar', baz: 'bar',
bag: 'boom', bag: 'boom',
@@ -300,7 +310,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?item.baz=bar&foo.page=3'; const query = '?item.baz=bar&foo.page=3';
const newParams = { bag: 'boom' } const newParams = { bag: 'boom' };
expect(addParams(config, query, newParams)).toEqual({ expect(addParams(config, query, newParams)).toEqual({
baz: 'bar', baz: 'bar',
bag: 'boom', bag: 'boom',
@@ -316,7 +326,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?item.baz=bar&item.baz=bang&item.page=3'; 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({ expect(addParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang', 'boom'], baz: ['bar', 'bang', 'boom'],
page: 3, page: 3,
@@ -331,7 +341,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?item.baz=bar&item.baz=bang&item.page=3'; 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({ expect(addParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang'], baz: ['bar', 'bang'],
page: 5, page: 5,
@@ -346,7 +356,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?item.baz=bar&item.baz=bang&item.page=3'; 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({ expect(addParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang', 'bust'], baz: ['bar', 'bang', 'bust'],
pat: 'pal', pat: 'pal',
@@ -364,7 +374,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?baz=bar&page=3&bag=boom'; const query = '?baz=bar&page=3&bag=boom';
const newParams = { bag: 'boom' } const newParams = { bag: 'boom' };
expect(removeParams(config, query, newParams)).toEqual({ expect(removeParams(config, query, newParams)).toEqual({
baz: 'bar', baz: 'bar',
page: 3, page: 3,
@@ -379,7 +389,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?baz=bar&baz=bang&page=3'; const query = '?baz=bar&baz=bang&page=3';
const newParams = { baz: 'bar' } const newParams = { baz: 'bar' };
expect(removeParams(config, query, newParams)).toEqual({ expect(removeParams(config, query, newParams)).toEqual({
baz: 'bang', baz: 'bang',
page: 3, page: 3,
@@ -394,7 +404,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?baz=bar&baz=bang&baz=bust&page=3'; const query = '?baz=bar&baz=bang&baz=bust&page=3';
const newParams = { baz: 'bar' } const newParams = { baz: 'bar' };
expect(removeParams(config, query, newParams)).toEqual({ expect(removeParams(config, query, newParams)).toEqual({
baz: ['bang', 'bust'], baz: ['bang', 'bust'],
page: 3, page: 3,
@@ -409,7 +419,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?baz=bar&baz=bang&page=3'; const query = '?baz=bar&baz=bang&page=3';
const newParams = { page: 3 } const newParams = { page: 3 };
expect(removeParams(config, query, newParams)).toEqual({ expect(removeParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang'], baz: ['bar', 'bang'],
page: 1, page: 1,
@@ -424,7 +434,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?baz=bar&baz=bang&baz=bust&pat=pal&page=3'; 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({ expect(removeParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang'], baz: ['bar', 'bang'],
page: 3, page: 3,
@@ -439,7 +449,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?item.baz=bar&item.page=3'; const query = '?item.baz=bar&item.page=3';
const newParams = { baz: 'bar' } const newParams = { baz: 'bar' };
expect(removeParams(config, query, newParams)).toEqual({ expect(removeParams(config, query, newParams)).toEqual({
page: 3, page: 3,
page_size: 15, page_size: 15,
@@ -453,7 +463,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?item.baz=bar&foo.page=3'; const query = '?item.baz=bar&foo.page=3';
const newParams = { baz: 'bar' } const newParams = { baz: 'bar' };
expect(removeParams(config, query, newParams)).toEqual({ expect(removeParams(config, query, newParams)).toEqual({
page: 1, page: 1,
page_size: 15, page_size: 15,
@@ -467,7 +477,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?item.baz=bar&item.baz=bang&item.page=3'; 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({ expect(removeParams(config, query, newParams)).toEqual({
baz: 'bang', baz: 'bang',
page: 3, page: 3,
@@ -482,7 +492,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?item.baz=bar&item.baz=bang&item.baz=bust&item.page=3'; 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({ expect(removeParams(config, query, newParams)).toEqual({
baz: ['bang', 'bust'], baz: ['bang', 'bust'],
page: 3, page: 3,
@@ -497,7 +507,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?item.baz=bar&item.baz=bang&item.page=3'; 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({ expect(removeParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang'], baz: ['bar', 'bang'],
page: 1, page: 1,
@@ -511,8 +521,9 @@ describe('qs (qs.js)', () => {
defaultParams: { page: 1, page_size: 15 }, defaultParams: { page: 1, page_size: 15 },
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}; };
const query = '?item.baz=bar&item.baz=bang&item.baz=bust&item.pat=pal&item.page=3'; const query =
const newParams = { baz: 'bust', pat: 'pal' } '?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({ expect(removeParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang'], baz: ['bar', 'bang'],
page: 3, page: 3,