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'));
});
}