diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts
index 0128cd3dd6df7..0dc3fc29ca805 100644
--- a/x-pack/plugins/endpoint/common/types.ts
+++ b/x-pack/plugins/endpoint/common/types.ts
@@ -118,4 +118,4 @@ export interface EndpointMetadata {
/**
* The PageId type is used for the payload when firing userNavigatedToPage actions
*/
-export type PageId = 'alertsPage' | 'endpointListPage';
+export type PageId = 'alertsPage' | 'managementPage';
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
index 9bea41126d296..a86c647e771d4 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
@@ -13,6 +13,7 @@ import { Provider } from 'react-redux';
import { Store } from 'redux';
import { appStoreFactory } from './store';
import { AlertIndex } from './view/alerts';
+import { ManagementList } from './view/managing';
/**
* This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle.
@@ -20,13 +21,12 @@ import { AlertIndex } from './view/alerts';
export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMountParameters) {
coreStart.http.get('/api/endpoint/hello-world');
- const [store, stopSagas] = appStoreFactory(coreStart);
+ const store = appStoreFactory(coreStart);
ReactDOM.render(, element);
return () => {
ReactDOM.unmountComponentAtNode(element);
- stopSagas();
};
}
@@ -49,22 +49,7 @@ const AppRoot: React.FunctionComponent = React.memo(({ basename, st
)}
/>
- {
- // FIXME: This is temporary. Will be removed in next PR for endpoint list
- store.dispatch({ type: 'userEnteredEndpointListPage' });
-
- return (
-
-
-
- );
- }}
- />
+
(
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts
index 593041af75c05..04c6cf7fc4634 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts
@@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EndpointListAction } from './endpoint_list';
+import { ManagementAction } from './managing';
import { AlertAction } from './alerts';
import { RoutingAction } from './routing';
-export type AppAction = EndpointListAction | AlertAction | RoutingAction;
+export type AppAction = ManagementAction | AlertAction | RoutingAction;
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/action.ts
deleted file mode 100644
index 02ec0f9d09035..0000000000000
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/action.ts
+++ /dev/null
@@ -1,25 +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 { EndpointListData } from './types';
-
-interface ServerReturnedEndpointList {
- type: 'serverReturnedEndpointList';
- payload: EndpointListData;
-}
-
-interface UserEnteredEndpointListPage {
- type: 'userEnteredEndpointListPage';
-}
-
-interface UserExitedEndpointListPage {
- type: 'userExitedEndpointListPage';
-}
-
-export type EndpointListAction =
- | ServerReturnedEndpointList
- | UserEnteredEndpointListPage
- | UserExitedEndpointListPage;
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.ts
deleted file mode 100644
index bdf0708457bb0..0000000000000
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.ts
+++ /dev/null
@@ -1,10 +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.
- */
-
-export { endpointListReducer } from './reducer';
-export { EndpointListAction } from './action';
-export { endpointListSaga } from './saga';
-export * from './types';
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/reducer.ts
deleted file mode 100644
index e57d9683e4707..0000000000000
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/reducer.ts
+++ /dev/null
@@ -1,36 +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 { Reducer } from 'redux';
-import { EndpointListState } from './types';
-import { AppAction } from '../action';
-
-const initialState = (): EndpointListState => {
- return {
- endpoints: [],
- request_page_size: 10,
- request_index: 0,
- total: 0,
- };
-};
-
-export const endpointListReducer: Reducer = (
- state = initialState(),
- action
-) => {
- if (action.type === 'serverReturnedEndpointList') {
- return {
- ...state,
- ...action.payload,
- };
- }
-
- if (action.type === 'userExitedEndpointListPage') {
- return initialState();
- }
-
- return state;
-};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts
deleted file mode 100644
index 6bf946873e179..0000000000000
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts
+++ /dev/null
@@ -1,118 +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 { CoreStart, HttpSetup } from 'kibana/public';
-import { applyMiddleware, createStore, Dispatch, Store } from 'redux';
-import { createSagaMiddleware, SagaContext } from '../../lib';
-import { endpointListSaga } from './saga';
-import { coreMock } from '../../../../../../../../src/core/public/mocks';
-import {
- EndpointData,
- EndpointListAction,
- EndpointListData,
- endpointListReducer,
- EndpointListState,
-} from './index';
-import { endpointListData } from './selectors';
-
-describe('endpoint list saga', () => {
- const sleep = (ms = 100) => new Promise(wakeup => setTimeout(wakeup, ms));
- let fakeCoreStart: jest.Mocked;
- let fakeHttpServices: jest.Mocked;
- let store: Store;
- let dispatch: Dispatch;
- let stopSagas: () => void;
-
- // TODO: consolidate the below ++ helpers in `index.test.ts` into a `test_helpers.ts`??
- const generateEndpoint = (): EndpointData => {
- return {
- machine_id: Math.random()
- .toString(16)
- .substr(2),
- created_at: new Date(),
- host: {
- name: '',
- hostname: '',
- ip: '',
- mac_address: '',
- os: {
- name: '',
- full: '',
- },
- },
- endpoint: {
- domain: '',
- is_base_image: true,
- active_directory_distinguished_name: '',
- active_directory_hostname: '',
- upgrade: {
- status: '',
- updated_at: new Date(),
- },
- isolation: {
- status: false,
- request_status: true,
- updated_at: new Date(),
- },
- policy: {
- name: '',
- id: '',
- },
- sensor: {
- persistence: true,
- status: {},
- },
- },
- };
- };
- const getEndpointListApiResponse = (): EndpointListData => {
- return {
- endpoints: [generateEndpoint()],
- request_page_size: 1,
- request_index: 1,
- total: 10,
- };
- };
-
- const endpointListSagaFactory = () => {
- return async (sagaContext: SagaContext) => {
- await endpointListSaga(sagaContext, fakeCoreStart).catch((e: Error) => {
- // eslint-disable-next-line no-console
- console.error(e);
- return Promise.reject(e);
- });
- };
- };
-
- beforeEach(() => {
- fakeCoreStart = coreMock.createStart({ basePath: '/mock' });
- fakeHttpServices = fakeCoreStart.http as jest.Mocked;
-
- const sagaMiddleware = createSagaMiddleware(endpointListSagaFactory());
- store = createStore(endpointListReducer, applyMiddleware(sagaMiddleware));
-
- sagaMiddleware.start();
- stopSagas = sagaMiddleware.stop;
- dispatch = store.dispatch;
- });
-
- afterEach(() => {
- stopSagas();
- });
-
- test('it handles `userEnteredEndpointListPage`', async () => {
- const apiResponse = getEndpointListApiResponse();
-
- fakeHttpServices.post.mockResolvedValue(apiResponse);
- expect(fakeHttpServices.post).not.toHaveBeenCalled();
-
- dispatch({ type: 'userEnteredEndpointListPage' });
- await sleep();
-
- expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/endpoints');
- expect(endpointListData(store.getState())).toEqual(apiResponse.endpoints);
- });
-});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.ts
deleted file mode 100644
index cc156cfa88002..0000000000000
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.ts
+++ /dev/null
@@ -1,26 +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 { CoreStart } from 'kibana/public';
-import { SagaContext } from '../../lib';
-import { EndpointListAction } from './action';
-
-export const endpointListSaga = async (
- { actionsAndState, dispatch }: SagaContext,
- coreStart: CoreStart
-) => {
- const { post: httpPost } = coreStart.http;
-
- for await (const { action } of actionsAndState()) {
- if (action.type === 'userEnteredEndpointListPage') {
- const response = await httpPost('/api/endpoint/endpoints');
- dispatch({
- type: 'serverReturnedEndpointList',
- payload: response,
- });
- }
- }
-};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts
deleted file mode 100644
index f2810dd89f857..0000000000000
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts
+++ /dev/null
@@ -1,54 +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.
- */
-
-// FIXME: temporary until server defined `interface` is moved
-export interface EndpointData {
- machine_id: string;
- created_at: Date;
- host: {
- name: string;
- hostname: string;
- ip: string;
- mac_address: string;
- os: {
- name: string;
- full: string;
- };
- };
- endpoint: {
- domain: string;
- is_base_image: boolean;
- active_directory_distinguished_name: string;
- active_directory_hostname: string;
- upgrade: {
- status?: string;
- updated_at?: Date;
- };
- isolation: {
- status: boolean;
- request_status?: string | boolean;
- updated_at?: Date;
- };
- policy: {
- name: string;
- id: string;
- };
- sensor: {
- persistence: boolean;
- status: object;
- };
- };
-}
-
-// FIXME: temporary until server defined `interface` is moved to a module we can reference
-export interface EndpointListData {
- endpoints: EndpointData[];
- request_page_size: number;
- request_index: number;
- total: number;
-}
-
-export type EndpointListState = EndpointListData;
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 a32f310392ca9..3bbcc3f25a6d8 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts
@@ -4,25 +4,62 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { createStore, compose, applyMiddleware, Store } from 'redux';
+import {
+ createStore,
+ compose,
+ applyMiddleware,
+ Store,
+ MiddlewareAPI,
+ Dispatch,
+ Middleware,
+} from 'redux';
import { CoreStart } from 'kibana/public';
-import { appSagaFactory } from './saga';
import { appReducer } from './reducer';
import { alertMiddlewareFactory } from './alerts/middleware';
+import { managementMiddlewareFactory } from './managing';
+import { GlobalState } from '../types';
+import { AppAction } from './action';
const composeWithReduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'EndpointApp' })
: compose;
-export const appStoreFactory = (coreStart: CoreStart): [Store, () => void] => {
- const sagaReduxMiddleware = appSagaFactory(coreStart);
+export type Selector = (state: S) => R;
+
+/**
+ * Wrap Redux Middleware and adjust 'getState()' to return the namespace from 'GlobalState that applies to the given Middleware concern.
+ *
+ * @param selector
+ * @param middleware
+ */
+export const substateMiddlewareFactory = (
+ selector: Selector,
+ middleware: Middleware<{}, Substate, Dispatch>
+): Middleware<{}, GlobalState, Dispatch> => {
+ return api => {
+ const substateAPI: MiddlewareAPI, Substate> = {
+ ...api,
+ getState() {
+ return selector(api.getState());
+ },
+ };
+ return middleware(substateAPI);
+ };
+};
+
+export const appStoreFactory = (coreStart: CoreStart): Store => {
const store = createStore(
appReducer,
composeWithReduxDevTools(
- applyMiddleware(alertMiddlewareFactory(coreStart), appSagaFactory(coreStart))
+ applyMiddleware(
+ alertMiddlewareFactory(coreStart),
+ substateMiddlewareFactory(
+ globalState => globalState.managementList,
+ managementMiddlewareFactory(coreStart)
+ )
+ )
)
);
- sagaReduxMiddleware.start();
- return [store, sagaReduxMiddleware.stop];
+ return store;
};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts
new file mode 100644
index 0000000000000..e916dc66c59f0
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts
@@ -0,0 +1,27 @@
+/*
+ * 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 { ManagementListPagination } from '../../types';
+import { EndpointResultList } from '../../../../../common/types';
+
+interface ServerReturnedManagementList {
+ type: 'serverReturnedManagementList';
+ payload: EndpointResultList;
+}
+
+interface UserExitedManagementList {
+ type: 'userExitedManagementList';
+}
+
+interface UserPaginatedManagementList {
+ type: 'userPaginatedManagementList';
+ payload: ManagementListPagination;
+}
+
+export type ManagementAction =
+ | ServerReturnedManagementList
+ | UserExitedManagementList
+ | UserPaginatedManagementList;
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts
similarity index 51%
rename from x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.test.ts
rename to x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts
index a46653f82ee45..dde0ba1e96a8a 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.test.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts
@@ -5,64 +5,52 @@
*/
import { createStore, Dispatch, Store } from 'redux';
-import { EndpointListAction, EndpointData, endpointListReducer, EndpointListState } from './index';
-import { endpointListData } from './selectors';
+import { ManagementAction, managementListReducer } from './index';
+import { EndpointMetadata } from '../../../../../common/types';
+import { ManagementListState } from '../../types';
+import { listData } from './selectors';
describe('endpoint_list store concerns', () => {
- let store: Store;
- let dispatch: Dispatch;
+ let store: Store;
+ let dispatch: Dispatch;
const createTestStore = () => {
- store = createStore(endpointListReducer);
+ store = createStore(managementListReducer);
dispatch = store.dispatch;
};
- const generateEndpoint = (): EndpointData => {
+ const generateEndpoint = (): EndpointMetadata => {
return {
- machine_id: Math.random()
- .toString(16)
- .substr(2),
- created_at: new Date(),
- host: {
- name: '',
- hostname: '',
- ip: '',
- mac_address: '',
- os: {
- name: '',
- full: '',
- },
+ event: {
+ created: new Date(0),
},
endpoint: {
- domain: '',
- is_base_image: true,
- active_directory_distinguished_name: '',
- active_directory_hostname: '',
- upgrade: {
- status: '',
- updated_at: new Date(),
- },
- isolation: {
- status: false,
- request_status: true,
- updated_at: new Date(),
- },
policy: {
- name: '',
id: '',
},
- sensor: {
- persistence: true,
- status: {},
+ },
+ agent: {
+ version: '',
+ id: '',
+ },
+ host: {
+ id: '',
+ hostname: '',
+ ip: [''],
+ mac: [''],
+ os: {
+ name: '',
+ full: '',
+ version: '',
},
},
};
};
const loadDataToStore = () => {
dispatch({
- type: 'serverReturnedEndpointList',
+ type: 'serverReturnedManagementList',
payload: {
endpoints: [generateEndpoint()],
request_page_size: 1,
- request_index: 1,
+ request_page_index: 1,
total: 10,
},
});
@@ -76,39 +64,40 @@ describe('endpoint_list store concerns', () => {
test('it creates default state', () => {
expect(store.getState()).toEqual({
endpoints: [],
- request_page_size: 10,
- request_index: 0,
+ pageSize: 10,
+ pageIndex: 0,
total: 0,
+ loading: false,
});
});
- test('it handles `serverReturnedEndpointList', () => {
+ test('it handles `serverReturnedManagementList', () => {
const payload = {
endpoints: [generateEndpoint()],
request_page_size: 1,
- request_index: 1,
+ request_page_index: 1,
total: 10,
};
dispatch({
- type: 'serverReturnedEndpointList',
+ type: 'serverReturnedManagementList',
payload,
});
const currentState = store.getState();
expect(currentState.endpoints).toEqual(payload.endpoints);
- expect(currentState.request_page_size).toEqual(payload.request_page_size);
- expect(currentState.request_index).toEqual(payload.request_index);
+ expect(currentState.pageSize).toEqual(payload.request_page_size);
+ expect(currentState.pageIndex).toEqual(payload.request_page_index);
expect(currentState.total).toEqual(payload.total);
});
- test('it handles `userExitedEndpointListPage`', () => {
+ test('it handles `userExitedManagementListPage`', () => {
loadDataToStore();
expect(store.getState().total).toEqual(10);
- dispatch({ type: 'userExitedEndpointListPage' });
+ dispatch({ type: 'userExitedManagementList' });
expect(store.getState().endpoints.length).toEqual(0);
- expect(store.getState().request_index).toEqual(0);
+ expect(store.getState().pageIndex).toEqual(0);
});
});
@@ -118,9 +107,9 @@ describe('endpoint_list store concerns', () => {
loadDataToStore();
});
- test('it selects `endpointListData`', () => {
+ test('it selects `managementListData`', () => {
const currentState = store.getState();
- expect(endpointListData(currentState)).toEqual(currentState.endpoints);
+ expect(listData(currentState)).toEqual(currentState.endpoints);
});
});
});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts
similarity index 60%
rename from x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts
rename to x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts
index 6ffcebc3f41aa..f0bfe27c9e30f 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts
@@ -4,6 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EndpointListState } from './types';
-
-export const endpointListData = (state: EndpointListState) => state.endpoints;
+export { managementListReducer } from './reducer';
+export { ManagementAction } from './action';
+export { managementMiddlewareFactory } from './middleware';
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts
new file mode 100644
index 0000000000000..095e49a6c4306
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts
@@ -0,0 +1,80 @@
+/*
+ * 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 { applyMiddleware, createStore, Dispatch, Store } from 'redux';
+import { coreMock } from '../../../../../../../../src/core/public/mocks';
+import { managementListReducer, managementMiddlewareFactory } from './index';
+import { EndpointMetadata, EndpointResultList } from '../../../../../common/types';
+import { ManagementListState } from '../../types';
+import { AppAction } from '../action';
+import { listData } from './selectors';
+describe('endpoint list saga', () => {
+ const sleep = (ms = 100) => new Promise(wakeup => setTimeout(wakeup, ms));
+ let fakeCoreStart: jest.Mocked;
+ let fakeHttpServices: jest.Mocked;
+ let store: Store;
+ let getState: typeof store['getState'];
+ let dispatch: Dispatch;
+ // https://github.com/elastic/endpoint-app-team/issues/131
+ const generateEndpoint = (): EndpointMetadata => {
+ return {
+ event: {
+ created: new Date(0),
+ },
+ endpoint: {
+ policy: {
+ id: '',
+ },
+ },
+ agent: {
+ version: '',
+ id: '',
+ },
+ host: {
+ id: '',
+ hostname: '',
+ ip: [''],
+ mac: [''],
+ os: {
+ name: '',
+ full: '',
+ version: '',
+ },
+ },
+ };
+ };
+ const getEndpointListApiResponse = (): EndpointResultList => {
+ return {
+ endpoints: [generateEndpoint()],
+ request_page_size: 1,
+ request_page_index: 1,
+ total: 10,
+ };
+ };
+ beforeEach(() => {
+ fakeCoreStart = coreMock.createStart({ basePath: '/mock' });
+ fakeHttpServices = fakeCoreStart.http as jest.Mocked;
+ store = createStore(
+ managementListReducer,
+ applyMiddleware(managementMiddlewareFactory(fakeCoreStart))
+ );
+ getState = store.getState;
+ dispatch = store.dispatch;
+ });
+ test('it handles `userNavigatedToPage`', async () => {
+ const apiResponse = getEndpointListApiResponse();
+ fakeHttpServices.post.mockResolvedValue(apiResponse);
+ expect(fakeHttpServices.post).not.toHaveBeenCalled();
+ dispatch({ type: 'userNavigatedToPage', payload: 'managementPage' });
+ await sleep();
+ expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/endpoints', {
+ body: JSON.stringify({
+ paging_properties: [{ page_index: 0 }, { page_size: 10 }],
+ }),
+ });
+ expect(listData(getState())).toEqual(apiResponse.endpoints);
+ });
+});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts
new file mode 100644
index 0000000000000..ae756caf5aa35
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts
@@ -0,0 +1,36 @@
+/*
+ * 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 { MiddlewareFactory } from '../../types';
+import { pageIndex, pageSize } from './selectors';
+import { ManagementListState } from '../../types';
+import { AppAction } from '../action';
+
+export const managementMiddlewareFactory: MiddlewareFactory = coreStart => {
+ return ({ getState, dispatch }) => next => async (action: AppAction) => {
+ next(action);
+ if (
+ (action.type === 'userNavigatedToPage' && action.payload === 'managementPage') ||
+ action.type === 'userPaginatedManagementList'
+ ) {
+ const managementPageIndex = pageIndex(getState());
+ const managementPageSize = pageSize(getState());
+ const response = await coreStart.http.post('/api/endpoint/endpoints', {
+ body: JSON.stringify({
+ paging_properties: [
+ { page_index: managementPageIndex },
+ { page_size: managementPageSize },
+ ],
+ }),
+ });
+ response.request_page_index = managementPageIndex;
+ dispatch({
+ type: 'serverReturnedManagementList',
+ payload: response,
+ });
+ }
+ };
+};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts
new file mode 100644
index 0000000000000..bbbbdc4d17ce6
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts
@@ -0,0 +1,55 @@
+/*
+ * 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 { Reducer } from 'redux';
+import { ManagementListState } from '../../types';
+import { AppAction } from '../action';
+
+const initialState = (): ManagementListState => {
+ return {
+ endpoints: [],
+ pageSize: 10,
+ pageIndex: 0,
+ total: 0,
+ loading: false,
+ };
+};
+
+export const managementListReducer: Reducer = (
+ state = initialState(),
+ action
+) => {
+ if (action.type === 'serverReturnedManagementList') {
+ const {
+ endpoints,
+ total,
+ request_page_size: pageSize,
+ request_page_index: pageIndex,
+ } = action.payload;
+ return {
+ ...state,
+ endpoints,
+ total,
+ pageSize,
+ pageIndex,
+ loading: false,
+ };
+ }
+
+ if (action.type === 'userExitedManagementList') {
+ return initialState();
+ }
+
+ if (action.type === 'userPaginatedManagementList') {
+ return {
+ ...state,
+ ...action.payload,
+ loading: true,
+ };
+ }
+
+ return state;
+};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts
new file mode 100644
index 0000000000000..3dcb144c2bade
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { ManagementListState } from '../../types';
+
+export const listData = (state: ManagementListState) => state.endpoints;
+
+export const pageIndex = (state: ManagementListState) => state.pageIndex;
+
+export const pageSize = (state: ManagementListState) => state.pageSize;
+
+export const totalHits = (state: ManagementListState) => state.total;
+
+export const isLoading = (state: ManagementListState) => state.loading;
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts
index a9cf6d9980519..7d738c266fae0 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts
@@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { combineReducers, Reducer } from 'redux';
-import { endpointListReducer } from './endpoint_list';
+import { managementListReducer } from './managing';
import { AppAction } from './action';
import { alertListReducer } from './alerts';
import { GlobalState } from '../types';
export const appReducer: Reducer = combineReducers({
- endpointList: endpointListReducer,
+ managementList: managementListReducer,
alertList: alertListReducer,
});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts
deleted file mode 100644
index 3b7de79d5443c..0000000000000
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts
+++ /dev/null
@@ -1,18 +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 { CoreStart } from 'kibana/public';
-import { createSagaMiddleware, SagaContext } from '../lib';
-import { endpointListSaga } from './endpoint_list';
-
-export const appSagaFactory = (coreStart: CoreStart) => {
- return createSagaMiddleware(async (sagaContext: SagaContext) => {
- await Promise.all([
- // Concerns specific sagas here
- endpointListSaga(sagaContext, coreStart),
- ]);
- });
-};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts
index 5f02d36308053..02a7793fc38b0 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts
@@ -6,20 +6,42 @@
import { Dispatch, MiddlewareAPI } from 'redux';
import { CoreStart } from 'kibana/public';
-import { EndpointListState } from './store/endpoint_list';
+import { EndpointMetadata } from '../../../common/types';
import { AppAction } from './store/action';
import { AlertResultList } from '../../../common/types';
-export type MiddlewareFactory = (
+export type MiddlewareFactory = (
coreStart: CoreStart
) => (
- api: MiddlewareAPI, GlobalState>
+ api: MiddlewareAPI, S>
) => (next: Dispatch) => (action: AppAction) => unknown;
+export interface ManagementListState {
+ endpoints: EndpointMetadata[];
+ total: number;
+ pageSize: number;
+ pageIndex: number;
+ loading: boolean;
+}
+
+export interface ManagementListPagination {
+ pageIndex: number;
+ pageSize: number;
+}
+
export interface GlobalState {
- readonly endpointList: EndpointListState;
+ readonly managementList: ManagementListState;
readonly alertList: AlertListState;
}
export type AlertListData = AlertResultList;
export type AlertListState = AlertResultList;
+export type CreateStructuredSelector = <
+ SelectorMap extends { [key: string]: (...args: never[]) => unknown }
+>(
+ selectorMap: SelectorMap
+) => (
+ state: SelectorMap[keyof SelectorMap] extends (state: infer State) => unknown ? State : never
+) => {
+ [Key in keyof SelectorMap]: ReturnType;
+};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/hooks.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/hooks.ts
new file mode 100644
index 0000000000000..a0720fbd8aeeb
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/hooks.ts
@@ -0,0 +1,16 @@
+/*
+ * 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, ManagementListState } from '../../types';
+
+export function useManagementListSelector(
+ selector: (state: ManagementListState) => TSelected
+) {
+ return useSelector(function(state: GlobalState) {
+ return selector(state.managementList);
+ });
+}
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx
new file mode 100644
index 0000000000000..44b08f25c7653
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx
@@ -0,0 +1,167 @@
+/*
+ * 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 React, { useMemo, useCallback } from 'react';
+import { useDispatch } from 'react-redux';
+import {
+ EuiPage,
+ EuiPageBody,
+ EuiPageContent,
+ EuiPageContentBody,
+ EuiPageContentHeader,
+ EuiPageContentHeaderSection,
+ EuiTitle,
+ EuiBasicTable,
+ EuiTextColor,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { createStructuredSelector } from 'reselect';
+import * as selectors from '../../store/managing/selectors';
+import { ManagementAction } from '../../store/managing/action';
+import { useManagementListSelector } from './hooks';
+import { usePageId } from '../use_page_id';
+import { CreateStructuredSelector } from '../../types';
+
+const selector = (createStructuredSelector as CreateStructuredSelector)(selectors);
+export const ManagementList = () => {
+ usePageId('managementPage');
+ const dispatch = useDispatch<(a: ManagementAction) => void>();
+ const {
+ listData,
+ pageIndex,
+ pageSize,
+ totalHits: totalItemCount,
+ isLoading,
+ } = useManagementListSelector(selector);
+
+ const paginationSetup = useMemo(() => {
+ return {
+ pageIndex,
+ pageSize,
+ totalItemCount,
+ pageSizeOptions: [10, 20, 50],
+ hidePerPageOptions: false,
+ };
+ }, [pageIndex, pageSize, totalItemCount]);
+
+ const onTableChange = useCallback(
+ ({ page }: { page: { index: number; size: number } }) => {
+ const { index, size } = page;
+ dispatch({
+ type: 'userPaginatedManagementList',
+ payload: { pageIndex: index, pageSize: size },
+ });
+ },
+ [dispatch]
+ );
+
+ const columns = [
+ {
+ field: 'host.hostname',
+ name: i18n.translate('xpack.endpoint.management.list.host', {
+ defaultMessage: 'Hostname',
+ }),
+ },
+ {
+ field: '',
+ name: i18n.translate('xpack.endpoint.management.list.policy', {
+ defaultMessage: 'Policy',
+ }),
+ render: () => {
+ return 'Policy Name';
+ },
+ },
+ {
+ field: '',
+ name: i18n.translate('xpack.endpoint.management.list.policyStatus', {
+ defaultMessage: 'Policy Status',
+ }),
+ render: () => {
+ return 'Policy Status';
+ },
+ },
+ {
+ field: '',
+ name: i18n.translate('xpack.endpoint.management.list.alerts', {
+ defaultMessage: 'Alerts',
+ }),
+ render: () => {
+ return '0';
+ },
+ },
+ {
+ field: 'host.os.name',
+ name: i18n.translate('xpack.endpoint.management.list.os', {
+ defaultMessage: 'Operating System',
+ }),
+ },
+ {
+ field: 'host.ip',
+ name: i18n.translate('xpack.endpoint.management.list.ip', {
+ defaultMessage: 'IP Address',
+ }),
+ },
+ {
+ field: '',
+ name: i18n.translate('xpack.endpoint.management.list.sensorVersion', {
+ defaultMessage: 'Sensor Version',
+ }),
+ render: () => {
+ return 'version';
+ },
+ },
+ {
+ field: '',
+ name: i18n.translate('xpack.endpoint.management.list.lastActive', {
+ defaultMessage: 'Last Active',
+ }),
+ render: () => {
+ return 'xxxx';
+ },
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts
index d8eb969b99b3b..bda336e73c4f8 100644
--- a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts
+++ b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts
@@ -47,7 +47,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
ensureCurrentUrl: false,
shouldLoginIfPrompted: false,
});
- await testSubjects.existOrFail('endpointManagement');
+ await testSubjects.existOrFail('managementViewTitle');
});
});
diff --git a/x-pack/test/functional/apps/endpoint/index.ts b/x-pack/test/functional/apps/endpoint/index.ts
index e44a4cb846f2c..5fdf54b98cda6 100644
--- a/x-pack/test/functional/apps/endpoint/index.ts
+++ b/x-pack/test/functional/apps/endpoint/index.ts
@@ -11,5 +11,6 @@ export default function({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./feature_controls'));
loadTestFile(require.resolve('./landing_page'));
+ loadTestFile(require.resolve('./management'));
});
}
diff --git a/x-pack/test/functional/apps/endpoint/management.ts b/x-pack/test/functional/apps/endpoint/management.ts
new file mode 100644
index 0000000000000..bac87f34ceb82
--- /dev/null
+++ b/x-pack/test/functional/apps/endpoint/management.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default ({ getPageObjects, getService }: FtrProviderContext) => {
+ const pageObjects = getPageObjects(['common', 'endpoint']);
+ const esArchiver = getService('esArchiver');
+ const testSubjects = getService('testSubjects');
+
+ describe('Endpoint Management List', function() {
+ this.tags('ciGroup7');
+ before(async () => {
+ await esArchiver.load('endpoint/endpoints/api_feature');
+ await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/management');
+ });
+
+ it('finds title', async () => {
+ const title = await testSubjects.getVisibleText('managementViewTitle');
+ expect(title).to.equal('Hosts');
+ });
+
+ it('displays table data', async () => {
+ const data = await pageObjects.endpoint.getManagementTableData();
+ [
+ 'Hostnamecadmann-4.example.com',
+ 'PolicyPolicy Name',
+ 'Policy StatusPolicy Status',
+ 'Alerts0',
+ 'Operating Systemwindows 10.0',
+ 'IP Address10.192.213.130, 10.70.28.129',
+ 'Sensor Versionversion',
+ 'Last Activexxxx',
+ ].forEach((cellValue, index) => {
+ expect(data[1][index]).to.equal(cellValue);
+ });
+ });
+
+ after(async () => {
+ await esArchiver.unload('endpoint/endpoints/api_feature');
+ });
+ });
+};
diff --git a/x-pack/test/functional/page_objects/endpoint_page.ts b/x-pack/test/functional/page_objects/endpoint_page.ts
index f02a899f6d37d..a306a855a83eb 100644
--- a/x-pack/test/functional/page_objects/endpoint_page.ts
+++ b/x-pack/test/functional/page_objects/endpoint_page.ts
@@ -8,10 +8,15 @@ import { FtrProviderContext } from '../ftr_provider_context';
export function EndpointPageProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
+ const table = getService('table');
return {
async welcomeEndpointTitle() {
return await testSubjects.getVisibleText('welcomeTitle');
},
+
+ async getManagementTableData() {
+ return await table.getDataFromTestSubj('managementListTable');
+ },
};
}