Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] Task/hostlist pagination (#63722) #64430

Merged
merged 1 commit into from
Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { HostListPagination, ServerApiError } from '../../types';
import { ServerApiError } from '../../types';
import { HostResultList, HostInfo } from '../../../../../common/types';

interface ServerReturnedHostList {
type: 'serverReturnedHostList';
payload: HostResultList;
}

interface ServerFailedToReturnHostList {
type: 'serverFailedToReturnHostList';
payload: ServerApiError;
}

interface ServerReturnedHostDetails {
type: 'serverReturnedHostDetails';
payload: HostInfo;
Expand All @@ -22,13 +27,8 @@ interface ServerFailedToReturnHostDetails {
payload: ServerApiError;
}

interface UserPaginatedHostList {
type: 'userPaginatedHostList';
payload: HostListPagination;
}

export type HostAction =
| ServerReturnedHostList
| ServerFailedToReturnHostList
| ServerReturnedHostDetails
| ServerFailedToReturnHostDetails
| UserPaginatedHostList;
| ServerFailedToReturnHostDetails;
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { CoreStart, HttpSetup } from 'kibana/public';
import { DepsStartMock, depsStartMock } from '../../mocks';
import { AppAction, HostState, HostIndexUIQueryParams } from '../../types';
import { Immutable, HostResultList } from '../../../../../common/types';
import { History, createBrowserHistory } from 'history';
import { hostMiddlewareFactory } from './middleware';
import { applyMiddleware, Store, createStore } from 'redux';
import { hostListReducer } from './reducer';
import { coreMock } from 'src/core/public/mocks';
import { urlFromQueryParams } from '../../view/hosts/url_from_query_params';
import { uiQueryParams } from './selectors';
import { mockHostResultList } from './mock_host_result_list';
import { MiddlewareActionSpyHelper, createSpyMiddleware } from '../test_utils';

describe('host list pagination: ', () => {
let fakeCoreStart: jest.Mocked<CoreStart>;
let depsStart: DepsStartMock;
let fakeHttpServices: jest.Mocked<HttpSetup>;
let history: History<never>;
let store: Store<Immutable<HostState>, Immutable<AppAction>>;
let queryParams: () => HostIndexUIQueryParams;
let waitForAction: MiddlewareActionSpyHelper['waitForAction'];
let actionSpyMiddleware;
const getEndpointListApiResponse = (): HostResultList => {
return mockHostResultList({ request_page_size: 1, request_page_index: 1, total: 10 });
};

let historyPush: (params: HostIndexUIQueryParams) => void;
beforeEach(() => {
fakeCoreStart = coreMock.createStart();
depsStart = depsStartMock();
fakeHttpServices = fakeCoreStart.http as jest.Mocked<HttpSetup>;
history = createBrowserHistory();
const middleware = hostMiddlewareFactory(fakeCoreStart, depsStart);
({ actionSpyMiddleware, waitForAction } = createSpyMiddleware<HostState>());
store = createStore(hostListReducer, applyMiddleware(middleware, actionSpyMiddleware));

history.listen(location => {
store.dispatch({ type: 'userChangedUrl', payload: location });
});

queryParams = () => uiQueryParams(store.getState());

historyPush = (nextQueryParams: HostIndexUIQueryParams): void => {
return history.push(urlFromQueryParams(nextQueryParams));
};
});

describe('when the user enteres the host list for the first time', () => {
it('the api is called with page_index and page_size defaulting to 0 and 10 respectively', async () => {
const apiResponse = getEndpointListApiResponse();
fakeHttpServices.post.mockResolvedValue(apiResponse);
expect(fakeHttpServices.post).not.toHaveBeenCalled();

store.dispatch({
type: 'userChangedUrl',
payload: {
...history.location,
pathname: '/hosts',
},
});
await waitForAction('serverReturnedHostList');
expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', {
body: JSON.stringify({
paging_properties: [{ page_index: '0' }, { page_size: '10' }],
}),
});
});
});
describe('when a new page size is passed', () => {
it('should modify the url correctly', () => {
historyPush({ ...queryParams(), page_size: '20' });
expect(queryParams()).toMatchInlineSnapshot(`
Object {
"page_index": "0",
"page_size": "20",
}
`);
});
});
describe('when an invalid page size is passed', () => {
it('should modify the page size in the url to the default page size', () => {
historyPush({ ...queryParams(), page_size: '1' });
expect(queryParams()).toEqual({ page_index: '0', page_size: '10' });
});
});

describe('when a negative page size is passed', () => {
it('should modify the page size in the url to the default page size', () => {
historyPush({ ...queryParams(), page_size: '-1' });
expect(queryParams()).toEqual({ page_index: '0', page_size: '10' });
});
});

describe('when a new page index is passed', () => {
it('should modify the page index in the url correctly', () => {
historyPush({ ...queryParams(), page_index: '2' });
expect(queryParams()).toEqual({ page_index: '2', page_size: '10' });
});
});

describe('when a negative page index is passed', () => {
it('should modify the page index in the url to the default page index', () => {
historyPush({ ...queryParams(), page_index: '-2' });
expect(queryParams()).toEqual({ page_index: '0', page_size: '10' });
});
});

describe('when invalid params are passed in the url', () => {
it('ignores non-numeric values for page_index and page_size', () => {
historyPush({ ...queryParams, page_index: 'one', page_size: 'fifty' });
expect(queryParams()).toEqual({ page_index: '0', page_size: '10' });
});

it('ignores unknown url search params', () => {
store.dispatch({
type: 'userChangedUrl',
payload: {
...history.location,
pathname: '/hosts',
search: '?foo=bar',
},
});
expect(queryParams()).toEqual({ page_index: '0', page_size: '10' });
});

it('ignores multiple values of the same query params except the last value', () => {
store.dispatch({
type: 'userChangedUrl',
payload: {
...history.location,
pathname: '/hosts',
search: '?page_index=2&page_index=3&page_size=20&page_size=50',
},
});
expect(queryParams()).toEqual({ page_index: '3', page_size: '50' });
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

import { createStore, Dispatch, Store } from 'redux';
import { HostAction, hostListReducer } from './index';
import { HostListState } from '../../types';
import { HostState } from '../../types';
import { listData } from './selectors';
import { mockHostResultList } from './mock_host_result_list';

describe('HostList store concerns', () => {
let store: Store<HostListState>;
let store: Store<HostState>;
let dispatch: Dispatch<HostAction>;
const createTestStore = () => {
store = createStore(hostListReducer);
Expand All @@ -37,6 +37,11 @@ describe('HostList store concerns', () => {
pageIndex: 0,
total: 0,
loading: false,
error: undefined,
details: undefined,
detailsLoading: false,
detailsError: undefined,
location: undefined,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,23 @@ import { coreMock } from '../../../../../../../../src/core/public/mocks';
import { History, createBrowserHistory } from 'history';
import { hostListReducer, hostMiddlewareFactory } from './index';
import { HostResultList, Immutable } from '../../../../../common/types';
import { HostListState } from '../../types';
import { HostState } from '../../types';
import { AppAction } from '../action';
import { listData } from './selectors';
import { DepsStartMock, depsStartMock } from '../../mocks';
import { mockHostResultList } from './mock_host_result_list';
import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../test_utils';

describe('host list middleware', () => {
const sleep = (ms = 100) => new Promise(wakeup => setTimeout(wakeup, ms));
let fakeCoreStart: jest.Mocked<CoreStart>;
let depsStart: DepsStartMock;
let fakeHttpServices: jest.Mocked<HttpSetup>;
type HostListStore = Store<Immutable<HostListState>, Immutable<AppAction>>;
type HostListStore = Store<Immutable<HostState>, Immutable<AppAction>>;
let store: HostListStore;
let getState: HostListStore['getState'];
let dispatch: HostListStore['dispatch'];
let waitForAction: MiddlewareActionSpyHelper['waitForAction'];
let actionSpyMiddleware;

let history: History<never>;
const getEndpointListApiResponse = (): HostResultList => {
Expand All @@ -33,15 +35,16 @@ describe('host list middleware', () => {
fakeCoreStart = coreMock.createStart({ basePath: '/mock' });
depsStart = depsStartMock();
fakeHttpServices = fakeCoreStart.http as jest.Mocked<HttpSetup>;
({ actionSpyMiddleware, waitForAction } = createSpyMiddleware<HostState>());
store = createStore(
hostListReducer,
applyMiddleware(hostMiddlewareFactory(fakeCoreStart, depsStart))
applyMiddleware(hostMiddlewareFactory(fakeCoreStart, depsStart), actionSpyMiddleware)
);
getState = store.getState;
dispatch = store.dispatch;
history = createBrowserHistory();
});
test('handles `userChangedUrl`', async () => {
it('handles `userChangedUrl`', async () => {
const apiResponse = getEndpointListApiResponse();
fakeHttpServices.post.mockResolvedValue(apiResponse);
expect(fakeHttpServices.post).not.toHaveBeenCalled();
Expand All @@ -53,10 +56,10 @@ describe('host list middleware', () => {
pathname: '/hosts',
},
});
await sleep();
await waitForAction('serverReturnedHostList');
expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', {
body: JSON.stringify({
paging_properties: [{ page_index: 0 }, { page_size: 10 }],
paging_properties: [{ page_index: '0' }, { page_size: '10' }],
}),
});
expect(listData(getState())).toEqual(apiResponse.hosts.map(hostInfo => hostInfo.metadata));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,64 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { HostResultList } from '../../../../../common/types';
import { isOnHostPage, hasSelectedHost, uiQueryParams, listData } from './selectors';
import { HostState } from '../../types';
import { ImmutableMiddlewareFactory } from '../../types';
import { pageIndex, pageSize, isOnHostPage, hasSelectedHost, uiQueryParams } from './selectors';
import { HostListState } from '../../types';

export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostListState> = coreStart => {
export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = coreStart => {
return ({ getState, dispatch }) => next => async action => {
next(action);
const state = getState();
if (
(action.type === 'userChangedUrl' &&
isOnHostPage(state) &&
hasSelectedHost(state) !== true) ||
action.type === 'userPaginatedHostList'
action.type === 'userChangedUrl' &&
isOnHostPage(state) &&
hasSelectedHost(state) !== true
) {
const hostPageIndex = pageIndex(state);
const hostPageSize = pageSize(state);
const response = await coreStart.http.post('/api/endpoint/metadata', {
body: JSON.stringify({
paging_properties: [{ page_index: hostPageIndex }, { page_size: hostPageSize }],
}),
});
response.request_page_index = hostPageIndex;
dispatch({
type: 'serverReturnedHostList',
payload: response,
});
const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(state);
try {
const response = await coreStart.http.post<HostResultList>('/api/endpoint/metadata', {
body: JSON.stringify({
paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }],
}),
});
response.request_page_index = Number(pageIndex);
dispatch({
type: 'serverReturnedHostList',
payload: response,
});
} catch (error) {
dispatch({
type: 'serverFailedToReturnHostList',
payload: error,
});
}
}
if (action.type === 'userChangedUrl' && hasSelectedHost(state) !== false) {
// If user navigated directly to a host details page, load the host list
if (listData(state).length === 0) {
const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(state);
try {
const response = await coreStart.http.post('/api/endpoint/metadata', {
body: JSON.stringify({
paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }],
}),
});
response.request_page_index = Number(pageIndex);
dispatch({
type: 'serverReturnedHostList',
payload: response,
});
} catch (error) {
dispatch({
type: 'serverFailedToReturnHostList',
payload: error,
});
return;
}
}

// call the host details api
const { selected_host: selectedHost } = uiQueryParams(state);
try {
const response = await coreStart.http.get(`/api/endpoint/metadata/${selectedHost}`);
Expand Down
Loading