From f6dc67470d5796ed99df1013c4bf007ab3492167 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Tue, 18 Feb 2020 15:21:10 -0500 Subject: [PATCH] Alert list frontend pagination (#57142) --- .../public/applications/endpoint/index.tsx | 55 ++++--- .../endpoint/store/alerts/action.ts | 5 +- .../endpoint/store/alerts/alert_list.test.ts | 85 +++++++++++ .../alerts/alert_list_pagination.test.ts | 98 +++++++++++++ .../endpoint/store/alerts/middleware.ts | 18 +-- .../endpoint/store/alerts/reducer.ts | 6 + .../endpoint/store/alerts/selectors.ts | 70 +++++++++ .../applications/endpoint/store/index.ts | 5 +- .../endpoint/store/routing/action.ts | 10 +- .../applications/endpoint/store/selectors.ts | 31 ---- .../public/applications/endpoint/types.ts | 17 ++- .../view/alerts/hooks/use_alerts_selector.ts | 12 ++ .../endpoint/view/alerts/index.tsx | 136 +++++++++++++----- .../functional/apps/endpoint/alert_list.ts | 25 ++++ x-pack/test/functional/apps/endpoint/index.ts | 1 + 15 files changed, 469 insertions(+), 105 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/selectors.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/hooks/use_alerts_selector.ts create mode 100644 x-pack/test/functional/apps/endpoint/alert_list.ts diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 7bb3b13525914..8530d6206d398 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -8,20 +8,22 @@ import * as React from 'react'; import ReactDOM from 'react-dom'; import { CoreStart, AppMountParameters } from 'kibana/public'; import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; -import { Route, BrowserRouter, Switch } from 'react-router-dom'; -import { Provider } from 'react-redux'; +import { Route, Switch, BrowserRouter, useLocation } from 'react-router-dom'; +import { Provider, useDispatch } from 'react-redux'; import { Store } from 'redux'; +import { memo } from 'react'; import { appStoreFactory } from './store'; import { AlertIndex } from './view/alerts'; import { ManagementList } from './view/managing'; import { PolicyList } from './view/policy'; +import { AppAction } from './store/action'; +import { EndpointAppLocation } from './types'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. */ export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMountParameters) { coreStart.http.get('/api/endpoint/hello-world'); - const store = appStoreFactory(coreStart); ReactDOM.render(, element); @@ -31,6 +33,13 @@ export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMou }; } +const RouteCapture = memo(({ children }) => { + const location: EndpointAppLocation = useLocation(); + const dispatch: (action: AppAction) => unknown = useDispatch(); + dispatch({ type: 'userChangedUrl', payload: location }); + return <>{children}; +}); + interface RouterProps { basename: string; store: Store; @@ -40,25 +49,27 @@ const AppRoot: React.FunctionComponent = React.memo(({ basename, st - - ( -

- -

- )} - /> - - - - ( - - )} - /> -
+ + + ( +

+ +

+ )} + /> + + } /> + + ( + + )} + /> +
+
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/action.ts index 464a04eff5ebd..a628a95003a7f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/action.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Immutable } from '../../../../../common/types'; import { AlertListData } from '../../types'; interface ServerReturnedAlertsData { - type: 'serverReturnedAlertsData'; - payload: AlertListData; + readonly type: 'serverReturnedAlertsData'; + readonly payload: Immutable; } export type AlertAction = ServerReturnedAlertsData; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts new file mode 100644 index 0000000000000..6ba7a34ae81d1 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts @@ -0,0 +1,85 @@ +/* + * 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 { Store, createStore, applyMiddleware } from 'redux'; +import { History } from 'history'; +import { alertListReducer } from './reducer'; +import { AlertListState } from '../../types'; +import { alertMiddlewareFactory } from './middleware'; +import { AppAction } from '../action'; +import { coreMock } from 'src/core/public/mocks'; +import { AlertResultList } from '../../../../../common/types'; +import { isOnAlertPage } from './selectors'; +import { createBrowserHistory } from 'history'; + +describe('alert list tests', () => { + let store: Store; + let coreStart: ReturnType; + let history: History; + beforeEach(() => { + coreStart = coreMock.createStart(); + history = createBrowserHistory(); + const middleware = alertMiddlewareFactory(coreStart); + store = createStore(alertListReducer, applyMiddleware(middleware)); + }); + describe('when the user navigates to the alert list page', () => { + beforeEach(() => { + coreStart.http.get.mockImplementation(async () => { + const response: AlertResultList = { + alerts: [ + { + '@timestamp': new Date(1542341895000), + agent: { + id: 'ced9c68e-b94a-4d66-bb4c-6106514f0a2f', + version: '3.0.0', + }, + event: { + action: 'open', + }, + file_classification: { + malware_classification: { + score: 3, + }, + }, + host: { + hostname: 'HD-c15-bc09190a', + ip: '10.179.244.14', + os: { + name: 'Windows', + }, + }, + thread: {}, + }, + ], + total: 1, + request_page_size: 10, + request_page_index: 0, + result_from_index: 0, + }; + return response; + }); + + // Simulates user navigating to the /alerts page + store.dispatch({ + type: 'userChangedUrl', + payload: { + ...history.location, + pathname: '/alerts', + }, + }); + }); + + it("should recognize it's on the alert list page", () => { + const actual = isOnAlertPage(store.getState()); + expect(actual).toBe(true); + }); + + it('should return alertListData', () => { + const actualResponseLength = store.getState().alerts.length; + expect(actualResponseLength).toEqual(1); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts new file mode 100644 index 0000000000000..77708a3c77e2b --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { Store, createStore, applyMiddleware } from 'redux'; +import { History } from 'history'; +import { alertListReducer } from './reducer'; +import { AlertListState } from '../../types'; +import { alertMiddlewareFactory } from './middleware'; +import { AppAction } from '../action'; +import { coreMock } from 'src/core/public/mocks'; +import { createBrowserHistory } from 'history'; +import { + urlFromNewPageSizeParam, + paginationDataFromUrl, + urlFromNewPageIndexParam, +} from './selectors'; + +describe('alert list pagination', () => { + let store: Store; + let coreStart: ReturnType; + let history: History; + beforeEach(() => { + coreStart = coreMock.createStart(); + history = createBrowserHistory(); + const middleware = alertMiddlewareFactory(coreStart); + store = createStore(alertListReducer, applyMiddleware(middleware)); + }); + describe('when the user navigates to the alert list page', () => { + describe('when a new page size is passed', () => { + beforeEach(() => { + const urlPageSizeSelector = urlFromNewPageSizeParam(store.getState()); + history.push(urlPageSizeSelector(1)); + store.dispatch({ type: 'userChangedUrl', payload: history.location }); + }); + it('should modify the url correctly', () => { + const actualPaginationQuery = paginationDataFromUrl(store.getState()); + expect(actualPaginationQuery).toMatchInlineSnapshot(` + Object { + "page_size": "1", + } + `); + }); + + describe('and then a new page index is passed', () => { + beforeEach(() => { + const urlPageIndexSelector = urlFromNewPageIndexParam(store.getState()); + history.push(urlPageIndexSelector(1)); + store.dispatch({ type: 'userChangedUrl', payload: history.location }); + }); + it('should modify the url in the correct order', () => { + const actualPaginationQuery = paginationDataFromUrl(store.getState()); + expect(actualPaginationQuery).toMatchInlineSnapshot(` + Object { + "page_index": "1", + "page_size": "1", + } + `); + }); + }); + }); + + describe('when a new page index is passed', () => { + beforeEach(() => { + const urlPageIndexSelector = urlFromNewPageIndexParam(store.getState()); + history.push(urlPageIndexSelector(1)); + store.dispatch({ type: 'userChangedUrl', payload: history.location }); + }); + it('should modify the url correctly', () => { + const actualPaginationQuery = paginationDataFromUrl(store.getState()); + expect(actualPaginationQuery).toMatchInlineSnapshot(` + Object { + "page_index": "1", + } + `); + }); + + describe('and then a new page size is passed', () => { + beforeEach(() => { + const urlPageSizeSelector = urlFromNewPageSizeParam(store.getState()); + history.push(urlPageSizeSelector(1)); + store.dispatch({ type: 'userChangedUrl', payload: history.location }); + }); + it('should modify the url correctly and reset index to `0`', () => { + const actualPaginationQuery = paginationDataFromUrl(store.getState()); + expect(actualPaginationQuery).toMatchInlineSnapshot(` + Object { + "page_index": "0", + "page_size": "1", + } + `); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts index 4a7fac147852b..059507c8f0658 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts @@ -4,19 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; -import { HttpFetchQuery } from 'src/core/public'; +import { HttpFetchQuery } from 'kibana/public'; +import { AlertResultList } from '../../../../../common/types'; import { AppAction } from '../action'; -import { MiddlewareFactory, AlertListData } from '../../types'; - -export const alertMiddlewareFactory: MiddlewareFactory = coreStart => { - const qp = parse(window.location.search.slice(1), { sort: false }); +import { MiddlewareFactory, AlertListState } from '../../types'; +import { isOnAlertPage, paginationDataFromUrl } from './selectors'; +export const alertMiddlewareFactory: MiddlewareFactory = coreStart => { return api => next => async (action: AppAction) => { next(action); - if (action.type === 'userNavigatedToPage' && action.payload === 'alertsPage') { - const response: AlertListData = await coreStart.http.get('/api/endpoint/alerts', { - query: qp as HttpFetchQuery, + const state = api.getState(); + if (action.type === 'userChangedUrl' && isOnAlertPage(state)) { + const response: AlertResultList = await coreStart.http.get(`/api/endpoint/alerts`, { + query: paginationDataFromUrl(state) as HttpFetchQuery, }); api.dispatch({ type: 'serverReturnedAlertsData', payload: response }); } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts index de79476245d29..6369bb9fb2d0d 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts @@ -15,6 +15,7 @@ const initialState = (): AlertListState => { request_page_index: 0, result_from_index: 0, total: 0, + location: undefined, }; }; @@ -27,6 +28,11 @@ export const alertListReducer: Reducer = ( ...state, ...action.payload, }; + } else if (action.type === 'userChangedUrl') { + return { + ...state, + location: action.payload, + }; } return state; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts index 51903a0a641e8..6ad053888729c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts @@ -4,6 +4,76 @@ * you may not use this file except in compliance with the Elastic License. */ +import qs from 'querystring'; import { AlertListState } from '../../types'; +/** + * Returns the Alert Data array from state + */ export const alertListData = (state: AlertListState) => state.alerts; + +/** + * Returns the alert list pagination data from state + */ +export const alertListPagination = (state: AlertListState) => { + return { + pageIndex: state.request_page_index, + pageSize: state.request_page_size, + resultFromIndex: state.result_from_index, + total: state.total, + }; +}; + +/** + * Returns a boolean based on whether or not the user is on the alerts page + */ +export const isOnAlertPage = (state: AlertListState): boolean => { + return state.location ? state.location.pathname === '/alerts' : false; +}; + +/** + * Returns the query object received from parsing the URL query params + */ +export const paginationDataFromUrl = (state: AlertListState): qs.ParsedUrlQuery => { + if (state.location) { + // Removes the `?` from the beginning of query string if it exists + const query = qs.parse(state.location.search.slice(1)); + return { + ...(query.page_size ? { page_size: query.page_size } : {}), + ...(query.page_index ? { page_index: query.page_index } : {}), + }; + } else { + return {}; + } +}; + +/** + * Returns a function that takes in a new page size and returns a new query param string + */ +export const urlFromNewPageSizeParam: ( + state: AlertListState +) => (newPageSize: number) => string = state => { + return newPageSize => { + const urlPaginationData = paginationDataFromUrl(state); + urlPaginationData.page_size = newPageSize.toString(); + + // Only set the url back to page zero if the user has changed the page index already + if (urlPaginationData.page_index !== undefined) { + urlPaginationData.page_index = '0'; + } + return '?' + qs.stringify(urlPaginationData); + }; +}; + +/** + * Returns a function that takes in a new page index and returns a new query param string + */ +export const urlFromNewPageIndexParam: ( + state: AlertListState +) => (newPageIndex: number) => string = state => { + return newPageIndex => { + const urlPaginationData = paginationDataFromUrl(state); + urlPaginationData.page_index = newPageIndex.toString(); + return '?' + qs.stringify(urlPaginationData); + }; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts index 8fe61ae01d319..3aeeeaf1c09e2 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -53,7 +53,6 @@ export const appStoreFactory = (coreStart: CoreStart): Store => { appReducer, composeWithReduxDevTools( applyMiddleware( - alertMiddlewareFactory(coreStart), substateMiddlewareFactory( globalState => globalState.managementList, managementMiddlewareFactory(coreStart) @@ -61,6 +60,10 @@ export const appStoreFactory = (coreStart: CoreStart): Store => { substateMiddlewareFactory( globalState => globalState.policyList, policyListMiddlewareFactory(coreStart) + ), + substateMiddlewareFactory( + globalState => globalState.alertList, + alertMiddlewareFactory(coreStart) ) ) ) diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/routing/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/routing/action.ts index 9080af8c91817..c7e9970e58c30 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/routing/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/routing/action.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PageId } from '../../../../../common/types'; +import { PageId, Immutable } from '../../../../../common/types'; +import { EndpointAppLocation } from '../alerts'; interface UserNavigatedToPage { readonly type: 'userNavigatedToPage'; @@ -16,4 +17,9 @@ interface UserNavigatedFromPage { readonly payload: PageId; } -export type RoutingAction = UserNavigatedToPage | UserNavigatedFromPage; +interface UserChangedUrl { + readonly type: 'userChangedUrl'; + readonly payload: Immutable; +} + +export type RoutingAction = UserNavigatedToPage | UserNavigatedFromPage | UserChangedUrl; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors.ts deleted file mode 100644 index 2766707271cde..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 { GlobalState } from '../types'; -import * as alertListSelectors from './alerts/selectors'; - -export const alertListData = composeSelectors( - alertListStateSelector, - alertListSelectors.alertListData -); - -/** - * Returns the alert list state from within Global State - */ -function alertListStateSelector(state: GlobalState) { - return state.alertList; -} - -/** - * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a - * concern-specific selector. `selector` should return the concern-specific state. - */ -function composeSelectors( - selector: (state: OuterState) => InnerState, - secondSelector: (state: InnerState) => ReturnValue -): (state: OuterState) => ReturnValue { - return state => secondSelector(selector(state)); -} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index 6b20012592fd9..d07521d09a119 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -8,7 +8,7 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { CoreStart } from 'kibana/public'; import { EndpointMetadata } from '../../../common/types'; import { AppAction } from './store/action'; -import { AlertResultList } from '../../../common/types'; +import { AlertResultList, Immutable } from '../../../common/types'; export type MiddlewareFactory = ( coreStart: CoreStart @@ -63,8 +63,6 @@ export interface GlobalState { readonly policyList: PolicyListState; } -export type AlertListData = AlertResultList; -export type AlertListState = AlertResultList; export type CreateStructuredSelector = < SelectorMap extends { [key: string]: (...args: never[]) => unknown } >( @@ -74,3 +72,16 @@ export type CreateStructuredSelector = < ) => { [Key in keyof SelectorMap]: ReturnType; }; + +export interface EndpointAppLocation { + pathname: string; + search: string; + state: never; + hash: string; + key?: string; +} + +export type AlertListData = AlertResultList; +export type AlertListState = Immutable & { + readonly location?: Immutable; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/hooks/use_alerts_selector.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/hooks/use_alerts_selector.ts new file mode 100644 index 0000000000000..d3962f908757c --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/hooks/use_alerts_selector.ts @@ -0,0 +1,12 @@ +/* + * 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 { useSelector } from 'react-redux'; +import { GlobalState, AlertListState } from '../../../types'; + +export function useAlertListSelector(selector: (state: AlertListState) => TSelected) { + return useSelector((state: GlobalState) => selector(state.alertList)); +} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx index 8c32426dcc868..045b82200b11b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx @@ -4,41 +4,94 @@ * you may not use this file except in compliance with the Elastic License. */ -import { memo, useState, useMemo } from 'react'; +import { memo, useState, useMemo, useCallback } from 'react'; import React from 'react'; -import { EuiDataGrid } from '@elastic/eui'; -import { useSelector } from 'react-redux'; +import { EuiDataGrid, EuiDataGridColumn, EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import * as selectors from '../../store/selectors'; -import { usePageId } from '../use_page_id'; +import { useHistory } from 'react-router-dom'; +import * as selectors from '../../store/alerts/selectors'; +import { useAlertListSelector } from './hooks/use_alerts_selector'; export const AlertIndex = memo(() => { - usePageId('alertsPage'); + const history = useHistory(); - const columns: Array<{ id: string }> = useMemo(() => { + const columns: EuiDataGridColumn[] = useMemo(() => { return [ - { id: 'alert_type' }, - { id: 'event_type' }, - { id: 'os' }, - { id: 'ip_address' }, - { id: 'host_name' }, - { id: 'timestamp' }, - { id: 'archived' }, - { id: 'malware_score' }, + { + id: 'alert_type', + display: i18n.translate('xpack.endpoint.application.endpoint.alerts.alertType', { + defaultMessage: 'Alert Type', + }), + }, + { + id: 'event_type', + display: i18n.translate('xpack.endpoint.application.endpoint.alerts.eventType', { + defaultMessage: 'Event Type', + }), + }, + { + id: 'os', + display: i18n.translate('xpack.endpoint.application.endpoint.alerts.os', { + defaultMessage: 'OS', + }), + }, + { + id: 'ip_address', + display: i18n.translate('xpack.endpoint.application.endpoint.alerts.ipAddress', { + defaultMessage: 'IP Address', + }), + }, + { + id: 'host_name', + display: i18n.translate('xpack.endpoint.application.endpoint.alerts.hostName', { + defaultMessage: 'Host Name', + }), + }, + { + id: 'timestamp', + display: i18n.translate('xpack.endpoint.application.endpoint.alerts.timestamp', { + defaultMessage: 'Timestamp', + }), + }, + { + id: 'archived', + display: i18n.translate('xpack.endpoint.application.endpoint.alerts.archived', { + defaultMessage: 'Archived', + }), + }, + { + id: 'malware_score', + display: i18n.translate('xpack.endpoint.application.endpoint.alerts.malwareScore', { + defaultMessage: 'Malware Score', + }), + }, ]; }, []); - const [visibleColumns, setVisibleColumns] = useState(() => columns.map(({ id }) => id)); + const { pageIndex, pageSize, total } = useAlertListSelector(selectors.alertListPagination); + const urlFromNewPageSizeParam = useAlertListSelector(selectors.urlFromNewPageSizeParam); + const urlFromNewPageIndexParam = useAlertListSelector(selectors.urlFromNewPageIndexParam); + const alertListData = useAlertListSelector(selectors.alertListData); + + const onChangeItemsPerPage = useCallback( + newPageSize => history.push(urlFromNewPageSizeParam(newPageSize)), + [history, urlFromNewPageSizeParam] + ); + + const onChangePage = useCallback( + newPageIndex => history.push(urlFromNewPageIndexParam(newPageIndex)), + [history, urlFromNewPageIndexParam] + ); - const json = useSelector(selectors.alertListData); + const [visibleColumns, setVisibleColumns] = useState(() => columns.map(({ id }) => id)); const renderCellValue = useMemo(() => { return ({ rowIndex, columnId }: { rowIndex: number; columnId: string }) => { - if (rowIndex > json.length) { + if (rowIndex > total) { return null; } - const row = json[rowIndex]; + const row = alertListData[rowIndex % pageSize]; if (columnId === 'alert_type') { return i18n.translate( @@ -64,23 +117,36 @@ export const AlertIndex = memo(() => { } return null; }; - }, [json]); + }, [alertListData, pageSize, total]); + + const pagination = useMemo(() => { + return { + pageIndex, + pageSize, + pageSizeOptions: [10, 20, 50], + onChangeItemsPerPage, + onChangePage, + }; + }, [onChangeItemsPerPage, onChangePage, pageIndex, pageSize]); return ( - + + + + + + + ); }); diff --git a/x-pack/test/functional/apps/endpoint/alert_list.ts b/x-pack/test/functional/apps/endpoint/alert_list.ts new file mode 100644 index 0000000000000..089fa487ef1b8 --- /dev/null +++ b/x-pack/test/functional/apps/endpoint/alert_list.ts @@ -0,0 +1,25 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['common', 'endpoint']); + const testSubjects = getService('testSubjects'); + + describe('Endpoint Alert List', function() { + this.tags(['ciGroup7']); + before(async () => { + await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/alerts'); + }); + + it('loads the Alert List Page', async () => { + await testSubjects.existOrFail('alertListPage'); + }); + it('includes Alert list data grid', async () => { + await testSubjects.existOrFail('alertListGrid'); + }); + }); +} diff --git a/x-pack/test/functional/apps/endpoint/index.ts b/x-pack/test/functional/apps/endpoint/index.ts index 0ea9344a67aba..818c040f824d9 100644 --- a/x-pack/test/functional/apps/endpoint/index.ts +++ b/x-pack/test/functional/apps/endpoint/index.ts @@ -13,5 +13,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./landing_page')); loadTestFile(require.resolve('./management')); loadTestFile(require.resolve('./policy_list')); + loadTestFile(require.resolve('./alert_list')); }); }