diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 82c95b37ee7b..d69e068bdea3 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -9,6 +9,9 @@ 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 { Store } from 'redux'; +import { appStoreFactory } from './store'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. @@ -16,7 +19,9 @@ import { Route, BrowserRouter, Switch } from 'react-router-dom'; export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMountParameters) { coreStart.http.get('/api/endpoint/hello-world'); - ReactDOM.render(, element); + const store = appStoreFactory(coreStart); + + ReactDOM.render(, element); return () => { ReactDOM.unmountComponentAtNode(element); @@ -25,38 +30,46 @@ export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMou interface RouterProps { basename: string; + store: Store; } -const AppRoot: React.FunctionComponent = React.memo(({ basename }) => ( - - - - ( -

- -

- )} - /> - ( -

- -

- )} - /> - ( - - )} - /> -
-
-
+const AppRoot: React.FunctionComponent = React.memo(({ basename, store }) => ( + + + + + ( +

+ +

+ )} + /> + { + // 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/lib/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/index.ts new file mode 100644 index 000000000000..ba2e1ce8f9fe --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/index.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './saga'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts new file mode 100644 index 000000000000..0387eac0e7c7 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { createSagaMiddleware, SagaContext } from './index'; +import { applyMiddleware, createStore, Reducer } from 'redux'; + +describe('saga', () => { + const INCREMENT_COUNTER = 'INCREMENT'; + const DELAYED_INCREMENT_COUNTER = 'DELAYED INCREMENT COUNTER'; + const STOP_SAGA_PROCESSING = 'BREAK ASYNC ITERATOR'; + + const sleep = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms)); + let reducerA: Reducer; + let sideAffect: (a: unknown, s: unknown) => void; + let sagaExe: (sagaContext: SagaContext) => Promise; + + beforeEach(() => { + reducerA = jest.fn((prevState = { count: 0 }, { type }) => { + switch (type) { + case INCREMENT_COUNTER: + return { ...prevState, count: prevState.count + 1 }; + default: + return prevState; + } + }); + + sideAffect = jest.fn(); + + sagaExe = jest.fn(async ({ actionsAndState, dispatch }: SagaContext) => { + for await (const { action, state } of actionsAndState()) { + expect(action).toBeDefined(); + expect(state).toBeDefined(); + + if (action.type === STOP_SAGA_PROCESSING) { + break; + } + + sideAffect(action, state); + + if (action.type === DELAYED_INCREMENT_COUNTER) { + await sleep(1); + dispatch({ + type: INCREMENT_COUNTER, + }); + } + } + }); + }); + + test('it returns Redux Middleware from createSagaMiddleware()', () => { + const sagaMiddleware = createSagaMiddleware(async () => {}); + expect(sagaMiddleware).toBeInstanceOf(Function); + }); + test('it does nothing if saga is not started', () => { + const store = createStore(reducerA, applyMiddleware(createSagaMiddleware(sagaExe))); + expect(store.getState().count).toEqual(0); + expect(reducerA).toHaveBeenCalled(); + expect(sagaExe).toHaveBeenCalled(); + expect(sideAffect).not.toHaveBeenCalled(); + expect(store.getState()).toEqual({ count: 0 }); + }); + test('it updates store once running', async () => { + const sagaMiddleware = createSagaMiddleware(sagaExe); + const store = createStore(reducerA, applyMiddleware(sagaMiddleware)); + + expect(store.getState()).toEqual({ count: 0 }); + expect(sagaExe).toHaveBeenCalled(); + + store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); + expect(store.getState()).toEqual({ count: 0 }); + + await sleep(100); + + expect(sideAffect).toHaveBeenCalled(); + expect(store.getState()).toEqual({ count: 1 }); + }); + test('it stops processing if break out of loop', async () => { + const sagaMiddleware = createSagaMiddleware(sagaExe); + const store = createStore(reducerA, applyMiddleware(sagaMiddleware)); + + store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); + await sleep(100); + + expect(store.getState()).toEqual({ count: 1 }); + expect(sideAffect).toHaveBeenCalledTimes(2); + + store.dispatch({ type: STOP_SAGA_PROCESSING }); + await sleep(100); + + expect(store.getState()).toEqual({ count: 1 }); + expect(sideAffect).toHaveBeenCalledTimes(2); + + store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); + await sleep(100); + + expect(store.getState()).toEqual({ count: 1 }); + expect(sideAffect).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts new file mode 100644 index 000000000000..b93360ec6b5a --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts @@ -0,0 +1,129 @@ +/* + * 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 { AnyAction, Dispatch, Middleware, MiddlewareAPI } from 'redux'; +import { GlobalState } from '../store'; + +interface QueuedAction { + /** + * The Redux action that was dispatched + */ + action: TAction; + /** + * The Global state at the time the action was dispatched + */ + state: GlobalState; +} + +interface IteratorInstance { + queue: QueuedAction[]; + nextResolve: null | ((inst: QueuedAction) => void); +} + +type Saga = (storeContext: SagaContext) => Promise; + +type StoreActionsAndState = AsyncIterableIterator>; + +export interface SagaContext { + /** + * A generator function that will `yield` `Promise`s that resolve with a `QueuedAction` + */ + actionsAndState: () => StoreActionsAndState; + dispatch: Dispatch; +} + +const noop = () => {}; + +/** + * Creates Saga Middleware for use with Redux. + * + * @param {Saga} saga The `saga` should initialize a long-running `for await...of` loop against + * the return value of the `actionsAndState()` method provided by the `SagaContext`. + * + * @return {Middleware} + * + * @example + * + * type TPossibleActions = { type: 'add', payload: any[] }; + * //... + * const endpointsSaga = async ({ actionsAndState, dispatch }: SagaContext) => { + * for await (const { action, state } of actionsAndState()) { + * if (action.type === "userRequestedResource") { + * const resourceData = await doApiFetch('of/some/resource'); + * dispatch({ + * type: 'add', + * payload: [ resourceData ] + * }); + * } + * } + * } + * const endpointsSagaMiddleware = createSagaMiddleware(endpointsSaga); + * //.... + * const store = createStore(reducers, [ endpointsSagaMiddleware ]); + */ +export function createSagaMiddleware(saga: Saga): Middleware { + const iteratorInstances = new Set(); + let runSaga: () => void = noop; + + async function* getActionsAndStateIterator(): StoreActionsAndState { + const instance: IteratorInstance = { queue: [], nextResolve: null }; + iteratorInstances.add(instance); + try { + while (true) { + yield await nextActionAndState(); + } + } finally { + // If the consumer stops consuming this (e.g. `break` or `return` is called in the `for await` + // then this `finally` block will run and unregister this instance and reset `runSaga` + iteratorInstances.delete(instance); + runSaga = noop; + } + + function nextActionAndState() { + if (instance.queue.length) { + return Promise.resolve(instance.queue.shift() as QueuedAction); + } else { + return new Promise(function(resolve) { + instance.nextResolve = resolve; + }); + } + } + } + + function enqueue(value: QueuedAction) { + for (const iteratorInstance of iteratorInstances) { + iteratorInstance.queue.push(value); + if (iteratorInstance.nextResolve !== null) { + iteratorInstance.nextResolve(iteratorInstance.queue.shift() as QueuedAction); + iteratorInstance.nextResolve = null; + } + } + } + + function middleware({ getState, dispatch }: MiddlewareAPI) { + if (runSaga === noop) { + runSaga = saga.bind>(null, { + actionsAndState: getActionsAndStateIterator, + dispatch, + }); + runSaga(); + } + return (next: Dispatch) => (action: AnyAction) => { + // Call the next dispatch method in the middleware chain. + const returnValue = next(action); + + enqueue({ + action, + state: getState(), + }); + + // This will likely be the action itself, unless a middleware further in chain changed it. + return returnValue; + }; + } + + return middleware; +} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/actions.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/actions.ts new file mode 100644 index 000000000000..796dabce1d76 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/actions.ts @@ -0,0 +1,9 @@ +/* + * 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 { EndpointListAction } from './endpoint_list'; + +export type AppAction = EndpointListAction; 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 new file mode 100644 index 000000000000..02ec0f9d0903 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/action.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 { 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.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.test.ts new file mode 100644 index 000000000000..a46653f82ee4 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.test.ts @@ -0,0 +1,126 @@ +/* + * 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 { createStore, Dispatch, Store } from 'redux'; +import { EndpointListAction, EndpointData, endpointListReducer, EndpointListState } from './index'; +import { endpointListData } from './selectors'; + +describe('endpoint_list store concerns', () => { + let store: Store; + let dispatch: Dispatch; + const createTestStore = () => { + store = createStore(endpointListReducer); + dispatch = store.dispatch; + }; + 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 loadDataToStore = () => { + dispatch({ + type: 'serverReturnedEndpointList', + payload: { + endpoints: [generateEndpoint()], + request_page_size: 1, + request_index: 1, + total: 10, + }, + }); + }; + + describe('# Reducers', () => { + beforeEach(() => { + createTestStore(); + }); + + test('it creates default state', () => { + expect(store.getState()).toEqual({ + endpoints: [], + request_page_size: 10, + request_index: 0, + total: 0, + }); + }); + + test('it handles `serverReturnedEndpointList', () => { + const payload = { + endpoints: [generateEndpoint()], + request_page_size: 1, + request_index: 1, + total: 10, + }; + dispatch({ + type: 'serverReturnedEndpointList', + 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.total).toEqual(payload.total); + }); + + test('it handles `userExitedEndpointListPage`', () => { + loadDataToStore(); + + expect(store.getState().total).toEqual(10); + + dispatch({ type: 'userExitedEndpointListPage' }); + expect(store.getState().endpoints.length).toEqual(0); + expect(store.getState().request_index).toEqual(0); + }); + }); + + describe('# Selectors', () => { + beforeEach(() => { + createTestStore(); + loadDataToStore(); + }); + + test('it selects `endpointListData`', () => { + const currentState = store.getState(); + expect(endpointListData(currentState)).toEqual(currentState.endpoints); + }); + }); +}); 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 new file mode 100644 index 000000000000..bdf0708457bb --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.ts @@ -0,0 +1,10 @@ +/* + * 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 new file mode 100644 index 000000000000..9813777c988e --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/reducer.ts @@ -0,0 +1,32 @@ +/* + * 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 { EndpointListState } from './types'; +import { EndpointListAction } from './action'; + +const initialState = (): EndpointListState => { + return { + endpoints: [], + request_page_size: 10, + request_index: 0, + total: 0, + }; +}; + +export const endpointListReducer = (state = initialState(), action: EndpointListAction) => { + 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 new file mode 100644 index 000000000000..92bf3b7fd92d --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts @@ -0,0 +1,111 @@ +/* + * 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; + + // 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; + store = createStore( + endpointListReducer, + applyMiddleware(createSagaMiddleware(endpointListSagaFactory())) + ); + dispatch = store.dispatch; + }); + + 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 new file mode 100644 index 000000000000..cc156cfa8800 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.ts @@ -0,0 +1,26 @@ +/* + * 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/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts new file mode 100644 index 000000000000..6ffcebc3f41a --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts @@ -0,0 +1,9 @@ +/* + * 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 { EndpointListState } from './types'; + +export const endpointListData = (state: EndpointListState) => state.endpoints; 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 new file mode 100644 index 000000000000..f2810dd89f85 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts @@ -0,0 +1,54 @@ +/* + * 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 new file mode 100644 index 000000000000..d0dc002031ce --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { createStore, compose, applyMiddleware } from 'redux'; +import { CoreStart } from 'kibana/public'; +import { appSagaFactory } from './saga'; +import { appReducer } from './reducer'; + +export { GlobalState } from './reducer'; + +const composeWithReduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ + ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'EndpointApp' }) + : compose; + +export const appStoreFactory = (coreStart: CoreStart) => { + const store = createStore( + appReducer, + composeWithReduxDevTools(applyMiddleware(appSagaFactory(coreStart))) + ); + return store; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts new file mode 100644 index 000000000000..59ca4de91ac8 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.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 { combineReducers, Reducer } from 'redux'; +import { endpointListReducer, EndpointListState } from './endpoint_list'; +import { AppAction } from './actions'; + +export interface GlobalState { + endpointList: EndpointListState; +} + +export const appReducer: Reducer = combineReducers({ + endpointList: endpointListReducer, +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts new file mode 100644 index 000000000000..3b7de79d5443 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts @@ -0,0 +1,18 @@ +/* + * 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), + ]); + }); +};