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