From 62341ffe569e9ab8ce8b731dcd72ad6f7342db33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 30 Jan 2019 10:31:38 +0100 Subject: [PATCH 01/11] Added actionCreatorFactory and tests --- .../core/redux/actionCreatorFactory.test.ts | 53 +++++++++++++++++++ public/app/core/redux/actionCreatorFactory.ts | 33 ++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 public/app/core/redux/actionCreatorFactory.test.ts create mode 100644 public/app/core/redux/actionCreatorFactory.ts diff --git a/public/app/core/redux/actionCreatorFactory.test.ts b/public/app/core/redux/actionCreatorFactory.test.ts new file mode 100644 index 00000000000..f79c1ebe0dc --- /dev/null +++ b/public/app/core/redux/actionCreatorFactory.test.ts @@ -0,0 +1,53 @@ +import { actionCreatorFactory, resetAllActionCreatorTypes } from './actionCreatorFactory'; + +interface Dummy { + n: number; + s: string; + o: { + n: number; + s: string; + b: boolean; + }; + b: boolean; +} + +const setup = payload => { + resetAllActionCreatorTypes(); + const actionCreator = actionCreatorFactory('dummy').create(); + const result = actionCreator(payload); + + return { + actionCreator, + result, + }; +}; + +describe('actionCreatorFactory', () => { + describe('when calling create', () => { + it('then it should create correct type string', () => { + const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } }; + const { actionCreator, result } = setup(payload); + + expect(actionCreator.type).toEqual('dummy'); + expect(result.type).toEqual('dummy'); + }); + + it('then it should create correct payload', () => { + const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } }; + const { result } = setup(payload); + + expect(result.payload).toEqual(payload); + }); + }); + + describe('when calling create with existing type', () => { + it('then it should throw error', () => { + const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } }; + setup(payload); + + expect(() => { + actionCreatorFactory('dummy').create(); + }).toThrow(); + }); + }); +}); diff --git a/public/app/core/redux/actionCreatorFactory.ts b/public/app/core/redux/actionCreatorFactory.ts new file mode 100644 index 00000000000..936440f735a --- /dev/null +++ b/public/app/core/redux/actionCreatorFactory.ts @@ -0,0 +1,33 @@ +import { Action } from 'redux'; + +const allActionCreators: string[] = []; + +export interface GrafanaAction extends Action { + readonly type: string; + readonly payload: Payload; +} + +export interface GrafanaActionCreator { + readonly type: string; + (payload: Payload): GrafanaAction; +} + +export interface ActionCreatorFactory { + create: () => GrafanaActionCreator; +} + +export const actionCreatorFactory = (type: string): ActionCreatorFactory => { + const create = (): GrafanaActionCreator => { + return Object.assign((payload: Payload): GrafanaAction => ({ type, payload }), { type }); + }; + + if (allActionCreators.some(t => type === type)) { + throw new Error(`There is already an actionCreator defined with the type ${type}`); + } + + allActionCreators.push(type); + + return { create }; +}; + +export const resetAllActionCreatorTypes = () => (allActionCreators.length = 0); From 0ddaa95d0e0d254679d4dc9e06ffa72a9ac7110f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 30 Jan 2019 11:13:36 +0100 Subject: [PATCH 02/11] Added reducerFactory and tests --- .../core/redux/actionCreatorFactory.test.ts | 2 +- public/app/core/redux/reducerFactory.test.ts | 99 +++++++++++++++++++ public/app/core/redux/reducerFactory.ts | 55 +++++++++++ 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 public/app/core/redux/reducerFactory.test.ts create mode 100644 public/app/core/redux/reducerFactory.ts diff --git a/public/app/core/redux/actionCreatorFactory.test.ts b/public/app/core/redux/actionCreatorFactory.test.ts index f79c1ebe0dc..18ba6915368 100644 --- a/public/app/core/redux/actionCreatorFactory.test.ts +++ b/public/app/core/redux/actionCreatorFactory.test.ts @@ -11,7 +11,7 @@ interface Dummy { b: boolean; } -const setup = payload => { +const setup = (payload: Dummy) => { resetAllActionCreatorTypes(); const actionCreator = actionCreatorFactory('dummy').create(); const result = actionCreator(payload); diff --git a/public/app/core/redux/reducerFactory.test.ts b/public/app/core/redux/reducerFactory.test.ts new file mode 100644 index 00000000000..85b3939efad --- /dev/null +++ b/public/app/core/redux/reducerFactory.test.ts @@ -0,0 +1,99 @@ +import { reducerFactory } from './reducerFactory'; +import { actionCreatorFactory, GrafanaAction } from './actionCreatorFactory'; + +interface DummyReducerState { + n: number; + s: string; + b: boolean; + o: { + n: number; + s: string; + b: boolean; + }; +} + +const dummyReducerIntialState: DummyReducerState = { + n: 1, + s: 'One', + b: true, + o: { + n: 2, + s: 'two', + b: false, + }, +}; + +const dummyActionCreator = actionCreatorFactory('dummy').create(); + +const dummyReducer = reducerFactory(dummyReducerIntialState) + .addHandler({ + creator: dummyActionCreator, + handler: ({ state, action }) => { + return { ...state, ...action.payload }; + }, + }) + .create(); + +describe('reducerFactory', () => { + describe('given it is created with a defined handler', () => { + describe('when reducer is called with no state', () => { + describe('and with an action that the handler can not handle', () => { + it('then the resulting state should be intial state', () => { + const result = dummyReducer(undefined as DummyReducerState, {} as GrafanaAction); + + expect(result).toEqual(dummyReducerIntialState); + }); + }); + + describe('and with an action that the handler can handle', () => { + it('then the resulting state should correct', () => { + const payload = { n: 10, s: 'ten', b: false, o: { n: 20, s: 'twenty', b: true } }; + const result = dummyReducer(undefined as DummyReducerState, dummyActionCreator(payload)); + + expect(result).toEqual(payload); + }); + }); + }); + + describe('when reducer is called with a state', () => { + describe('and with an action that the handler can not handle', () => { + it('then the resulting state should be intial state', () => { + const result = dummyReducer(dummyReducerIntialState, {} as GrafanaAction); + + expect(result).toEqual(dummyReducerIntialState); + }); + }); + + describe('and with an action that the handler can handle', () => { + it('then the resulting state should correct', () => { + const payload = { n: 10, s: 'ten', b: false, o: { n: 20, s: 'twenty', b: true } }; + const result = dummyReducer(dummyReducerIntialState, dummyActionCreator(payload)); + + expect(result).toEqual(payload); + }); + }); + }); + }); + + describe('given a handler is added', () => { + describe('when a handler with the same creator is added', () => { + it('then is should throw', () => { + const faultyReducer = reducerFactory(dummyReducerIntialState).addHandler({ + creator: dummyActionCreator, + handler: ({ state, action }) => { + return { ...state, ...action.payload }; + }, + }); + + expect(() => { + faultyReducer.addHandler({ + creator: dummyActionCreator, + handler: ({ state }) => { + return state; + }, + }); + }).toThrow(); + }); + }); + }); +}); diff --git a/public/app/core/redux/reducerFactory.ts b/public/app/core/redux/reducerFactory.ts new file mode 100644 index 00000000000..0ce9ac95edd --- /dev/null +++ b/public/app/core/redux/reducerFactory.ts @@ -0,0 +1,55 @@ +import { GrafanaAction, GrafanaActionCreator } from './actionCreatorFactory'; +import { Reducer } from 'redux'; + +export interface ActionHandler { + state: State; + action: GrafanaAction; +} + +export interface ActionHandlerConfig { + creator: GrafanaActionCreator; + handler: (handler: ActionHandler) => State; +} + +export interface AddActionHandler { + addHandler: (config: ActionHandlerConfig) => CreateReducer; +} + +export interface CreateReducer extends AddActionHandler { + create: () => Reducer>; +} + +export const reducerFactory = (initialState: State): AddActionHandler => { + const allHandlerConfigs: Array> = []; + + const addHandler = (config: ActionHandlerConfig): CreateReducer => { + if (allHandlerConfigs.some(c => c.creator.type === config.creator.type)) { + throw new Error(`There is already a handlers defined with the type ${config.creator.type}`); + } + + allHandlerConfigs.push(config); + + return instance; + }; + + const create = (): Reducer> => { + const reducer: Reducer> = (state: State = initialState, action: GrafanaAction) => { + const validHandlers = allHandlerConfigs + .filter(config => config.creator.type === action.type) + .map(config => config.handler); + + return validHandlers.reduce((currentState, handler) => { + return handler({ state: currentState, action }); + }, state || initialState); + }; + + return reducer; + }; + + const instance: CreateReducer = { + addHandler, + create, + }; + + return instance; +}; From 65fb77ce73cc9bcab8d63d899dbbdf9c4f749fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 30 Jan 2019 11:28:23 +0100 Subject: [PATCH 03/11] Fixed a small bug and added case sensitivity --- public/app/core/redux/actionCreatorFactory.test.ts | 2 +- public/app/core/redux/actionCreatorFactory.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/public/app/core/redux/actionCreatorFactory.test.ts b/public/app/core/redux/actionCreatorFactory.test.ts index 18ba6915368..96464354411 100644 --- a/public/app/core/redux/actionCreatorFactory.test.ts +++ b/public/app/core/redux/actionCreatorFactory.test.ts @@ -46,7 +46,7 @@ describe('actionCreatorFactory', () => { setup(payload); expect(() => { - actionCreatorFactory('dummy').create(); + actionCreatorFactory('DuMmY').create(); }).toThrow(); }); }); diff --git a/public/app/core/redux/actionCreatorFactory.ts b/public/app/core/redux/actionCreatorFactory.ts index 936440f735a..d6e33160d5f 100644 --- a/public/app/core/redux/actionCreatorFactory.ts +++ b/public/app/core/redux/actionCreatorFactory.ts @@ -21,7 +21,7 @@ export const actionCreatorFactory = (type: string): ActionCreatorFactor return Object.assign((payload: Payload): GrafanaAction => ({ type, payload }), { type }); }; - if (allActionCreators.some(t => type === type)) { + if (allActionCreators.some(t => (t && type ? t.toLocaleUpperCase() === type.toLocaleUpperCase() : false))) { throw new Error(`There is already an actionCreator defined with the type ${type}`); } @@ -30,4 +30,5 @@ export const actionCreatorFactory = (type: string): ActionCreatorFactor return { create }; }; +// Should only be used by tests export const resetAllActionCreatorTypes = () => (allActionCreators.length = 0); From 94ce065f749ab94d887447c845c5a70ccbd981b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 30 Jan 2019 11:59:00 +0100 Subject: [PATCH 04/11] Simplified inteface for reducerFactory --- public/app/core/redux/reducerFactory.test.ts | 14 ++++----- public/app/core/redux/reducerFactory.ts | 31 ++++++++------------ 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/public/app/core/redux/reducerFactory.test.ts b/public/app/core/redux/reducerFactory.test.ts index 85b3939efad..7e1d5e350e0 100644 --- a/public/app/core/redux/reducerFactory.test.ts +++ b/public/app/core/redux/reducerFactory.test.ts @@ -27,10 +27,8 @@ const dummyActionCreator = actionCreatorFactory('dummy').crea const dummyReducer = reducerFactory(dummyReducerIntialState) .addHandler({ - creator: dummyActionCreator, - handler: ({ state, action }) => { - return { ...state, ...action.payload }; - }, + filter: dummyActionCreator, + handler: (state, action) => ({ ...state, ...action.payload }), }) .create(); @@ -79,16 +77,16 @@ describe('reducerFactory', () => { describe('when a handler with the same creator is added', () => { it('then is should throw', () => { const faultyReducer = reducerFactory(dummyReducerIntialState).addHandler({ - creator: dummyActionCreator, - handler: ({ state, action }) => { + filter: dummyActionCreator, + handler: (state, action) => { return { ...state, ...action.payload }; }, }); expect(() => { faultyReducer.addHandler({ - creator: dummyActionCreator, - handler: ({ state }) => { + filter: dummyActionCreator, + handler: state => { return state; }, }); diff --git a/public/app/core/redux/reducerFactory.ts b/public/app/core/redux/reducerFactory.ts index 0ce9ac95edd..9fcc52649e3 100644 --- a/public/app/core/redux/reducerFactory.ts +++ b/public/app/core/redux/reducerFactory.ts @@ -1,30 +1,25 @@ import { GrafanaAction, GrafanaActionCreator } from './actionCreatorFactory'; import { Reducer } from 'redux'; -export interface ActionHandler { - state: State; - action: GrafanaAction; +export interface HandlerConfig { + filter: GrafanaActionCreator; + handler: (state: State, action: GrafanaAction) => State; } -export interface ActionHandlerConfig { - creator: GrafanaActionCreator; - handler: (handler: ActionHandler) => State; +export interface AddHandler { + addHandler: (config: HandlerConfig) => CreateReducer; } -export interface AddActionHandler { - addHandler: (config: ActionHandlerConfig) => CreateReducer; -} - -export interface CreateReducer extends AddActionHandler { +export interface CreateReducer extends AddHandler { create: () => Reducer>; } -export const reducerFactory = (initialState: State): AddActionHandler => { - const allHandlerConfigs: Array> = []; +export const reducerFactory = (initialState: State): AddHandler => { + const allHandlerConfigs: Array> = []; - const addHandler = (config: ActionHandlerConfig): CreateReducer => { - if (allHandlerConfigs.some(c => c.creator.type === config.creator.type)) { - throw new Error(`There is already a handlers defined with the type ${config.creator.type}`); + const addHandler = (config: HandlerConfig): CreateReducer => { + if (allHandlerConfigs.some(c => c.filter.type === config.filter.type)) { + throw new Error(`There is already a handlers defined with the type ${config.filter.type}`); } allHandlerConfigs.push(config); @@ -35,11 +30,11 @@ export const reducerFactory = (initialState: State): AddActionHandler> => { const reducer: Reducer> = (state: State = initialState, action: GrafanaAction) => { const validHandlers = allHandlerConfigs - .filter(config => config.creator.type === action.type) + .filter(config => config.filter.type === action.type) .map(config => config.handler); return validHandlers.reduce((currentState, handler) => { - return handler({ state: currentState, action }); + return handler(currentState, action); }, state || initialState); }; From 2236a1a36db7f2d5caab352fe00d58c9ab004b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 30 Jan 2019 12:03:30 +0100 Subject: [PATCH 05/11] Minor change to Action interfaces --- public/app/core/redux/actionCreatorFactory.ts | 12 ++++++------ public/app/core/redux/reducerFactory.test.ts | 6 +++--- public/app/core/redux/reducerFactory.ts | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/public/app/core/redux/actionCreatorFactory.ts b/public/app/core/redux/actionCreatorFactory.ts index d6e33160d5f..fd12ac6ca36 100644 --- a/public/app/core/redux/actionCreatorFactory.ts +++ b/public/app/core/redux/actionCreatorFactory.ts @@ -2,23 +2,23 @@ import { Action } from 'redux'; const allActionCreators: string[] = []; -export interface GrafanaAction extends Action { +export interface ActionOf extends Action { readonly type: string; readonly payload: Payload; } -export interface GrafanaActionCreator { +export interface ActionCreator { readonly type: string; - (payload: Payload): GrafanaAction; + (payload: Payload): ActionOf; } export interface ActionCreatorFactory { - create: () => GrafanaActionCreator; + create: () => ActionCreator; } export const actionCreatorFactory = (type: string): ActionCreatorFactory => { - const create = (): GrafanaActionCreator => { - return Object.assign((payload: Payload): GrafanaAction => ({ type, payload }), { type }); + const create = (): ActionCreator => { + return Object.assign((payload: Payload): ActionOf => ({ type, payload }), { type }); }; if (allActionCreators.some(t => (t && type ? t.toLocaleUpperCase() === type.toLocaleUpperCase() : false))) { diff --git a/public/app/core/redux/reducerFactory.test.ts b/public/app/core/redux/reducerFactory.test.ts index 7e1d5e350e0..2d0acc93749 100644 --- a/public/app/core/redux/reducerFactory.test.ts +++ b/public/app/core/redux/reducerFactory.test.ts @@ -1,5 +1,5 @@ import { reducerFactory } from './reducerFactory'; -import { actionCreatorFactory, GrafanaAction } from './actionCreatorFactory'; +import { actionCreatorFactory, ActionOf } from './actionCreatorFactory'; interface DummyReducerState { n: number; @@ -37,7 +37,7 @@ describe('reducerFactory', () => { describe('when reducer is called with no state', () => { describe('and with an action that the handler can not handle', () => { it('then the resulting state should be intial state', () => { - const result = dummyReducer(undefined as DummyReducerState, {} as GrafanaAction); + const result = dummyReducer(undefined as DummyReducerState, {} as ActionOf); expect(result).toEqual(dummyReducerIntialState); }); @@ -56,7 +56,7 @@ describe('reducerFactory', () => { describe('when reducer is called with a state', () => { describe('and with an action that the handler can not handle', () => { it('then the resulting state should be intial state', () => { - const result = dummyReducer(dummyReducerIntialState, {} as GrafanaAction); + const result = dummyReducer(dummyReducerIntialState, {} as ActionOf); expect(result).toEqual(dummyReducerIntialState); }); diff --git a/public/app/core/redux/reducerFactory.ts b/public/app/core/redux/reducerFactory.ts index 9fcc52649e3..2df5217ea6e 100644 --- a/public/app/core/redux/reducerFactory.ts +++ b/public/app/core/redux/reducerFactory.ts @@ -1,9 +1,9 @@ -import { GrafanaAction, GrafanaActionCreator } from './actionCreatorFactory'; +import { ActionOf, ActionCreator } from './actionCreatorFactory'; import { Reducer } from 'redux'; export interface HandlerConfig { - filter: GrafanaActionCreator; - handler: (state: State, action: GrafanaAction) => State; + filter: ActionCreator; + handler: (state: State, action: ActionOf) => State; } export interface AddHandler { @@ -11,7 +11,7 @@ export interface AddHandler { } export interface CreateReducer extends AddHandler { - create: () => Reducer>; + create: () => Reducer>; } export const reducerFactory = (initialState: State): AddHandler => { @@ -27,8 +27,8 @@ export const reducerFactory = (initialState: State): AddHandler => return instance; }; - const create = (): Reducer> => { - const reducer: Reducer> = (state: State = initialState, action: GrafanaAction) => { + const create = (): Reducer> => { + const reducer: Reducer> = (state: State = initialState, action: ActionOf) => { const validHandlers = allHandlerConfigs .filter(config => config.filter.type === action.type) .map(config => config.handler); From d3815beb1c3ded857fbe3bd4f411cbf8676c112a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 30 Jan 2019 20:07:14 +0100 Subject: [PATCH 06/11] Refactored Datasources as POC --- public/app/core/redux/index.ts | 4 + public/app/core/redux/reducerFactory.test.ts | 6 +- public/app/core/redux/reducerFactory.ts | 23 ++-- .../datasources/DataSourcesListPage.test.tsx | 11 +- .../settings/DataSourceSettingsPage.test.tsx | 5 +- .../app/features/datasources/state/actions.ts | 120 +++--------------- .../features/datasources/state/reducers.ts | 107 ++++++++++------ 7 files changed, 112 insertions(+), 164 deletions(-) create mode 100644 public/app/core/redux/index.ts diff --git a/public/app/core/redux/index.ts b/public/app/core/redux/index.ts new file mode 100644 index 00000000000..359f160b9ce --- /dev/null +++ b/public/app/core/redux/index.ts @@ -0,0 +1,4 @@ +import { actionCreatorFactory } from './actionCreatorFactory'; +import { reducerFactory } from './reducerFactory'; + +export { actionCreatorFactory, reducerFactory }; diff --git a/public/app/core/redux/reducerFactory.test.ts b/public/app/core/redux/reducerFactory.test.ts index 2d0acc93749..5057b6d84b5 100644 --- a/public/app/core/redux/reducerFactory.test.ts +++ b/public/app/core/redux/reducerFactory.test.ts @@ -28,7 +28,7 @@ const dummyActionCreator = actionCreatorFactory('dummy').crea const dummyReducer = reducerFactory(dummyReducerIntialState) .addHandler({ filter: dummyActionCreator, - handler: (state, action) => ({ ...state, ...action.payload }), + mapper: (state, action) => ({ ...state, ...action.payload }), }) .create(); @@ -78,7 +78,7 @@ describe('reducerFactory', () => { it('then is should throw', () => { const faultyReducer = reducerFactory(dummyReducerIntialState).addHandler({ filter: dummyActionCreator, - handler: (state, action) => { + mapper: (state, action) => { return { ...state, ...action.payload }; }, }); @@ -86,7 +86,7 @@ describe('reducerFactory', () => { expect(() => { faultyReducer.addHandler({ filter: dummyActionCreator, - handler: state => { + mapper: state => { return state; }, }); diff --git a/public/app/core/redux/reducerFactory.ts b/public/app/core/redux/reducerFactory.ts index 2df5217ea6e..6858dd41879 100644 --- a/public/app/core/redux/reducerFactory.ts +++ b/public/app/core/redux/reducerFactory.ts @@ -1,9 +1,10 @@ import { ActionOf, ActionCreator } from './actionCreatorFactory'; -import { Reducer } from 'redux'; + +export type Mapper = (state: State, action: ActionOf) => State; export interface HandlerConfig { filter: ActionCreator; - handler: (state: State, action: ActionOf) => State; + mapper: Mapper; } export interface AddHandler { @@ -11,7 +12,7 @@ export interface AddHandler { } export interface CreateReducer extends AddHandler { - create: () => Reducer>; + create: () => Mapper; } export const reducerFactory = (initialState: State): AddHandler => { @@ -27,18 +28,14 @@ export const reducerFactory = (initialState: State): AddHandler => return instance; }; - const create = (): Reducer> => { - const reducer: Reducer> = (state: State = initialState, action: ActionOf) => { - const validHandlers = allHandlerConfigs - .filter(config => config.filter.type === action.type) - .map(config => config.handler); + const create = () => (state: State = initialState, action: ActionOf): State => { + const handlerConfig = allHandlerConfigs.filter(config => config.filter.type === action.type)[0]; - return validHandlers.reduce((currentState, handler) => { - return handler(currentState, action); - }, state || initialState); - }; + if (handlerConfig) { + return handlerConfig.mapper(state, action); + } - return reducer; + return state; }; const instance: CreateReducer = { diff --git a/public/app/features/datasources/DataSourcesListPage.test.tsx b/public/app/features/datasources/DataSourcesListPage.test.tsx index 44ef7a1cc49..65077201f65 100644 --- a/public/app/features/datasources/DataSourcesListPage.test.tsx +++ b/public/app/features/datasources/DataSourcesListPage.test.tsx @@ -5,6 +5,7 @@ import { NavModel } from 'app/types'; import { DataSourceSettings } from '@grafana/ui/src/types'; import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; import { getMockDataSources } from './__mocks__/dataSourcesMocks'; +import { setDataSourcesSearchQuery, setDataSourcesLayoutMode } from './state/actions'; const setup = (propOverrides?: object) => { const props: Props = { @@ -13,16 +14,16 @@ const setup = (propOverrides?: object) => { loadDataSources: jest.fn(), navModel: { main: { - text: 'Configuration' + text: 'Configuration', }, node: { - text: 'Data Sources' - } + text: 'Data Sources', + }, } as NavModel, dataSourcesCount: 0, searchQuery: '', - setDataSourcesSearchQuery: jest.fn(), - setDataSourcesLayoutMode: jest.fn(), + setDataSourcesSearchQuery, + setDataSourcesLayoutMode, hasFetched: false, }; diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx index 8efc92be5be..204eeb8b1e9 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.test.tsx @@ -5,6 +5,7 @@ import { NavModel } from 'app/types'; import { DataSourceSettings } from '@grafana/ui'; import { getMockDataSource } from '../__mocks__/dataSourcesMocks'; import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks'; +import { setDataSourceName, setIsDefault } from '../state/actions'; const setup = (propOverrides?: object) => { const props: Props = { @@ -14,9 +15,9 @@ const setup = (propOverrides?: object) => { pageId: 1, deleteDataSource: jest.fn(), loadDataSource: jest.fn(), - setDataSourceName: jest.fn(), + setDataSourceName, updateDataSource: jest.fn(), - setIsDefault: jest.fn(), + setIsDefault, }; Object.assign(props, propOverrides); diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 0fa260ffafa..4cd61537cea 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -8,6 +8,8 @@ import { UpdateLocationAction } from 'app/core/actions/location'; import { buildNavModel } from './navModel'; import { DataSourceSettings } from '@grafana/ui/src/types'; import { Plugin, StoreState } from 'app/types'; +import { actionCreatorFactory } from 'app/core/redux'; +import { ActionOf } from 'app/core/redux/actionCreatorFactory'; export enum ActionTypes { LoadDataSources = 'LOAD_DATA_SOURCES', @@ -22,124 +24,36 @@ export enum ActionTypes { SetIsDefault = 'SET_IS_DEFAULT', } -interface LoadDataSourcesAction { - type: ActionTypes.LoadDataSources; - payload: DataSourceSettings[]; -} +export const dataSourceLoaded = actionCreatorFactory(ActionTypes.LoadDataSource).create(); -interface SetDataSourcesSearchQueryAction { - type: ActionTypes.SetDataSourcesSearchQuery; - payload: string; -} +export const dataSourcesLoaded = actionCreatorFactory(ActionTypes.LoadDataSources).create(); -interface SetDataSourcesLayoutModeAction { - type: ActionTypes.SetDataSourcesLayoutMode; - payload: LayoutMode; -} +export const dataSourceMetaLoaded = actionCreatorFactory(ActionTypes.LoadDataSourceMeta).create(); -interface LoadDataSourceTypesAction { - type: ActionTypes.LoadDataSourceTypes; -} +export const dataSourceTypesLoad = actionCreatorFactory(ActionTypes.LoadDataSourceTypes).create(); -interface LoadedDataSourceTypesAction { - type: ActionTypes.LoadedDataSourceTypes; - payload: Plugin[]; -} +export const dataSourceTypesLoaded = actionCreatorFactory(ActionTypes.LoadedDataSourceTypes).create(); -interface SetDataSourceTypeSearchQueryAction { - type: ActionTypes.SetDataSourceTypeSearchQuery; - payload: string; -} +export const setDataSourcesSearchQuery = actionCreatorFactory(ActionTypes.SetDataSourcesSearchQuery).create(); -interface LoadDataSourceAction { - type: ActionTypes.LoadDataSource; - payload: DataSourceSettings; -} +export const setDataSourcesLayoutMode = actionCreatorFactory(ActionTypes.SetDataSourcesLayoutMode).create(); -interface LoadDataSourceMetaAction { - type: ActionTypes.LoadDataSourceMeta; - payload: Plugin; -} +export const setDataSourceTypeSearchQuery = actionCreatorFactory( + ActionTypes.SetDataSourceTypeSearchQuery +).create(); -interface SetDataSourceNameAction { - type: ActionTypes.SetDataSourceName; - payload: string; -} +export const setDataSourceName = actionCreatorFactory(ActionTypes.SetDataSourceName).create(); -interface SetIsDefaultAction { - type: ActionTypes.SetIsDefault; - payload: boolean; -} +export const setIsDefault = actionCreatorFactory(ActionTypes.SetIsDefault).create(); -const dataSourcesLoaded = (dataSources: DataSourceSettings[]): LoadDataSourcesAction => ({ - type: ActionTypes.LoadDataSources, - payload: dataSources, -}); - -const dataSourceLoaded = (dataSource: DataSourceSettings): LoadDataSourceAction => ({ - type: ActionTypes.LoadDataSource, - payload: dataSource, -}); - -const dataSourceMetaLoaded = (dataSourceMeta: Plugin): LoadDataSourceMetaAction => ({ - type: ActionTypes.LoadDataSourceMeta, - payload: dataSourceMeta, -}); - -const dataSourceTypesLoad = (): LoadDataSourceTypesAction => ({ - type: ActionTypes.LoadDataSourceTypes, -}); - -const dataSourceTypesLoaded = (dataSourceTypes: Plugin[]): LoadedDataSourceTypesAction => ({ - type: ActionTypes.LoadedDataSourceTypes, - payload: dataSourceTypes, -}); - -export const setDataSourcesSearchQuery = (searchQuery: string): SetDataSourcesSearchQueryAction => ({ - type: ActionTypes.SetDataSourcesSearchQuery, - payload: searchQuery, -}); - -export const setDataSourcesLayoutMode = (layoutMode: LayoutMode): SetDataSourcesLayoutModeAction => ({ - type: ActionTypes.SetDataSourcesLayoutMode, - payload: layoutMode, -}); - -export const setDataSourceTypeSearchQuery = (query: string): SetDataSourceTypeSearchQueryAction => ({ - type: ActionTypes.SetDataSourceTypeSearchQuery, - payload: query, -}); - -export const setDataSourceName = (name: string) => ({ - type: ActionTypes.SetDataSourceName, - payload: name, -}); - -export const setIsDefault = (state: boolean) => ({ - type: ActionTypes.SetIsDefault, - payload: state, -}); - -export type Action = - | LoadDataSourcesAction - | SetDataSourcesSearchQueryAction - | SetDataSourcesLayoutModeAction - | UpdateLocationAction - | LoadDataSourceTypesAction - | LoadedDataSourceTypesAction - | SetDataSourceTypeSearchQueryAction - | LoadDataSourceAction - | UpdateNavIndexAction - | LoadDataSourceMetaAction - | SetDataSourceNameAction - | SetIsDefaultAction; +export type Action = UpdateLocationAction | UpdateNavIndexAction | ActionOf; type ThunkResult = ThunkAction; export function loadDataSources(): ThunkResult { return async dispatch => { const response = await getBackendSrv().get('/api/datasources'); - dispatch(dataSourcesLoaded(response)); + dataSourcesLoaded(response); }; } @@ -177,7 +91,7 @@ export function addDataSource(plugin: Plugin): ThunkResult { export function loadDataSourceTypes(): ThunkResult { return async dispatch => { - dispatch(dataSourceTypesLoad()); + dispatch(dataSourceTypesLoad({})); const result = await getBackendSrv().get('/api/plugins', { enabled: 1, type: 'datasource' }); dispatch(dataSourceTypesLoaded(result)); }; diff --git a/public/app/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts index 66151990aea..68bc30201d6 100644 --- a/public/app/features/datasources/state/reducers.ts +++ b/public/app/features/datasources/state/reducers.ts @@ -1,56 +1,87 @@ import { DataSourcesState, Plugin } from 'app/types'; import { DataSourceSettings } from '@grafana/ui/src/types'; -import { Action, ActionTypes } from './actions'; +import { + dataSourceLoaded, + dataSourcesLoaded, + setDataSourcesSearchQuery, + setDataSourcesLayoutMode, + dataSourceTypesLoad, + dataSourceTypesLoaded, + setDataSourceTypeSearchQuery, + dataSourceMetaLoaded, + setDataSourceName, + setIsDefault, +} from './actions'; import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector'; +import { reducerFactory } from 'app/core/redux'; const initialState: DataSourcesState = { - dataSources: [] as DataSourceSettings[], + dataSources: [], dataSource: {} as DataSourceSettings, layoutMode: LayoutModes.List, searchQuery: '', dataSourcesCount: 0, - dataSourceTypes: [] as Plugin[], + dataSourceTypes: [], dataSourceTypeSearchQuery: '', hasFetched: false, isLoadingDataSources: false, dataSourceMeta: {} as Plugin, }; -export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => { - switch (action.type) { - case ActionTypes.LoadDataSources: - return { ...state, hasFetched: true, dataSources: action.payload, dataSourcesCount: action.payload.length }; - - case ActionTypes.LoadDataSource: - return { ...state, dataSource: action.payload }; - - case ActionTypes.SetDataSourcesSearchQuery: - return { ...state, searchQuery: action.payload }; - - case ActionTypes.SetDataSourcesLayoutMode: - return { ...state, layoutMode: action.payload }; - - case ActionTypes.LoadDataSourceTypes: - return { ...state, dataSourceTypes: [], isLoadingDataSources: true }; - - case ActionTypes.LoadedDataSourceTypes: - return { ...state, dataSourceTypes: action.payload, isLoadingDataSources: false }; - - case ActionTypes.SetDataSourceTypeSearchQuery: - return { ...state, dataSourceTypeSearchQuery: action.payload }; - - case ActionTypes.LoadDataSourceMeta: - return { ...state, dataSourceMeta: action.payload }; - - case ActionTypes.SetDataSourceName: - return { ...state, dataSource: { ...state.dataSource, name: action.payload } }; - - case ActionTypes.SetIsDefault: - return { ...state, dataSource: { ...state.dataSource, isDefault: action.payload } }; - } - - return state; -}; +export const dataSourcesReducer = reducerFactory(initialState) + .addHandler({ + filter: dataSourcesLoaded, + mapper: (state, action) => ({ + ...state, + hasFetched: true, + dataSources: action.payload, + dataSourcesCount: action.payload.length, + }), + }) + .addHandler({ + filter: dataSourceLoaded, + mapper: (state, action) => ({ ...state, dataSource: action.payload }), + }) + .addHandler({ + filter: setDataSourcesSearchQuery, + mapper: (state, action) => ({ ...state, searchQuery: action.payload }), + }) + .addHandler({ + filter: setDataSourcesLayoutMode, + mapper: (state, action) => ({ ...state, layoutMode: action.payload }), + }) + .addHandler({ + filter: dataSourceTypesLoad, + mapper: state => ({ ...state, dataSourceTypes: [], isLoadingDataSources: true }), + }) + .addHandler({ + filter: dataSourceTypesLoaded, + mapper: (state, action) => ({ + ...state, + dataSourceTypes: action.payload, + isLoadingDataSources: false, + }), + }) + .addHandler({ + filter: setDataSourceTypeSearchQuery, + mapper: (state, action) => ({ ...state, dataSourceTypeSearchQuery: action.payload }), + }) + .addHandler({ + filter: dataSourceMetaLoaded, + mapper: (state, action) => ({ ...state, dataSourceMeta: action.payload }), + }) + .addHandler({ + filter: setDataSourceName, + mapper: (state, action) => ({ ...state, dataSource: { ...state.dataSource, name: action.payload } }), + }) + .addHandler({ + filter: setIsDefault, + mapper: (state, action) => ({ + ...state, + dataSource: { ...state.dataSource, isDefault: action.payload }, + }), + }) + .create(); export default { dataSources: dataSourcesReducer, From 2f47b225a0d6259f4aa2a46daf962539c466dcd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 31 Jan 2019 06:38:40 +0100 Subject: [PATCH 07/11] Removed ActionTypes and fixed a noPayloadActionCreatorFactory --- .../core/redux/actionCreatorFactory.test.ts | 44 ++++++++++++++--- public/app/core/redux/actionCreatorFactory.ts | 23 +++++++++ public/app/core/redux/reducerFactory.test.ts | 6 +-- public/app/core/redux/reducerFactory.ts | 36 +++++++------- .../app/features/datasources/state/actions.ts | 49 ++++++++----------- .../features/datasources/state/reducers.ts | 20 ++++---- 6 files changed, 110 insertions(+), 68 deletions(-) diff --git a/public/app/core/redux/actionCreatorFactory.test.ts b/public/app/core/redux/actionCreatorFactory.test.ts index 96464354411..274079311b3 100644 --- a/public/app/core/redux/actionCreatorFactory.test.ts +++ b/public/app/core/redux/actionCreatorFactory.test.ts @@ -1,4 +1,8 @@ -import { actionCreatorFactory, resetAllActionCreatorTypes } from './actionCreatorFactory'; +import { + actionCreatorFactory, + resetAllActionCreatorTypes, + noPayloadActionCreatorFactory, +} from './actionCreatorFactory'; interface Dummy { n: number; @@ -11,15 +15,14 @@ interface Dummy { b: boolean; } -const setup = (payload: Dummy) => { +const setup = (payload?: Dummy) => { resetAllActionCreatorTypes(); const actionCreator = actionCreatorFactory('dummy').create(); + const noPayloadactionCreator = noPayloadActionCreatorFactory('NoPayload').create(); const result = actionCreator(payload); + const noPayloadResult = noPayloadactionCreator(); - return { - actionCreator, - result, - }; + return { actionCreator, noPayloadactionCreator, result, noPayloadResult }; }; describe('actionCreatorFactory', () => { @@ -46,7 +49,34 @@ describe('actionCreatorFactory', () => { setup(payload); expect(() => { - actionCreatorFactory('DuMmY').create(); + noPayloadActionCreatorFactory('DuMmY').create(); + }).toThrow(); + }); + }); +}); + +describe('noPayloadActionCreatorFactory', () => { + describe('when calling create', () => { + it('then it should create correct type string', () => { + const { noPayloadResult, noPayloadactionCreator } = setup(); + + expect(noPayloadactionCreator.type).toEqual('NoPayload'); + expect(noPayloadResult.type).toEqual('NoPayload'); + }); + + it('then it should create correct payload', () => { + const { noPayloadResult } = setup(); + + expect(noPayloadResult.payload).toBeUndefined(); + }); + }); + + describe('when calling create with existing type', () => { + it('then it should throw error', () => { + setup(); + + expect(() => { + actionCreatorFactory('nOpAyLoAd').create(); }).toThrow(); }); }); diff --git a/public/app/core/redux/actionCreatorFactory.ts b/public/app/core/redux/actionCreatorFactory.ts index fd12ac6ca36..d6477144df4 100644 --- a/public/app/core/redux/actionCreatorFactory.ts +++ b/public/app/core/redux/actionCreatorFactory.ts @@ -12,10 +12,19 @@ export interface ActionCreator { (payload: Payload): ActionOf; } +export interface NoPayloadActionCreator { + readonly type: string; + (): ActionOf; +} + export interface ActionCreatorFactory { create: () => ActionCreator; } +export interface NoPayloadActionCreatorFactory { + create: () => NoPayloadActionCreator; +} + export const actionCreatorFactory = (type: string): ActionCreatorFactory => { const create = (): ActionCreator => { return Object.assign((payload: Payload): ActionOf => ({ type, payload }), { type }); @@ -30,5 +39,19 @@ export const actionCreatorFactory = (type: string): ActionCreatorFactor return { create }; }; +export const noPayloadActionCreatorFactory = (type: string): NoPayloadActionCreatorFactory => { + const create = (): NoPayloadActionCreator => { + return Object.assign((): ActionOf => ({ type, payload: undefined }), { type }); + }; + + if (allActionCreators.some(t => (t && type ? t.toLocaleUpperCase() === type.toLocaleUpperCase() : false))) { + throw new Error(`There is already an actionCreator defined with the type ${type}`); + } + + allActionCreators.push(type); + + return { create }; +}; + // Should only be used by tests export const resetAllActionCreatorTypes = () => (allActionCreators.length = 0); diff --git a/public/app/core/redux/reducerFactory.test.ts b/public/app/core/redux/reducerFactory.test.ts index 5057b6d84b5..48cffc1ca7a 100644 --- a/public/app/core/redux/reducerFactory.test.ts +++ b/public/app/core/redux/reducerFactory.test.ts @@ -26,7 +26,7 @@ const dummyReducerIntialState: DummyReducerState = { const dummyActionCreator = actionCreatorFactory('dummy').create(); const dummyReducer = reducerFactory(dummyReducerIntialState) - .addHandler({ + .addMapper({ filter: dummyActionCreator, mapper: (state, action) => ({ ...state, ...action.payload }), }) @@ -76,7 +76,7 @@ describe('reducerFactory', () => { describe('given a handler is added', () => { describe('when a handler with the same creator is added', () => { it('then is should throw', () => { - const faultyReducer = reducerFactory(dummyReducerIntialState).addHandler({ + const faultyReducer = reducerFactory(dummyReducerIntialState).addMapper({ filter: dummyActionCreator, mapper: (state, action) => { return { ...state, ...action.payload }; @@ -84,7 +84,7 @@ describe('reducerFactory', () => { }); expect(() => { - faultyReducer.addHandler({ + faultyReducer.addMapper({ filter: dummyActionCreator, mapper: state => { return state; diff --git a/public/app/core/redux/reducerFactory.ts b/public/app/core/redux/reducerFactory.ts index 6858dd41879..c70d91e5e5f 100644 --- a/public/app/core/redux/reducerFactory.ts +++ b/public/app/core/redux/reducerFactory.ts @@ -1,47 +1,45 @@ import { ActionOf, ActionCreator } from './actionCreatorFactory'; +import { Reducer } from 'redux'; export type Mapper = (state: State, action: ActionOf) => State; -export interface HandlerConfig { +export interface MapperConfig { filter: ActionCreator; mapper: Mapper; } -export interface AddHandler { - addHandler: (config: HandlerConfig) => CreateReducer; +export interface AddMapper { + addMapper: (config: MapperConfig) => CreateReducer; } -export interface CreateReducer extends AddHandler { - create: () => Mapper; +export interface CreateReducer extends AddMapper { + create: () => Reducer>; } -export const reducerFactory = (initialState: State): AddHandler => { - const allHandlerConfigs: Array> = []; +export const reducerFactory = (initialState: State): AddMapper => { + const allMapperConfigs: Array> = []; - const addHandler = (config: HandlerConfig): CreateReducer => { - if (allHandlerConfigs.some(c => c.filter.type === config.filter.type)) { - throw new Error(`There is already a handlers defined with the type ${config.filter.type}`); + const addMapper = (config: MapperConfig): CreateReducer => { + if (allMapperConfigs.some(c => c.filter.type === config.filter.type)) { + throw new Error(`There is already a Mappers defined with the type ${config.filter.type}`); } - allHandlerConfigs.push(config); + allMapperConfigs.push(config); return instance; }; - const create = () => (state: State = initialState, action: ActionOf): State => { - const handlerConfig = allHandlerConfigs.filter(config => config.filter.type === action.type)[0]; + const create = (): Reducer> => (state: State = initialState, action: ActionOf): State => { + const mapperConfig = allMapperConfigs.filter(config => config.filter.type === action.type)[0]; - if (handlerConfig) { - return handlerConfig.mapper(state, action); + if (mapperConfig) { + return mapperConfig.mapper(state, action); } return state; }; - const instance: CreateReducer = { - addHandler, - create, - }; + const instance: CreateReducer = { addMapper, create }; return instance; }; diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 4cd61537cea..2e21b3066d1 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -9,51 +9,42 @@ import { buildNavModel } from './navModel'; import { DataSourceSettings } from '@grafana/ui/src/types'; import { Plugin, StoreState } from 'app/types'; import { actionCreatorFactory } from 'app/core/redux'; -import { ActionOf } from 'app/core/redux/actionCreatorFactory'; +import { ActionOf, noPayloadActionCreatorFactory } from 'app/core/redux/actionCreatorFactory'; -export enum ActionTypes { - LoadDataSources = 'LOAD_DATA_SOURCES', - LoadDataSourceTypes = 'LOAD_DATA_SOURCE_TYPES', - LoadedDataSourceTypes = 'LOADED_DATA_SOURCE_TYPES', - LoadDataSource = 'LOAD_DATA_SOURCE', - LoadDataSourceMeta = 'LOAD_DATA_SOURCE_META', - SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY', - SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE', - SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY', - SetDataSourceName = 'SET_DATA_SOURCE_NAME', - SetIsDefault = 'SET_IS_DEFAULT', -} +export const dataSourceLoaded = actionCreatorFactory('LOAD_DATA_SOURCE').create(); -export const dataSourceLoaded = actionCreatorFactory(ActionTypes.LoadDataSource).create(); +export const dataSourcesLoaded = actionCreatorFactory('LOAD_DATA_SOURCES').create(); -export const dataSourcesLoaded = actionCreatorFactory(ActionTypes.LoadDataSources).create(); +export const dataSourceMetaLoaded = actionCreatorFactory('LOAD_DATA_SOURCE_META').create(); -export const dataSourceMetaLoaded = actionCreatorFactory(ActionTypes.LoadDataSourceMeta).create(); +export const dataSourceTypesLoad = noPayloadActionCreatorFactory('LOAD_DATA_SOURCE_TYPES').create(); -export const dataSourceTypesLoad = actionCreatorFactory(ActionTypes.LoadDataSourceTypes).create(); +export const dataSourceTypesLoaded = actionCreatorFactory('LOADED_DATA_SOURCE_TYPES').create(); -export const dataSourceTypesLoaded = actionCreatorFactory(ActionTypes.LoadedDataSourceTypes).create(); +export const setDataSourcesSearchQuery = actionCreatorFactory('SET_DATA_SOURCES_SEARCH_QUERY').create(); -export const setDataSourcesSearchQuery = actionCreatorFactory(ActionTypes.SetDataSourcesSearchQuery).create(); +export const setDataSourcesLayoutMode = actionCreatorFactory('SET_DATA_SOURCES_LAYOUT_MODE').create(); -export const setDataSourcesLayoutMode = actionCreatorFactory(ActionTypes.SetDataSourcesLayoutMode).create(); +export const setDataSourceTypeSearchQuery = actionCreatorFactory('SET_DATA_SOURCE_TYPE_SEARCH_QUERY').create(); -export const setDataSourceTypeSearchQuery = actionCreatorFactory( - ActionTypes.SetDataSourceTypeSearchQuery -).create(); +export const setDataSourceName = actionCreatorFactory('SET_DATA_SOURCE_NAME').create(); -export const setDataSourceName = actionCreatorFactory(ActionTypes.SetDataSourceName).create(); +export const setIsDefault = actionCreatorFactory('SET_IS_DEFAULT').create(); -export const setIsDefault = actionCreatorFactory(ActionTypes.SetIsDefault).create(); - -export type Action = UpdateLocationAction | UpdateNavIndexAction | ActionOf; +export type Action = + | UpdateLocationAction + | UpdateNavIndexAction + | ActionOf + | ActionOf + | ActionOf + | ActionOf; type ThunkResult = ThunkAction; export function loadDataSources(): ThunkResult { return async dispatch => { const response = await getBackendSrv().get('/api/datasources'); - dataSourcesLoaded(response); + dispatch(dataSourcesLoaded(response)); }; } @@ -91,7 +82,7 @@ export function addDataSource(plugin: Plugin): ThunkResult { export function loadDataSourceTypes(): ThunkResult { return async dispatch => { - dispatch(dataSourceTypesLoad({})); + dispatch(dataSourceTypesLoad()); const result = await getBackendSrv().get('/api/plugins', { enabled: 1, type: 'datasource' }); dispatch(dataSourceTypesLoaded(result)); }; diff --git a/public/app/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts index 68bc30201d6..20aa58c8594 100644 --- a/public/app/features/datasources/state/reducers.ts +++ b/public/app/features/datasources/state/reducers.ts @@ -29,7 +29,7 @@ const initialState: DataSourcesState = { }; export const dataSourcesReducer = reducerFactory(initialState) - .addHandler({ + .addMapper({ filter: dataSourcesLoaded, mapper: (state, action) => ({ ...state, @@ -38,23 +38,23 @@ export const dataSourcesReducer = reducerFactory(initialState) dataSourcesCount: action.payload.length, }), }) - .addHandler({ + .addMapper({ filter: dataSourceLoaded, mapper: (state, action) => ({ ...state, dataSource: action.payload }), }) - .addHandler({ + .addMapper({ filter: setDataSourcesSearchQuery, mapper: (state, action) => ({ ...state, searchQuery: action.payload }), }) - .addHandler({ + .addMapper({ filter: setDataSourcesLayoutMode, mapper: (state, action) => ({ ...state, layoutMode: action.payload }), }) - .addHandler({ + .addMapper({ filter: dataSourceTypesLoad, mapper: state => ({ ...state, dataSourceTypes: [], isLoadingDataSources: true }), }) - .addHandler({ + .addMapper({ filter: dataSourceTypesLoaded, mapper: (state, action) => ({ ...state, @@ -62,19 +62,19 @@ export const dataSourcesReducer = reducerFactory(initialState) isLoadingDataSources: false, }), }) - .addHandler({ + .addMapper({ filter: setDataSourceTypeSearchQuery, mapper: (state, action) => ({ ...state, dataSourceTypeSearchQuery: action.payload }), }) - .addHandler({ + .addMapper({ filter: dataSourceMetaLoaded, mapper: (state, action) => ({ ...state, dataSourceMeta: action.payload }), }) - .addHandler({ + .addMapper({ filter: setDataSourceName, mapper: (state, action) => ({ ...state, dataSource: { ...state.dataSource, name: action.payload } }), }) - .addHandler({ + .addMapper({ filter: setIsDefault, mapper: (state, action) => ({ ...state, From 6a84a85a80f1a492a9da495b215e5bc0b268fa14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 31 Jan 2019 07:37:36 +0100 Subject: [PATCH 08/11] Added reducerTester, reducer tests and tests --- .../datasources/state/reducers.test.ts | 137 ++++++++++++++++++ .../features/datasources/state/reducers.ts | 2 +- public/test/core/redux/reducerTester.test.ts | 58 ++++++++ public/test/core/redux/reducerTester.ts | 79 ++++++++++ 4 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 public/app/features/datasources/state/reducers.test.ts create mode 100644 public/test/core/redux/reducerTester.test.ts create mode 100644 public/test/core/redux/reducerTester.ts diff --git a/public/app/features/datasources/state/reducers.test.ts b/public/app/features/datasources/state/reducers.test.ts new file mode 100644 index 00000000000..540089f3e65 --- /dev/null +++ b/public/app/features/datasources/state/reducers.test.ts @@ -0,0 +1,137 @@ +import { reducerTester } from 'test/core/redux/reducerTester'; +import { dataSourcesReducer, initialState } from './reducers'; +import { + dataSourcesLoaded, + dataSourceLoaded, + setDataSourcesSearchQuery, + setDataSourcesLayoutMode, + dataSourceTypesLoad, + dataSourceTypesLoaded, + setDataSourceTypeSearchQuery, + dataSourceMetaLoaded, + setDataSourceName, + setIsDefault, +} from './actions'; +import { getMockDataSources, getMockDataSource } from '../__mocks__/dataSourcesMocks'; +import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector'; +import { DataSourcesState } from 'app/types'; +import { PluginMetaInfo } from '@grafana/ui'; + +const mockPlugin = () => ({ + defaultNavUrl: 'defaultNavUrl', + enabled: true, + hasUpdate: true, + id: 'id', + info: {} as PluginMetaInfo, + latestVersion: 'latestVersion', + name: 'name', + pinned: true, + state: 'state', + type: 'type', + module: {}, +}); + +describe('dataSourcesReducer', () => { + describe('when dataSourcesLoaded is dispatched', () => { + it('then state should be correct', () => { + const dataSources = getMockDataSources(0); + + reducerTester() + .givenReducer(dataSourcesReducer, initialState) + .whenActionIsDispatched(dataSourcesLoaded(dataSources)) + .thenStateShouldEqual({ ...initialState, hasFetched: true, dataSources, dataSourcesCount: 1 }); + }); + }); + + describe('when dataSourceLoaded is dispatched', () => { + it('then state should be correct', () => { + const dataSource = getMockDataSource(); + + reducerTester() + .givenReducer(dataSourcesReducer, initialState) + .whenActionIsDispatched(dataSourceLoaded(dataSource)) + .thenStateShouldEqual({ ...initialState, dataSource }); + }); + }); + + describe('when setDataSourcesSearchQuery is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(dataSourcesReducer, initialState) + .whenActionIsDispatched(setDataSourcesSearchQuery('some query')) + .thenStateShouldEqual({ ...initialState, searchQuery: 'some query' }); + }); + }); + + describe('when setDataSourcesLayoutMode is dispatched', () => { + it('then state should be correct', () => { + const layoutMode: LayoutModes = LayoutModes.Grid; + + reducerTester() + .givenReducer(dataSourcesReducer, initialState) + .whenActionIsDispatched(setDataSourcesLayoutMode(layoutMode)) + .thenStateShouldEqual({ ...initialState, layoutMode: LayoutModes.Grid }); + }); + }); + + describe('when dataSourceTypesLoad is dispatched', () => { + it('then state should be correct', () => { + const state: DataSourcesState = { ...initialState, dataSourceTypes: [mockPlugin()] }; + + reducerTester() + .givenReducer(dataSourcesReducer, state) + .whenActionIsDispatched(dataSourceTypesLoad()) + .thenStateShouldEqual({ ...initialState, dataSourceTypes: [], isLoadingDataSources: true }); + }); + }); + + describe('when dataSourceTypesLoaded is dispatched', () => { + it('then state should be correct', () => { + const dataSourceTypes = [mockPlugin()]; + const state: DataSourcesState = { ...initialState, isLoadingDataSources: true }; + + reducerTester() + .givenReducer(dataSourcesReducer, state) + .whenActionIsDispatched(dataSourceTypesLoaded(dataSourceTypes)) + .thenStateShouldEqual({ ...initialState, dataSourceTypes, isLoadingDataSources: false }); + }); + }); + + describe('when setDataSourceTypeSearchQuery is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(dataSourcesReducer, initialState) + .whenActionIsDispatched(setDataSourceTypeSearchQuery('type search query')) + .thenStateShouldEqual({ ...initialState, dataSourceTypeSearchQuery: 'type search query' }); + }); + }); + + describe('when dataSourceMetaLoaded is dispatched', () => { + it('then state should be correct', () => { + const dataSourceMeta = mockPlugin(); + + reducerTester() + .givenReducer(dataSourcesReducer, initialState) + .whenActionIsDispatched(dataSourceMetaLoaded(dataSourceMeta)) + .thenStateShouldEqual({ ...initialState, dataSourceMeta }); + }); + }); + + describe('when setDataSourceName is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(dataSourcesReducer, initialState) + .whenActionIsDispatched(setDataSourceName('some name')) + .thenStateShouldEqual({ ...initialState, dataSource: { name: 'some name' } }); + }); + }); + + describe('when setIsDefault is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(dataSourcesReducer, initialState) + .whenActionIsDispatched(setIsDefault(true)) + .thenStateShouldEqual({ ...initialState, dataSource: { isDefault: true } }); + }); + }); +}); diff --git a/public/app/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts index 20aa58c8594..451e2d4650c 100644 --- a/public/app/features/datasources/state/reducers.ts +++ b/public/app/features/datasources/state/reducers.ts @@ -15,7 +15,7 @@ import { import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector'; import { reducerFactory } from 'app/core/redux'; -const initialState: DataSourcesState = { +export const initialState: DataSourcesState = { dataSources: [], dataSource: {} as DataSourceSettings, layoutMode: LayoutModes.List, diff --git a/public/test/core/redux/reducerTester.test.ts b/public/test/core/redux/reducerTester.test.ts new file mode 100644 index 00000000000..871feb7e848 --- /dev/null +++ b/public/test/core/redux/reducerTester.test.ts @@ -0,0 +1,58 @@ +import { reducerFactory, actionCreatorFactory } from 'app/core/redux'; +import { reducerTester } from './reducerTester'; + +interface DummyState { + data: string[]; +} + +const initialState: DummyState = { + data: [], +}; + +const dummyAction = actionCreatorFactory('dummyAction').create(); + +const mutatingReducer = reducerFactory(initialState) + .addMapper({ + filter: dummyAction, + mapper: (state, action) => { + state.data.push(action.payload); + return state; + }, + }) + .create(); + +const okReducer = reducerFactory(initialState) + .addMapper({ + filter: dummyAction, + mapper: (state, action) => { + return { + ...state, + data: state.data.concat(action.payload), + }; + }, + }) + .create(); + +describe('reducerTester', () => { + describe('when reducer mutates state', () => { + it('then it should throw', () => { + expect(() => { + reducerTester() + .givenReducer(mutatingReducer, initialState) + .whenActionIsDispatched(dummyAction('some string')) + .thenStateShouldEqual({ ...initialState, data: ['some string'] }); + }).toThrow(); + }); + }); + + describe('when reducer does not mutate state', () => { + it('then it should not throw', () => { + expect(() => { + reducerTester() + .givenReducer(okReducer, initialState) + .whenActionIsDispatched(dummyAction('some string')) + .thenStateShouldEqual({ ...initialState, data: ['some string'] }); + }).not.toThrow(); + }); + }); +}); diff --git a/public/test/core/redux/reducerTester.ts b/public/test/core/redux/reducerTester.ts new file mode 100644 index 00000000000..f8d8c921767 --- /dev/null +++ b/public/test/core/redux/reducerTester.ts @@ -0,0 +1,79 @@ +import { Reducer } from 'redux'; + +import { ActionOf } from 'app/core/redux/actionCreatorFactory'; + +export interface Given { + givenReducer: (reducer: Reducer>, state: State) => When; +} + +export interface When { + whenActionIsDispatched: (action: ActionOf) => Then; +} + +export interface Then { + thenStateShouldEqual: (state: State) => Then; +} + +interface ObjectType extends Object { + [key: string]: any; +} + +const deepFreeze = (obj: T): T => { + Object.freeze(obj); + + const isNotException = (object: any, propertyName: any) => + typeof object === 'function' + ? propertyName !== 'caller' && propertyName !== 'callee' && propertyName !== 'arguments' + : true; + const hasOwnProp = Object.prototype.hasOwnProperty; + + if (obj && obj instanceof Object) { + const object: ObjectType = obj; + Object.getOwnPropertyNames(object).forEach(propertyName => { + const objectProperty: any = object[propertyName]; + if ( + hasOwnProp.call(object, propertyName) && + isNotException(object, propertyName) && + objectProperty && + (typeof objectProperty === 'object' || typeof objectProperty === 'function') && + Object.isFrozen(objectProperty) === false + ) { + deepFreeze(objectProperty); + } + }); + } + + return obj; +}; + +interface ReducerTester extends Given, When, Then {} + +export const reducerTester = (): Given => { + let reducerUnderTest: Reducer> = null; + let resultingState: State = null; + let initialState: State = null; + + const givenReducer = (reducer: Reducer>, state: State): When => { + reducerUnderTest = reducer; + initialState = { ...state }; + initialState = deepFreeze(initialState); + + return instance; + }; + + const whenActionIsDispatched = (action: ActionOf): Then => { + resultingState = reducerUnderTest(initialState, action); + + return instance; + }; + + const thenStateShouldEqual = (state: State): Then => { + expect(state).toEqual(resultingState); + + return instance; + }; + + const instance: ReducerTester = { thenStateShouldEqual, givenReducer, whenActionIsDispatched }; + + return instance; +}; From 99b500c740c44d86a8451ab0b2009cdfe2d0744e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 31 Jan 2019 07:48:33 +0100 Subject: [PATCH 09/11] Removed then clauses, no need to test the test within the test --- public/test/core/redux/reducerTester.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/public/test/core/redux/reducerTester.test.ts b/public/test/core/redux/reducerTester.test.ts index 871feb7e848..63d163647fc 100644 --- a/public/test/core/redux/reducerTester.test.ts +++ b/public/test/core/redux/reducerTester.test.ts @@ -39,8 +39,7 @@ describe('reducerTester', () => { expect(() => { reducerTester() .givenReducer(mutatingReducer, initialState) - .whenActionIsDispatched(dummyAction('some string')) - .thenStateShouldEqual({ ...initialState, data: ['some string'] }); + .whenActionIsDispatched(dummyAction('some string')); }).toThrow(); }); }); @@ -50,8 +49,7 @@ describe('reducerTester', () => { expect(() => { reducerTester() .givenReducer(okReducer, initialState) - .whenActionIsDispatched(dummyAction('some string')) - .thenStateShouldEqual({ ...initialState, data: ['some string'] }); + .whenActionIsDispatched(dummyAction('some string')); }).not.toThrow(); }); }); From 7e64ee82252631a3ba5f9bb8bc3e557bd0bcb5e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 31 Jan 2019 09:18:40 +0100 Subject: [PATCH 10/11] Fixed another type of fluent reducerFactory --- public/app/core/redux/reducerFactory.ts | 94 ++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/public/app/core/redux/reducerFactory.ts b/public/app/core/redux/reducerFactory.ts index c70d91e5e5f..50eb6ccd4c6 100644 --- a/public/app/core/redux/reducerFactory.ts +++ b/public/app/core/redux/reducerFactory.ts @@ -1,4 +1,4 @@ -import { ActionOf, ActionCreator } from './actionCreatorFactory'; +import { ActionOf, ActionCreator, actionCreatorFactory } from './actionCreatorFactory'; import { Reducer } from 'redux'; export type Mapper = (state: State, action: ActionOf) => State; @@ -43,3 +43,95 @@ export const reducerFactory = (initialState: State): AddMapper => return instance; }; + +/** Another type of fluent reducerFactory */ + +export interface FilterWith { + filterWith: (actionCreator: ActionCreator) => MapTo; +} + +export interface OrFilterWith { + orFilterWith: (actionCreator: ActionCreator) => MapTo; +} + +export interface MapTo { + mapTo: (mapper: Mapper) => CreateReducerEx; +} + +export interface CreateReducerEx extends OrFilterWith { + create: () => Reducer>; +} + +export const reducerFactoryEx = (initialState: State): FilterWith => { + const allMapperConfigs: Array> = []; + + const innerMapTo = (actionCreator: ActionCreator, mapper: Mapper): CreateReducerEx => { + allMapperConfigs.filter(config => config.filter.type === actionCreator.type)[0].mapper = mapper; + + return instance; + }; + + const filterWith = (actionCreator: ActionCreator): MapTo => { + if (allMapperConfigs.some(c => c.filter.type === actionCreator.type)) { + throw new Error(`There is already a mapper defined with the type ${actionCreator.type}`); + } + + allMapperConfigs.push({ filter: actionCreator, mapper: null }); + + const mapTo = (mapper: Mapper): CreateReducerEx => { + innerMapTo(actionCreator, mapper); + + return instance; + }; + + return { mapTo }; + }; + + const orFilterWith = (actionCreator: ActionCreator): MapTo => { + if (allMapperConfigs.some(c => c.filter.type === actionCreator.type)) { + throw new Error(`There is already a mapper defined with the type ${actionCreator.type}`); + } + + allMapperConfigs.push({ filter: actionCreator, mapper: null }); + + const mapTo = (mapper: Mapper): CreateReducerEx => { + innerMapTo(actionCreator, mapper); + + return instance; + }; + + return { mapTo }; + }; + + const create = (): Reducer> => (state: State = initialState, action: ActionOf): State => { + const mapperConfig = allMapperConfigs.filter(config => config.filter.type === action.type)[0]; + + if (mapperConfig) { + return mapperConfig.mapper(state, action); + } + + return state; + }; + + const instance = { filterWith, orFilterWith, create }; + + return instance; +}; + +interface TestState { + data: string[]; +} + +const initialState: TestState = { + data: [], +}; + +const dummyActionCreator = actionCreatorFactory('dummyActionCreator').create(); +const dummyActionCreator2 = actionCreatorFactory('dummyActionCreator2').create(); + +export const reducerFactoryExReducer = reducerFactoryEx(initialState) + .filterWith(dummyActionCreator) + .mapTo((state, action) => ({ ...state, data: state.data.concat(action.payload) })) + .orFilterWith(dummyActionCreator2) + .mapTo((state, action) => ({ ...state, data: state.data.concat(`${action.payload}`) })) + .create(); From efec897fd4958bfab45c6e4ec202a31ce9950541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 1 Feb 2019 06:52:25 +0100 Subject: [PATCH 11/11] Removed unused factory and fixed index based mapper lookup --- public/app/core/redux/reducerFactory.ts | 108 ++---------------------- 1 file changed, 8 insertions(+), 100 deletions(-) diff --git a/public/app/core/redux/reducerFactory.ts b/public/app/core/redux/reducerFactory.ts index 50eb6ccd4c6..bfa8e67bd4c 100644 --- a/public/app/core/redux/reducerFactory.ts +++ b/public/app/core/redux/reducerFactory.ts @@ -1,4 +1,4 @@ -import { ActionOf, ActionCreator, actionCreatorFactory } from './actionCreatorFactory'; +import { ActionOf, ActionCreator } from './actionCreatorFactory'; import { Reducer } from 'redux'; export type Mapper = (state: State, action: ActionOf) => State; @@ -17,23 +17,23 @@ export interface CreateReducer extends AddMapper { } export const reducerFactory = (initialState: State): AddMapper => { - const allMapperConfigs: Array> = []; + const allMappers: { [key: string]: Mapper } = {}; const addMapper = (config: MapperConfig): CreateReducer => { - if (allMapperConfigs.some(c => c.filter.type === config.filter.type)) { - throw new Error(`There is already a Mappers defined with the type ${config.filter.type}`); + if (allMappers[config.filter.type]) { + throw new Error(`There is already a mapper defined with the type ${config.filter.type}`); } - allMapperConfigs.push(config); + allMappers[config.filter.type] = config.mapper; return instance; }; const create = (): Reducer> => (state: State = initialState, action: ActionOf): State => { - const mapperConfig = allMapperConfigs.filter(config => config.filter.type === action.type)[0]; + const mapper = allMappers[action.type]; - if (mapperConfig) { - return mapperConfig.mapper(state, action); + if (mapper) { + return mapper(state, action); } return state; @@ -43,95 +43,3 @@ export const reducerFactory = (initialState: State): AddMapper => return instance; }; - -/** Another type of fluent reducerFactory */ - -export interface FilterWith { - filterWith: (actionCreator: ActionCreator) => MapTo; -} - -export interface OrFilterWith { - orFilterWith: (actionCreator: ActionCreator) => MapTo; -} - -export interface MapTo { - mapTo: (mapper: Mapper) => CreateReducerEx; -} - -export interface CreateReducerEx extends OrFilterWith { - create: () => Reducer>; -} - -export const reducerFactoryEx = (initialState: State): FilterWith => { - const allMapperConfigs: Array> = []; - - const innerMapTo = (actionCreator: ActionCreator, mapper: Mapper): CreateReducerEx => { - allMapperConfigs.filter(config => config.filter.type === actionCreator.type)[0].mapper = mapper; - - return instance; - }; - - const filterWith = (actionCreator: ActionCreator): MapTo => { - if (allMapperConfigs.some(c => c.filter.type === actionCreator.type)) { - throw new Error(`There is already a mapper defined with the type ${actionCreator.type}`); - } - - allMapperConfigs.push({ filter: actionCreator, mapper: null }); - - const mapTo = (mapper: Mapper): CreateReducerEx => { - innerMapTo(actionCreator, mapper); - - return instance; - }; - - return { mapTo }; - }; - - const orFilterWith = (actionCreator: ActionCreator): MapTo => { - if (allMapperConfigs.some(c => c.filter.type === actionCreator.type)) { - throw new Error(`There is already a mapper defined with the type ${actionCreator.type}`); - } - - allMapperConfigs.push({ filter: actionCreator, mapper: null }); - - const mapTo = (mapper: Mapper): CreateReducerEx => { - innerMapTo(actionCreator, mapper); - - return instance; - }; - - return { mapTo }; - }; - - const create = (): Reducer> => (state: State = initialState, action: ActionOf): State => { - const mapperConfig = allMapperConfigs.filter(config => config.filter.type === action.type)[0]; - - if (mapperConfig) { - return mapperConfig.mapper(state, action); - } - - return state; - }; - - const instance = { filterWith, orFilterWith, create }; - - return instance; -}; - -interface TestState { - data: string[]; -} - -const initialState: TestState = { - data: [], -}; - -const dummyActionCreator = actionCreatorFactory('dummyActionCreator').create(); -const dummyActionCreator2 = actionCreatorFactory('dummyActionCreator2').create(); - -export const reducerFactoryExReducer = reducerFactoryEx(initialState) - .filterWith(dummyActionCreator) - .mapTo((state, action) => ({ ...state, data: state.data.concat(action.payload) })) - .orFilterWith(dummyActionCreator2) - .mapTo((state, action) => ({ ...state, data: state.data.concat(`${action.payload}`) })) - .create();