From b9d2bb28cd3e5506621b25bcdd3fdf4f2ee4a34c Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 2 Jan 2020 12:51:44 -0500 Subject: [PATCH 01/19] Added saga library --- x-pack/plugins/endpoint/public/lib/index.ts | 7 + .../plugins/endpoint/public/lib/saga.test.ts | 101 ++++++++++++++ x-pack/plugins/endpoint/public/lib/saga.ts | 123 ++++++++++++++++++ x-pack/plugins/endpoint/public/types.ts | 13 ++ 4 files changed, 244 insertions(+) create mode 100644 x-pack/plugins/endpoint/public/lib/index.ts create mode 100644 x-pack/plugins/endpoint/public/lib/saga.test.ts create mode 100644 x-pack/plugins/endpoint/public/lib/saga.ts create mode 100644 x-pack/plugins/endpoint/public/types.ts diff --git a/x-pack/plugins/endpoint/public/lib/index.ts b/x-pack/plugins/endpoint/public/lib/index.ts new file mode 100644 index 0000000000000..ba2e1ce8f9fe6 --- /dev/null +++ b/x-pack/plugins/endpoint/public/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/lib/saga.test.ts b/x-pack/plugins/endpoint/public/lib/saga.test.ts new file mode 100644 index 0000000000000..0387eac0e7c7f --- /dev/null +++ b/x-pack/plugins/endpoint/public/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/lib/saga.ts b/x-pack/plugins/endpoint/public/lib/saga.ts new file mode 100644 index 0000000000000..af1722ae988aa --- /dev/null +++ b/x-pack/plugins/endpoint/public/lib/saga.ts @@ -0,0 +1,123 @@ +/* + * 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 '../types'; + +interface StoreAction extends AnyAction { + payload: unknown[]; + type: string; +} + +interface QueuedAction { + action: StoreAction; + state: GlobalState; +} + +interface IteratorInstance { + queue: QueuedAction[]; + nextResolve: null | ((inst: QueuedAction) => void); +} + +type Saga = (storeContext: SagaContext) => Promise; + +type StoreActionsAndState = AsyncIterableIterator; + +export interface SagaContext { + 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 + * + * 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: 'serverReturnedUserRequestedResource', + * 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: StoreAction) => { + // 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/types.ts b/x-pack/plugins/endpoint/public/types.ts new file mode 100644 index 0000000000000..8182b76cd3f87 --- /dev/null +++ b/x-pack/plugins/endpoint/public/types.ts @@ -0,0 +1,13 @@ +/* + * 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 reducers from './reducers'; + +// export type GlobalState = ReturnType; + +export interface GlobalState { + [key: string]: any; +} // TODO: replace once `reducers` are in place From 4aa7cd11bd03c48c90664a7a94620d155984db91 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 2 Jan 2020 14:19:24 -0500 Subject: [PATCH 02/19] Added action creator helper --- .../public/lib/action_creator.test.ts | 50 ++++++++++++++++ .../endpoint/public/lib/action_creator.ts | 57 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 x-pack/plugins/endpoint/public/lib/action_creator.test.ts create mode 100644 x-pack/plugins/endpoint/public/lib/action_creator.ts diff --git a/x-pack/plugins/endpoint/public/lib/action_creator.test.ts b/x-pack/plugins/endpoint/public/lib/action_creator.test.ts new file mode 100644 index 0000000000000..054ed53873ac5 --- /dev/null +++ b/x-pack/plugins/endpoint/public/lib/action_creator.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { createActions, createActionFactory } from './action_creator'; + +describe('action creator', () => { + describe('#createActions', () => { + test('it creates multiple action creators from Array', () => { + const actions = createActions(['actionOne', 'actionTwo']); + expect(actions).toEqual({ + actionOne: expect.any(Function), + actionTwo: expect.any(Function), + }); + expect(actions.actionOne.toString()).toEqual('actionOne'); + expect(actions.actionOne.type).toEqual('actionOne'); + }); + test('it creates multiple action creators from Set', () => { + const actions = createActions(new Set(['actionOne', 'actionTwo'])); + expect(actions).toEqual({ + actionOne: expect.any(Function), + actionTwo: expect.any(Function), + }); + expect(actions.actionOne.toString()).toEqual('actionOne'); + expect(actions.actionOne.type).toEqual('actionOne'); + }); + }); + + describe('#actionCreatorFactory', () => { + type PayloadWithOneArg = [{ data: { [key: string]: any } }]; + type PayloadWithTwoArgs = [{ data: { [key: string]: any } }, Set]; + + test('action creator returns expected object', () => { + const getAction = createActionFactory<'actionOne', PayloadWithOneArg>('actionOne'); + expect(getAction({ data: { one: 1, two: 2 } })).toEqual({ + type: 'actionOne', + payload: [{ data: { one: 1, two: 2 } }], + }); + + const getActionWithMultiPayload = createActionFactory<'actionOne', PayloadWithTwoArgs>( + 'actionOne' + ); + expect(getActionWithMultiPayload({ data: { one: 1, two: 2 } }, new Set(['hello']))).toEqual({ + type: 'actionOne', + payload: [{ data: { one: 1, two: 2 } }, new Set(['hello'])], + }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/lib/action_creator.ts b/x-pack/plugins/endpoint/public/lib/action_creator.ts new file mode 100644 index 0000000000000..1555f39fe3469 --- /dev/null +++ b/x-pack/plugins/endpoint/public/lib/action_creator.ts @@ -0,0 +1,57 @@ +/* + * 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. + */ + +type PayloadType = + | [] + | [unknown] + | [unknown, unknown] + | [unknown, unknown, unknown] + | [unknown, unknown, unknown, unknown] + | [unknown, unknown, unknown, unknown, unknown]; + +/** + * Returns a function that will produce a Redux action when called + * + * @param {String} type + */ +export function createActionFactory(type: Type) { + interface ActionCreator { + (...args: Payload): { payload: Payload; type: Type }; + type: Type; + } + + const actionHandler: ActionCreator = (...args: Payload) => { + const action = { + type, + // All of the actions specified in this project use a standard format where `payload` + // is an array of the arguments passed to the action creator + payload: args, + }; + + return action; + }; + actionHandler.toString = () => type; + actionHandler.type = type; + return actionHandler; +} + +/** + * Create a set (`Object`) of Actions based on a list of action types + * + * @param {Iterable} actionTypes + */ +export function createActions(actionTypes: Iterable) { + // FIXME: anyway to infer `actionName` below as one of the values in `actionTypes` input param? + const actionCreators: { + [actionType: string]: ReturnType; + } = {}; + + for (const type of actionTypes) { + actionCreators[type] = createActionFactory(type); + } + + return actionCreators; +} From 460efb1e6a61e2489e7ccb4297d787ee0661d090 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 2 Jan 2020 15:33:30 -0500 Subject: [PATCH 03/19] Added action creator exports to `/lib/index` --- x-pack/plugins/endpoint/public/lib/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/endpoint/public/lib/index.ts b/x-pack/plugins/endpoint/public/lib/index.ts index ba2e1ce8f9fe6..2ae9384f67f9e 100644 --- a/x-pack/plugins/endpoint/public/lib/index.ts +++ b/x-pack/plugins/endpoint/public/lib/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './action_creator'; export * from './saga'; From bbafe280a0e0ef6bef48e26e92bb75f072cf5cfe Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Fri, 3 Jan 2020 09:41:07 -0500 Subject: [PATCH 04/19] Moved files under `public/applications/endpoint` --- .../{ => applications/endpoint}/lib/action_creator.test.ts | 0 .../public/{ => applications/endpoint}/lib/action_creator.ts | 0 .../endpoint/public/{ => applications/endpoint}/lib/index.ts | 0 .../public/{ => applications/endpoint}/lib/saga.test.ts | 0 .../endpoint/public/{ => applications/endpoint}/lib/saga.ts | 3 +++ .../endpoint/public/{ => applications/endpoint}/types.ts | 0 6 files changed, 3 insertions(+) rename x-pack/plugins/endpoint/public/{ => applications/endpoint}/lib/action_creator.test.ts (100%) rename x-pack/plugins/endpoint/public/{ => applications/endpoint}/lib/action_creator.ts (100%) rename x-pack/plugins/endpoint/public/{ => applications/endpoint}/lib/index.ts (100%) rename x-pack/plugins/endpoint/public/{ => applications/endpoint}/lib/saga.test.ts (100%) rename x-pack/plugins/endpoint/public/{ => applications/endpoint}/lib/saga.ts (97%) rename x-pack/plugins/endpoint/public/{ => applications/endpoint}/types.ts (100%) diff --git a/x-pack/plugins/endpoint/public/lib/action_creator.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/action_creator.test.ts similarity index 100% rename from x-pack/plugins/endpoint/public/lib/action_creator.test.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/lib/action_creator.test.ts diff --git a/x-pack/plugins/endpoint/public/lib/action_creator.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/action_creator.ts similarity index 100% rename from x-pack/plugins/endpoint/public/lib/action_creator.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/lib/action_creator.ts diff --git a/x-pack/plugins/endpoint/public/lib/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/index.ts similarity index 100% rename from x-pack/plugins/endpoint/public/lib/index.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/lib/index.ts diff --git a/x-pack/plugins/endpoint/public/lib/saga.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts similarity index 100% rename from x-pack/plugins/endpoint/public/lib/saga.test.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts diff --git a/x-pack/plugins/endpoint/public/lib/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts similarity index 97% rename from x-pack/plugins/endpoint/public/lib/saga.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts index af1722ae988aa..9a3e38bbf1475 100644 --- a/x-pack/plugins/endpoint/public/lib/saga.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts @@ -27,6 +27,9 @@ type Saga = (storeContext: SagaContext) => Promise; type StoreActionsAndState = AsyncIterableIterator; export interface SagaContext { + /** + * A generator function that will `yield` a `Promise` that resolves with a `QueuedAction` + */ actionsAndState: () => StoreActionsAndState; dispatch: Dispatch; } diff --git a/x-pack/plugins/endpoint/public/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts similarity index 100% rename from x-pack/plugins/endpoint/public/types.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/types.ts From a73c2d830891cb442bfeafbd1ed88429d532a311 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Fri, 3 Jan 2020 12:42:38 -0500 Subject: [PATCH 05/19] Deleted `action_creator` --- .../endpoint/lib/action_creator.test.ts | 50 ---------------- .../endpoint/lib/action_creator.ts | 57 ------------------- .../public/applications/endpoint/lib/index.ts | 1 - .../public/applications/endpoint/lib/saga.ts | 2 +- 4 files changed, 1 insertion(+), 109 deletions(-) delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/lib/action_creator.test.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/lib/action_creator.ts diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/action_creator.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/action_creator.test.ts deleted file mode 100644 index 054ed53873ac5..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/action_creator.test.ts +++ /dev/null @@ -1,50 +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 { createActions, createActionFactory } from './action_creator'; - -describe('action creator', () => { - describe('#createActions', () => { - test('it creates multiple action creators from Array', () => { - const actions = createActions(['actionOne', 'actionTwo']); - expect(actions).toEqual({ - actionOne: expect.any(Function), - actionTwo: expect.any(Function), - }); - expect(actions.actionOne.toString()).toEqual('actionOne'); - expect(actions.actionOne.type).toEqual('actionOne'); - }); - test('it creates multiple action creators from Set', () => { - const actions = createActions(new Set(['actionOne', 'actionTwo'])); - expect(actions).toEqual({ - actionOne: expect.any(Function), - actionTwo: expect.any(Function), - }); - expect(actions.actionOne.toString()).toEqual('actionOne'); - expect(actions.actionOne.type).toEqual('actionOne'); - }); - }); - - describe('#actionCreatorFactory', () => { - type PayloadWithOneArg = [{ data: { [key: string]: any } }]; - type PayloadWithTwoArgs = [{ data: { [key: string]: any } }, Set]; - - test('action creator returns expected object', () => { - const getAction = createActionFactory<'actionOne', PayloadWithOneArg>('actionOne'); - expect(getAction({ data: { one: 1, two: 2 } })).toEqual({ - type: 'actionOne', - payload: [{ data: { one: 1, two: 2 } }], - }); - - const getActionWithMultiPayload = createActionFactory<'actionOne', PayloadWithTwoArgs>( - 'actionOne' - ); - expect(getActionWithMultiPayload({ data: { one: 1, two: 2 } }, new Set(['hello']))).toEqual({ - type: 'actionOne', - payload: [{ data: { one: 1, two: 2 } }, new Set(['hello'])], - }); - }); - }); -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/action_creator.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/action_creator.ts deleted file mode 100644 index 1555f39fe3469..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/action_creator.ts +++ /dev/null @@ -1,57 +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. - */ - -type PayloadType = - | [] - | [unknown] - | [unknown, unknown] - | [unknown, unknown, unknown] - | [unknown, unknown, unknown, unknown] - | [unknown, unknown, unknown, unknown, unknown]; - -/** - * Returns a function that will produce a Redux action when called - * - * @param {String} type - */ -export function createActionFactory(type: Type) { - interface ActionCreator { - (...args: Payload): { payload: Payload; type: Type }; - type: Type; - } - - const actionHandler: ActionCreator = (...args: Payload) => { - const action = { - type, - // All of the actions specified in this project use a standard format where `payload` - // is an array of the arguments passed to the action creator - payload: args, - }; - - return action; - }; - actionHandler.toString = () => type; - actionHandler.type = type; - return actionHandler; -} - -/** - * Create a set (`Object`) of Actions based on a list of action types - * - * @param {Iterable} actionTypes - */ -export function createActions(actionTypes: Iterable) { - // FIXME: anyway to infer `actionName` below as one of the values in `actionTypes` input param? - const actionCreators: { - [actionType: string]: ReturnType; - } = {}; - - for (const type of actionTypes) { - actionCreators[type] = createActionFactory(type); - } - - return actionCreators; -} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/index.ts index 2ae9384f67f9e..ba2e1ce8f9fe6 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './action_creator'; export * from './saga'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts index 9a3e38bbf1475..296805435a29c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts @@ -28,7 +28,7 @@ type StoreActionsAndState = AsyncIterableIterator; export interface SagaContext { /** - * A generator function that will `yield` a `Promise` that resolves with a `QueuedAction` + * A generator function that will `yield` `Promise`s that resolve with a `QueuedAction` */ actionsAndState: () => StoreActionsAndState; dispatch: Dispatch; From ade91f3e94bbb32a859c026abddc2fec3b78d4c3 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Fri, 3 Jan 2020 14:39:24 -0500 Subject: [PATCH 06/19] Initialize endpoint app redux store - Initializes the global app store - adds actions + reducers for `app` that store the Kibana `coreStart` services along with the `appBasePath` in the store - delete the `types.ts` file - GlobalState type moved to `store/index` --- .../public/applications/endpoint/index.tsx | 14 ++++++++ .../public/applications/endpoint/lib/saga.ts | 2 +- .../endpoint/store/app/actions.ts | 27 +++++++++++++++ .../endpoint/store/app/reducers.ts | 34 +++++++++++++++++++ .../applications/endpoint/store/index.ts | 16 +++++++++ .../endpoint/{types.ts => store/reducers.ts} | 11 +++--- 6 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/app/actions.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/app/reducers.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts rename x-pack/plugins/endpoint/public/applications/endpoint/{types.ts => store/reducers.ts} (55%) diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 82c95b37ee7b0..847a98f6f80ee 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -9,6 +9,8 @@ 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 { appStoreFactory } from './store'; +import { AppDispatch } from './store/app/actions'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. @@ -16,10 +18,22 @@ import { Route, BrowserRouter, Switch } from 'react-router-dom'; export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMountParameters) { coreStart.http.get('/api/endpoint/hello-world'); + const store = appStoreFactory(); + const dispatch: AppDispatch = store.dispatch; + + dispatch({ + type: 'appWillMount', + payload: { + coreStartServices: coreStart, + appBasePath, + }, + }); + ReactDOM.render(, element); return () => { ReactDOM.unmountComponentAtNode(element); + dispatch({ type: 'appDidUnmount' }); }; } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts index 296805435a29c..74fb284ef7020 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts @@ -5,7 +5,7 @@ */ import { AnyAction, Dispatch, Middleware, MiddlewareAPI } from 'redux'; -import { GlobalState } from '../types'; +import { GlobalState } from '../store'; interface StoreAction extends AnyAction { payload: unknown[]; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/app/actions.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/app/actions.ts new file mode 100644 index 0000000000000..7f4b249191a27 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/app/actions.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 { CoreStart } from 'kibana/public'; + +interface AppWillMount { + readonly type: 'appWillMount'; + payload: { + coreStartServices: CoreStart; + appBasePath: string; + }; +} + +/** + * Endpoint App has been un-mounted from DOM. Use this opportunity to remove any + * global event listeners, notify server-side services or other type of cleanup + */ +interface AppDidUnmount { + readonly type: 'appDidUnmount'; +} + +export type AppAction = AppWillMount | AppDidUnmount; + +export type AppDispatch = (action: AppAction) => AppAction; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/app/reducers.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/app/reducers.ts new file mode 100644 index 0000000000000..52fd0ff84f1c6 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/app/reducers.ts @@ -0,0 +1,34 @@ +/* + * 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 { AppAction } from './actions'; + +interface AppState { + coreStartServices: null | CoreStart; + appBasePath: string; +} + +const createAppState = (): AppState => { + return { + appBasePath: '', + coreStartServices: null, + }; +}; + +export const appReducer = (state: AppState = createAppState(), action: AppAction) => { + switch (action.type) { + case 'appWillMount': + const { coreStartServices, appBasePath } = action.payload; + return { ...state, coreStartServices, appBasePath }; + + case 'appDidUnmount': + return createAppState(); + + default: + return state; + } +}; 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 0000000000000..38d2356871f8d --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.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 { createStore } from 'redux'; +import { endpointAppReducers } from './reducers'; + +// FIXME: typing below is not correct - shows GlobaState as an unknown +export type GlobalState = ReturnType; + +export const appStoreFactory = () => { + const store = createStore(endpointAppReducers); + return store; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers.ts similarity index 55% rename from x-pack/plugins/endpoint/public/applications/endpoint/types.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/reducers.ts index 8182b76cd3f87..c3287da989a2b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -// import reducers from './reducers'; +import { combineReducers } from 'redux'; +import { appReducer } from './app/reducers'; -// export type GlobalState = ReturnType; - -export interface GlobalState { - [key: string]: any; -} // TODO: replace once `reducers` are in place +export const endpointAppReducers = combineReducers({ + app: appReducer, +}); From d7c9cedee1eeb207960d2130dc1caa6fe28185a8 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Fri, 3 Jan 2020 15:54:16 -0500 Subject: [PATCH 07/19] Added Redux Dev tools support + selectors for app --- .../applications/endpoint/store/app/selectors.ts | 13 +++++++++++++ .../public/applications/endpoint/store/index.ts | 10 ++++++---- .../public/applications/endpoint/store/reducers.ts | 7 +++++++ 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/app/selectors.ts diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/app/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/app/selectors.ts new file mode 100644 index 0000000000000..ada7df748f750 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/app/selectors.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GlobalState } from '../index'; + +const selectAppState = (state: GlobalState) => state.app; + +export const coreStartServices = (state: GlobalState) => { + return selectAppState(state).coreStartServices!; +}; 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 38d2356871f8d..6058532153cc3 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createStore } from 'redux'; +import { createStore, StoreEnhancer } from 'redux'; import { endpointAppReducers } from './reducers'; -// FIXME: typing below is not correct - shows GlobaState as an unknown -export type GlobalState = ReturnType; +export { GlobalState } from './reducers'; + +const composeWithReduxDevTools = + (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || ((enhancer?: StoreEnhancer) => enhancer); export const appStoreFactory = () => { - const store = createStore(endpointAppReducers); + const store = createStore(endpointAppReducers, composeWithReduxDevTools()); return store; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers.ts index c3287da989a2b..666a9584dac08 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers.ts @@ -7,6 +7,13 @@ import { combineReducers } from 'redux'; import { appReducer } from './app/reducers'; +// FIXME: why is `ReturnType` not working? +// export type GlobalState = ReturnType; + +export interface GlobalState { + app: ReturnType; +} + export const endpointAppReducers = combineReducers({ app: appReducer, }); From 66f585aeb6f23e391ed5843e044ded6ab1fb165a Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Fri, 3 Jan 2020 16:33:41 -0500 Subject: [PATCH 08/19] Refactor: moved files around inside of `store/` to desired dir structure --- .../public/applications/endpoint/index.tsx | 2 +- .../store/{app/actions.ts => actions/app.ts} | 0 .../public/applications/endpoint/store/index.ts | 11 +++++++---- .../store/{app/reducers.ts => reducers/app.ts} | 2 +- .../store/{reducers.ts => reducers/index.ts} | 10 +++++----- .../applications/endpoint/store/sagas/index.ts | 14 ++++++++++++++ .../store/{app/selectors.ts => selectors/app.ts} | 0 7 files changed, 28 insertions(+), 11 deletions(-) rename x-pack/plugins/endpoint/public/applications/endpoint/store/{app/actions.ts => actions/app.ts} (100%) rename x-pack/plugins/endpoint/public/applications/endpoint/store/{app/reducers.ts => reducers/app.ts} (95%) rename x-pack/plugins/endpoint/public/applications/endpoint/store/{reducers.ts => reducers/index.ts} (92%) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/sagas/index.ts rename x-pack/plugins/endpoint/public/applications/endpoint/store/{app/selectors.ts => selectors/app.ts} (100%) diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 847a98f6f80ee..ee3270f7b7b0c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -10,7 +10,7 @@ import { CoreStart, AppMountParameters } from 'kibana/public'; import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; import { Route, BrowserRouter, Switch } from 'react-router-dom'; import { appStoreFactory } from './store'; -import { AppDispatch } from './store/app/actions'; +import { AppDispatch } from './store/actions/app'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/app/actions.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/actions/app.ts similarity index 100% rename from x-pack/plugins/endpoint/public/applications/endpoint/store/app/actions.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/actions/app.ts 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 6058532153cc3..729ea3b6a11cb 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -4,15 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createStore, StoreEnhancer } from 'redux'; +import { createStore, compose, applyMiddleware } from 'redux'; import { endpointAppReducers } from './reducers'; +import { endpointAppSagas } from './sagas'; export { GlobalState } from './reducers'; -const composeWithReduxDevTools = - (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || ((enhancer?: StoreEnhancer) => enhancer); +const composeWithReduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; export const appStoreFactory = () => { - const store = createStore(endpointAppReducers, composeWithReduxDevTools()); + const store = createStore( + endpointAppReducers, + composeWithReduxDevTools(applyMiddleware(endpointAppSagas)) + ); return store; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/app/reducers.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.ts similarity index 95% rename from x-pack/plugins/endpoint/public/applications/endpoint/store/app/reducers.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.ts index 52fd0ff84f1c6..9c997c3355129 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/app/reducers.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.ts @@ -5,7 +5,7 @@ */ import { CoreStart } from 'kibana/public'; -import { AppAction } from './actions'; +import { AppAction } from '../actions/app'; interface AppState { coreStartServices: null | CoreStart; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/index.ts similarity index 92% rename from x-pack/plugins/endpoint/public/applications/endpoint/store/reducers.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/index.ts index 666a9584dac08..e635dc4f85f3a 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/index.ts @@ -5,7 +5,11 @@ */ import { combineReducers } from 'redux'; -import { appReducer } from './app/reducers'; +import { appReducer } from './app'; + +export const endpointAppReducers = combineReducers({ + app: appReducer, +}); // FIXME: why is `ReturnType` not working? // export type GlobalState = ReturnType; @@ -13,7 +17,3 @@ import { appReducer } from './app/reducers'; export interface GlobalState { app: ReturnType; } - -export const endpointAppReducers = combineReducers({ - app: appReducer, -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/sagas/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/sagas/index.ts new file mode 100644 index 0000000000000..4f1f2672e5eb2 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/sagas/index.ts @@ -0,0 +1,14 @@ +/* + * 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 '../../lib'; + +export const endpointAppSagas = createSagaMiddleware(async (sagaContext: SagaContext) => { + await Promise.all([ + // Individual Sagas go here, once they exist. Example: + // endpointsListSaga(sagaContext); + ]); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/app/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.ts similarity index 100% rename from x-pack/plugins/endpoint/public/applications/endpoint/store/app/selectors.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.ts From 3c639b2dd41534479e0c7e4726e154b6e1b99c9e Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Fri, 3 Jan 2020 17:17:37 -0500 Subject: [PATCH 09/19] tests for app seletors --- .../endpoint/store/selectors/app.test.ts | 62 +++++++++++++++++++ .../endpoint/store/selectors/app.ts | 4 ++ 2 files changed, 66 insertions(+) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.test.ts diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.test.ts new file mode 100644 index 0000000000000..47ca07015cb36 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.test.ts @@ -0,0 +1,62 @@ +/* + * 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 { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { endpointAppReducers, GlobalState } from '../reducers'; +import { createStore, Store } from 'redux'; +import { coreStartServices, appBasePath } from './app'; +import { AppDispatch } from '../actions/app'; +import { CoreStart } from 'kibana/public'; + +describe('app selectors', () => { + let store: Store; + let dispatch: AppDispatch; + const setupStore = () => { + store = createStore(endpointAppReducers); + dispatch = store.dispatch; + }; + + describe('before `appWillMount` action', () => { + let state: GlobalState; + + beforeEach(() => { + setupStore(); + state = store.getState(); + }); + + test('coreStartServices is initially null', () => { + expect(coreStartServices(state)).toBeNull(); + }); + + test('appBasePath is initially empty string', () => { + expect(appBasePath(state)).toEqual(''); + }); + }); + + describe('after `appWillMount` action', () => { + let coreStartServicesMock: CoreStart; + + beforeEach(() => { + setupStore(); + coreStartServicesMock = coreMock.createStart({ basePath: '/some/path' }); + dispatch({ + type: 'appWillMount', + payload: { + appBasePath: '/some/path', + coreStartServices: coreStartServicesMock, + }, + }); + }); + + test('coreStartServices set to an object', () => { + expect(coreStartServices(store.getState())).toEqual(coreStartServicesMock); + }); + + test('appBasePath is set', () => { + expect(appBasePath(store.getState())).toEqual('/some/path'); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.ts index ada7df748f750..cab80865acb4f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.ts @@ -11,3 +11,7 @@ const selectAppState = (state: GlobalState) => state.app; export const coreStartServices = (state: GlobalState) => { return selectAppState(state).coreStartServices!; }; + +export const appBasePath = (state: GlobalState) => { + return selectAppState(state).appBasePath; +}; From 37147e183872b38fbbb7c463d3064318e7c74e35 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Fri, 3 Jan 2020 17:47:15 -0500 Subject: [PATCH 10/19] Tests for app reducers --- .../endpoint/store/reducers/app.test.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.test.ts diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.test.ts new file mode 100644 index 0000000000000..9734e4bdfa499 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.test.ts @@ -0,0 +1,58 @@ +/* + * 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 { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { createStore, Store } from 'redux'; +import { endpointAppReducers, GlobalState } from './index'; +import { AppDispatch } from '../actions/app'; + +describe('app Reducers', () => { + const objectThatLooksLikeAppState = expect.objectContaining({ + appBasePath: expect.stringMatching('/some/path'), + coreStartServices: expect.objectContaining({ + application: expect.anything(), + chrome: expect.anything(), + docLinks: expect.anything(), + http: expect.anything(), + i18n: expect.anything(), + notifications: expect.anything(), + overlays: expect.anything(), + uiSettings: expect.anything(), + savedObjects: expect.anything(), + injectedMetadata: expect.anything(), + }), + }); + + let store: Store; + let dispatch: AppDispatch; + + beforeEach(() => { + store = createStore(endpointAppReducers); + dispatch = store.dispatch; + dispatch({ + type: 'appWillMount', + payload: { + appBasePath: '/some/path', + coreStartServices: coreMock.createStart({ basePath: '/some/path' }), + }, + }); + }); + + test('it stores kibana start/mount data on `appWillMount`', () => { + expect(store.getState().app).toEqual(objectThatLooksLikeAppState); + }); + + test('it resets the store on `appDidUnmount`', () => { + dispatch({ + type: 'appDidUnmount', + }); + + expect(store.getState().app).toEqual({ + appBasePath: '', + coreStartServices: null, + }); + }); +}); From bd33f5f5f0e3579946679ef0e9a55a8627acd533 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 14 Jan 2020 10:37:22 -0500 Subject: [PATCH 11/19] Deleted actions/reducers/selectors directories --- .../endpoint/store/actions/app.ts | 27 -------- .../endpoint/store/reducers/app.test.ts | 58 ----------------- .../endpoint/store/reducers/app.ts | 34 ---------- .../endpoint/store/reducers/index.ts | 19 ------ .../endpoint/store/sagas/index.ts | 14 ----- .../endpoint/store/selectors/app.test.ts | 62 ------------------- .../endpoint/store/selectors/app.ts | 17 ----- 7 files changed, 231 deletions(-) delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/actions/app.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.test.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/index.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/sagas/index.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.test.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.ts diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/actions/app.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/actions/app.ts deleted file mode 100644 index 7f4b249191a27..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/actions/app.ts +++ /dev/null @@ -1,27 +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'; - -interface AppWillMount { - readonly type: 'appWillMount'; - payload: { - coreStartServices: CoreStart; - appBasePath: string; - }; -} - -/** - * Endpoint App has been un-mounted from DOM. Use this opportunity to remove any - * global event listeners, notify server-side services or other type of cleanup - */ -interface AppDidUnmount { - readonly type: 'appDidUnmount'; -} - -export type AppAction = AppWillMount | AppDidUnmount; - -export type AppDispatch = (action: AppAction) => AppAction; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.test.ts deleted file mode 100644 index 9734e4bdfa499..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.test.ts +++ /dev/null @@ -1,58 +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 { coreMock } from '../../../../../../../../src/core/public/mocks'; -import { createStore, Store } from 'redux'; -import { endpointAppReducers, GlobalState } from './index'; -import { AppDispatch } from '../actions/app'; - -describe('app Reducers', () => { - const objectThatLooksLikeAppState = expect.objectContaining({ - appBasePath: expect.stringMatching('/some/path'), - coreStartServices: expect.objectContaining({ - application: expect.anything(), - chrome: expect.anything(), - docLinks: expect.anything(), - http: expect.anything(), - i18n: expect.anything(), - notifications: expect.anything(), - overlays: expect.anything(), - uiSettings: expect.anything(), - savedObjects: expect.anything(), - injectedMetadata: expect.anything(), - }), - }); - - let store: Store; - let dispatch: AppDispatch; - - beforeEach(() => { - store = createStore(endpointAppReducers); - dispatch = store.dispatch; - dispatch({ - type: 'appWillMount', - payload: { - appBasePath: '/some/path', - coreStartServices: coreMock.createStart({ basePath: '/some/path' }), - }, - }); - }); - - test('it stores kibana start/mount data on `appWillMount`', () => { - expect(store.getState().app).toEqual(objectThatLooksLikeAppState); - }); - - test('it resets the store on `appDidUnmount`', () => { - dispatch({ - type: 'appDidUnmount', - }); - - expect(store.getState().app).toEqual({ - appBasePath: '', - coreStartServices: null, - }); - }); -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.ts deleted file mode 100644 index 9c997c3355129..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/app.ts +++ /dev/null @@ -1,34 +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 { AppAction } from '../actions/app'; - -interface AppState { - coreStartServices: null | CoreStart; - appBasePath: string; -} - -const createAppState = (): AppState => { - return { - appBasePath: '', - coreStartServices: null, - }; -}; - -export const appReducer = (state: AppState = createAppState(), action: AppAction) => { - switch (action.type) { - case 'appWillMount': - const { coreStartServices, appBasePath } = action.payload; - return { ...state, coreStartServices, appBasePath }; - - case 'appDidUnmount': - return createAppState(); - - default: - return state; - } -}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/index.ts deleted file mode 100644 index e635dc4f85f3a..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducers/index.ts +++ /dev/null @@ -1,19 +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 { combineReducers } from 'redux'; -import { appReducer } from './app'; - -export const endpointAppReducers = combineReducers({ - app: appReducer, -}); - -// FIXME: why is `ReturnType` not working? -// export type GlobalState = ReturnType; - -export interface GlobalState { - app: ReturnType; -} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/sagas/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/sagas/index.ts deleted file mode 100644 index 4f1f2672e5eb2..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/sagas/index.ts +++ /dev/null @@ -1,14 +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 { createSagaMiddleware, SagaContext } from '../../lib'; - -export const endpointAppSagas = createSagaMiddleware(async (sagaContext: SagaContext) => { - await Promise.all([ - // Individual Sagas go here, once they exist. Example: - // endpointsListSaga(sagaContext); - ]); -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.test.ts deleted file mode 100644 index 47ca07015cb36..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.test.ts +++ /dev/null @@ -1,62 +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 { coreMock } from '../../../../../../../../src/core/public/mocks'; -import { endpointAppReducers, GlobalState } from '../reducers'; -import { createStore, Store } from 'redux'; -import { coreStartServices, appBasePath } from './app'; -import { AppDispatch } from '../actions/app'; -import { CoreStart } from 'kibana/public'; - -describe('app selectors', () => { - let store: Store; - let dispatch: AppDispatch; - const setupStore = () => { - store = createStore(endpointAppReducers); - dispatch = store.dispatch; - }; - - describe('before `appWillMount` action', () => { - let state: GlobalState; - - beforeEach(() => { - setupStore(); - state = store.getState(); - }); - - test('coreStartServices is initially null', () => { - expect(coreStartServices(state)).toBeNull(); - }); - - test('appBasePath is initially empty string', () => { - expect(appBasePath(state)).toEqual(''); - }); - }); - - describe('after `appWillMount` action', () => { - let coreStartServicesMock: CoreStart; - - beforeEach(() => { - setupStore(); - coreStartServicesMock = coreMock.createStart({ basePath: '/some/path' }); - dispatch({ - type: 'appWillMount', - payload: { - appBasePath: '/some/path', - coreStartServices: coreStartServicesMock, - }, - }); - }); - - test('coreStartServices set to an object', () => { - expect(coreStartServices(store.getState())).toEqual(coreStartServicesMock); - }); - - test('appBasePath is set', () => { - expect(appBasePath(store.getState())).toEqual('/some/path'); - }); - }); -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.ts deleted file mode 100644 index cab80865acb4f..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/selectors/app.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { GlobalState } from '../index'; - -const selectAppState = (state: GlobalState) => state.app; - -export const coreStartServices = (state: GlobalState) => { - return selectAppState(state).coreStartServices!; -}; - -export const appBasePath = (state: GlobalState) => { - return selectAppState(state).appBasePath; -}; From b155be63771a83089215637309e467a4fcaf30b2 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 14 Jan 2020 11:40:44 -0500 Subject: [PATCH 12/19] Revised structure for Store Includes initial stubs for endpoint_list --- .../public/applications/endpoint/index.tsx | 38 +++++++++---------- .../public/applications/endpoint/lib/saga.ts | 6 +++ .../endpoint/store/endpoint_list/action.ts | 25 ++++++++++++ .../endpoint/store/endpoint_list/index.ts | 10 +++++ .../endpoint/store/endpoint_list/reducer.ts | 32 ++++++++++++++++ .../endpoint/store/endpoint_list/saga.ts | 25 ++++++++++++ .../endpoint/store/endpoint_list/types.ts | 15 ++++++++ .../applications/endpoint/store/index.ts | 13 ++++--- .../applications/endpoint/store/reducer.ts | 15 ++++++++ .../applications/endpoint/store/saga.ts | 18 +++++++++ 10 files changed, 171 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/action.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/reducer.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index ee3270f7b7b0c..2dfc94a94c41b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -9,8 +9,11 @@ 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 { Dispatch } from 'redux'; import { appStoreFactory } from './store'; -import { AppDispatch } from './store/actions/app'; + +// FIXME: temporary until we figure out if redux can be upgraded to use hooks +let dispatch: Dispatch; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. @@ -18,22 +21,13 @@ import { AppDispatch } from './store/actions/app'; export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMountParameters) { coreStart.http.get('/api/endpoint/hello-world'); - const store = appStoreFactory(); - const dispatch: AppDispatch = store.dispatch; - - dispatch({ - type: 'appWillMount', - payload: { - coreStartServices: coreStart, - appBasePath, - }, - }); + const store = appStoreFactory(coreStart); + dispatch = store.dispatch; ReactDOM.render(, element); return () => { ReactDOM.unmountComponentAtNode(element); - dispatch({ type: 'appDidUnmount' }); }; } @@ -56,14 +50,18 @@ const AppRoot: React.FunctionComponent = React.memo(({ basename }) /> ( -

- -

- )} + render={() => { + dispatch({ type: 'userEnteredEndpointListPage' }); + + return ( +

+ +

+ ); + }} /> ( diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts index 74fb284ef7020..66655aae1de15 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts @@ -13,7 +13,13 @@ interface StoreAction extends AnyAction { } interface QueuedAction { + /** + * The Redux action that was dispatched + */ action: StoreAction; + /** + * The Global state at the time the action was dispatched + */ state: GlobalState; } 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 0000000000000..02ec0f9d09035 --- /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.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.ts new file mode 100644 index 0000000000000..bf598bc081dda --- /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 { endpointsListSaga } 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 0000000000000..9813777c988ef --- /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.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.ts new file mode 100644 index 0000000000000..11c188cfe3619 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.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 { CoreStart } from 'kibana/public'; +import { SagaContext } from '../../lib'; + +export const endpointsListSaga = 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 new file mode 100644 index 0000000000000..229d1eae4798f --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts @@ -0,0 +1,15 @@ +/* + * 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 to a module we can reference +export interface EndpointListData { + endpoints: object[]; + 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 729ea3b6a11cb..2a7f83d2d60ed 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -5,17 +5,18 @@ */ import { createStore, compose, applyMiddleware } from 'redux'; -import { endpointAppReducers } from './reducers'; -import { endpointAppSagas } from './sagas'; +import { CoreStart } from 'kibana/public'; +import { appSagaFactory } from './saga'; +import { appReducer } from './reducer'; -export { GlobalState } from './reducers'; +export { GlobalState } from './reducer'; const composeWithReduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; -export const appStoreFactory = () => { +export const appStoreFactory = (coreStart: CoreStart) => { const store = createStore( - endpointAppReducers, - composeWithReduxDevTools(applyMiddleware(endpointAppSagas)) + 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 0000000000000..7f540babefe95 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts @@ -0,0 +1,15 @@ +/* + * 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'; + +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 0000000000000..2add9d3e44f19 --- /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 { endpointsListSaga } from './endpoint_list'; + +export const appSagaFactory = (coreStart: CoreStart) => { + return createSagaMiddleware(async (sagaContext: SagaContext) => { + await Promise.all([ + // Concerns specific sagas here + endpointsListSaga(sagaContext, coreStart), + ]); + }); +}; From 2d55b2f8882f9579d822b365de74ea84e5db3aa4 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 14 Jan 2020 15:21:43 -0500 Subject: [PATCH 13/19] Add store Provider to endpoint --- .../public/applications/endpoint/index.tsx | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 2dfc94a94c41b..fc61cb419b038 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -9,12 +9,10 @@ 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 { Dispatch } from 'redux'; +import { Provider } from 'react-redux'; +import { Store } from 'redux'; import { appStoreFactory } from './store'; -// FIXME: temporary until we figure out if redux can be upgraded to use hooks -let dispatch: Dispatch; - /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. */ @@ -22,9 +20,8 @@ export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMou coreStart.http.get('/api/endpoint/hello-world'); const store = appStoreFactory(coreStart); - dispatch = store.dispatch; - ReactDOM.render(, element); + ReactDOM.render(, element); return () => { ReactDOM.unmountComponentAtNode(element); @@ -33,42 +30,45 @@ export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMou interface RouterProps { basename: string; + store: Store; } -const AppRoot: React.FunctionComponent = React.memo(({ basename }) => ( - - - - ( -

- -

- )} - /> - { - dispatch({ type: 'userEnteredEndpointListPage' }); - - return ( -

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

+

- ); - }} - /> - ( - - )} - /> -
-
-
+ )} + /> + { + store.dispatch({ type: 'userEnteredEndpointListPage' }); + + return ( +

+ +

+ ); + }} + /> + ( + + )} + /> + + + +
)); From 482dab0c9d1d57ec37b80681780077b88ee22245 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 14 Jan 2020 15:37:33 -0500 Subject: [PATCH 14/19] Add name to store for ReduxDevTools --- .../endpoint/public/applications/endpoint/store/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 2a7f83d2d60ed..d0dc002031ce2 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -11,7 +11,9 @@ import { appReducer } from './reducer'; export { GlobalState } from './reducer'; -const composeWithReduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; +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( From da1a7ba5599d57ba8ba579dd68b24bf4323b98ca Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 14 Jan 2020 16:06:13 -0500 Subject: [PATCH 15/19] Added sample selectors to endpoint_list + actions to store --- .../public/applications/endpoint/store/actions.ts | 9 +++++++++ .../endpoint/store/endpoint_list/selectors.ts | 9 +++++++++ .../public/applications/endpoint/store/reducer.ts | 3 ++- 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/actions.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts 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 0000000000000..796dabce1d76a --- /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/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts new file mode 100644 index 0000000000000..6ffcebc3f41aa --- /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/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts index 7f540babefe95..59ca4de91ac83 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts @@ -5,11 +5,12 @@ */ 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({ +export const appReducer: Reducer = combineReducers({ endpointList: endpointListReducer, }); From df9358167eeb8e62224af97bf58ebbfd05ea9f86 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 14 Jan 2020 16:37:41 -0500 Subject: [PATCH 16/19] Make `SagaContext` type generic that accepts Action types --- .../public/applications/endpoint/lib/saga.ts | 18 ++++++++++-------- .../endpoint/store/endpoint_list/saga.ts | 3 ++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts index 66655aae1de15..030cf0a318262 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts @@ -12,11 +12,11 @@ interface StoreAction extends AnyAction { type: string; } -interface QueuedAction { +interface QueuedAction { /** * The Redux action that was dispatched */ - action: StoreAction; + action: TAction; /** * The Global state at the time the action was dispatched */ @@ -30,14 +30,14 @@ interface IteratorInstance { type Saga = (storeContext: SagaContext) => Promise; -type StoreActionsAndState = AsyncIterableIterator; +type StoreActionsAndState = AsyncIterableIterator>; -export interface SagaContext { +export interface SagaContext { /** * A generator function that will `yield` `Promise`s that resolve with a `QueuedAction` */ - actionsAndState: () => StoreActionsAndState; - dispatch: Dispatch; + actionsAndState: () => StoreActionsAndState; + dispatch: Dispatch; } const noop = () => {}; @@ -52,12 +52,14 @@ const noop = () => {}; * * @example * - * const endpointsSaga = async ({ actionsAndState, dispatch }: SagaContext) => { + * 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: 'serverReturnedUserRequestedResource', + * type: 'add', * payload: [ resourceData ] * }); * } 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 index 11c188cfe3619..dcead206938d1 100644 --- 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 @@ -6,9 +6,10 @@ import { CoreStart } from 'kibana/public'; import { SagaContext } from '../../lib'; +import { EndpointListAction } from './action'; export const endpointsListSaga = async ( - { actionsAndState, dispatch }: SagaContext, + { actionsAndState, dispatch }: SagaContext, coreStart: CoreStart ) => { const { post: httpPost } = coreStart.http; From 58d903613e8772fa4de2f9a008b71d78777b3d74 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Wed, 15 Jan 2020 10:09:15 -0500 Subject: [PATCH 17/19] Correction to Saga types --- .../public/applications/endpoint/lib/saga.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts index 030cf0a318262..b93360ec6b5aa 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts @@ -7,12 +7,7 @@ import { AnyAction, Dispatch, Middleware, MiddlewareAPI } from 'redux'; import { GlobalState } from '../store'; -interface StoreAction extends AnyAction { - payload: unknown[]; - type: string; -} - -interface QueuedAction { +interface QueuedAction { /** * The Redux action that was dispatched */ @@ -30,9 +25,9 @@ interface IteratorInstance { type Saga = (storeContext: SagaContext) => Promise; -type StoreActionsAndState = AsyncIterableIterator>; +type StoreActionsAndState = AsyncIterableIterator>; -export interface SagaContext { +export interface SagaContext { /** * A generator function that will `yield` `Promise`s that resolve with a `QueuedAction` */ @@ -116,7 +111,7 @@ export function createSagaMiddleware(saga: Saga): Middleware { }); runSaga(); } - return (next: Dispatch) => (action: StoreAction) => { + return (next: Dispatch) => (action: AnyAction) => { // Call the next dispatch method in the middleware chain. const returnValue = next(action); From 03798c7a663d76efccdef2b04dcc93f97e78a4c2 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Wed, 15 Jan 2020 16:51:21 -0500 Subject: [PATCH 18/19] tests for endpoint_list store concerns --- .../public/applications/endpoint/index.tsx | 1 + .../store/endpoint_list/index.test.ts | 126 ++++++++++++++++++ .../endpoint/store/endpoint_list/index.ts | 2 +- .../endpoint/store/endpoint_list/saga.ts | 2 +- .../endpoint/store/endpoint_list/types.ts | 41 +++++- .../applications/endpoint/store/saga.ts | 4 +- 6 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.test.ts diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index fc61cb419b038..d69e068bdea3a 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -50,6 +50,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/endpoint_list/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.test.ts new file mode 100644 index 0000000000000..a46653f82ee45 --- /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 index bf598bc081dda..bdf0708457bb0 100644 --- 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 @@ -6,5 +6,5 @@ export { endpointListReducer } from './reducer'; export { EndpointListAction } from './action'; -export { endpointsListSaga } from './saga'; +export { endpointListSaga } from './saga'; export * from './types'; 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 index dcead206938d1..cc156cfa88002 100644 --- 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 @@ -8,7 +8,7 @@ import { CoreStart } from 'kibana/public'; import { SagaContext } from '../../lib'; import { EndpointListAction } from './action'; -export const endpointsListSaga = async ( +export const endpointListSaga = async ( { actionsAndState, dispatch }: SagaContext, coreStart: CoreStart ) => { 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 index 229d1eae4798f..f2810dd89f857 100644 --- 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 @@ -4,9 +4,48 @@ * 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: object[]; + endpoints: EndpointData[]; request_page_size: number; request_index: number; total: number; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts index 2add9d3e44f19..3b7de79d5443c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts @@ -6,13 +6,13 @@ import { CoreStart } from 'kibana/public'; import { createSagaMiddleware, SagaContext } from '../lib'; -import { endpointsListSaga } from './endpoint_list'; +import { endpointListSaga } from './endpoint_list'; export const appSagaFactory = (coreStart: CoreStart) => { return createSagaMiddleware(async (sagaContext: SagaContext) => { await Promise.all([ // Concerns specific sagas here - endpointsListSaga(sagaContext, coreStart), + endpointListSaga(sagaContext, coreStart), ]); }); }; From e6ee039548f9ba6d98515ea278c82bc2f589255c Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Wed, 15 Jan 2020 18:07:37 -0500 Subject: [PATCH 19/19] Tests for endpont_list saga --- .../endpoint/store/endpoint_list/saga.test.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts 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 0000000000000..92bf3b7fd92dd --- /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); + }); +});