Refactor selectors and use them in place of a few spots that accessed… (#14658)

* Refactor selectors and use them in place of a few spots that accessed state directly

* Add some more action/reducer/selector tests

* Move jest tests out of __tests__ folder

Without a better idea, i put them in reducers folder, even though they
really span reducers, selectors and actions.  They can’t be in the
__tests__ folder or the mocha build breaks.

* rename state param into specific part of state tree, add jsdocs to make this clearer
This commit is contained in:
Stacey Gammon 2017-10-30 15:06:53 -04:00 committed by GitHub
parent a132bc6d39
commit 70cb006838
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 334 additions and 79 deletions

View file

@ -9,6 +9,12 @@ import { updateViewMode, updatePanels, updateIsFullScreenMode, minimizePanel } f
import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory';
import { createPanelState, getPersistedStateId } from './panel';
import { getAppStateDefaults } from './lib';
import {
getViewMode,
getFullScreenMode,
getPanels,
getPanel,
} from '../selectors';
/**
* Dashboard state manager handles connecting angular and redux state between the angular and react portions of the
@ -83,12 +89,13 @@ export class DashboardStateManager {
}
_areStoreAndAppStatePanelsEqual() {
const { dashboard } = store.getState();
const state = store.getState();
// We need to run this comparison check or we can enter an infinite loop.
let differencesFound = false;
for (let i = 0; i < this.appState.panels.length; i++) {
const appStatePanel = this.appState.panels[i];
if (!_.isEqual(appStatePanel, dashboard.panels[appStatePanel.panelIndex])) {
const storePanel = getPanel(state, appStatePanel.panelIndex);
if (!_.isEqual(appStatePanel, storePanel)) {
differencesFound = true;
break;
}
@ -108,12 +115,12 @@ export class DashboardStateManager {
store.dispatch(updatePanels(this.getPanels()));
}
const { dashboard } = store.getState();
if (dashboard.viewMode !== this.getViewMode()) {
const state = store.getState();
if (getViewMode(state) !== this.getViewMode()) {
store.dispatch(updateViewMode(this.getViewMode()));
}
if (dashboard.view.isFullScreenMode !== this.getFullScreenMode()) {
if (getFullScreenMode(state) !== this.getFullScreenMode()) {
store.dispatch(updateIsFullScreenMode(this.getFullScreenMode()));
}
}
@ -123,13 +130,14 @@ export class DashboardStateManager {
return;
}
const { dashboard } = store.getState();
const state = store.getState();
// The only state that the store deals with that appState cares about is the panels array. Every other state change
// (that appState cares about) is initiated from appState (e.g. view mode).
this.appState.panels = [];
_.map(dashboard.panels, panel => {
_.map(getPanels(state), panel => {
this.appState.panels.push(panel);
});
this.changeListeners.forEach(function (listener) {
return listener({ dirty: true, clean: false });
});

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import { DashboardGrid } from './dashboard_grid';
import { updatePanel } from '../actions';
import { getPanels, getViewMode } from '../reducers';
import { getPanels, getViewMode } from '../selectors';
const mapStateToProps = ({ dashboard }) => ({
panels: getPanels(dashboard),

View file

@ -20,7 +20,7 @@ import {
getEmbeddableEditUrl,
getMaximizedPanelId,
getEmbeddableError,
} from '../reducers';
} from '../selectors';
const mapStateToProps = ({ dashboard }, { panelId }) => {
const embeddable = getEmbeddable(dashboard, panelId);

View file

@ -22,7 +22,3 @@ export const embeddable = handleActions({
title: '',
editUrl: '',
});
export const getTitle = state => state.title;
export const getEditUrl = state => state.editUrl;
export const getError = state => state.error;

View file

@ -1,26 +1,33 @@
import { store } from '../../store';
import { embeddableRenderError, embeddableRenderFinished } from '../actions';
import { getEmbeddableError, getEmbeddableTitle } from '../reducers';
import { getDashboard } from '../../reducers';
import {
getEmbeddableError,
getEmbeddableTitle,
getEmbeddableEditUrl,
} from '../../selectors';
describe('embeddable reducers', () => {
test('embeddableRenderError stores an error on the embeddable', () => {
store.dispatch(embeddableRenderError('1', new Error('Opps, something bad happened!')));
test('embeddableRenderError stores an error on the embeddable', () => {
store.dispatch(embeddableRenderError('1', new Error('Opps, something bad happened!')));
const error = getEmbeddableError(getDashboard(store.getState()), '1');
expect(error.message).toBe('Opps, something bad happened!');
const error = getEmbeddableError(store.getState(), '1');
expect(error.message).toBe('Opps, something bad happened!');
});
describe('embeddableRenderFinished', () => {
test('stores a new embeddable object and clears the error', () => {
store.dispatch(embeddableRenderFinished('1', { title: 'My Embeddable' }));
const embeddableTitle = getEmbeddableTitle(store.getState(), '1');
expect(embeddableTitle).toBe('My Embeddable');
});
describe('embeddableRenderFinished', () => {
test('stores a new embeddable object and clears the error', () => {
store.dispatch(embeddableRenderFinished('1', { title: 'My Embeddable' }));
const embeddableTitle = getEmbeddableTitle(getDashboard(store.getState()), '1');
expect(embeddableTitle).toBe('My Embeddable');
});
test('and clears the error', () => {
const error = getEmbeddableError(store.getState(), '1');
expect(error).toBe(undefined);
});
test('and clears the error', () => {
const error = getEmbeddableError(getDashboard(store.getState()), '1');
expect(error).toBe(undefined);
});
test('getEmbeddableEditUrl', () => {
store.dispatch(embeddableRenderFinished('1', { title: 'My Embeddable', editUrl: 'vis/edit/me' }));
const url = getEmbeddableEditUrl(store.getState(), '1');
expect(url).toBe('vis/edit/me');
});
});

View file

@ -8,9 +8,6 @@ import {
import {
embeddable,
getTitle,
getEditUrl,
getError,
} from './embeddable';
export const embeddables = handleActions({
@ -25,8 +22,3 @@ export const embeddables = handleActions({
[action.payload.panelId]: embeddable(state[action.payload.panelId], action),
}),
}, {});
export const getEmbeddable = (state, panelId) => state[panelId];
export const getEmbeddableTitle = (state, panelId) => getTitle(getEmbeddable(state, panelId));
export const getEmbeddableEditUrl = (state, panelId) => getEditUrl(getEmbeddable(state, panelId));
export const getEmbeddableError = (state, panelId) => getError(getEmbeddable(state, panelId));

View file

@ -1,23 +1,14 @@
import { combineReducers } from 'redux';
import {
embeddables,
getEmbeddableTitle as getEmbeddableTitleFromEmbeddables,
getEmbeddableEditUrl as getEmbeddableEditUrlFromEmbeddables,
getEmbeddableError as getEmbeddableErrorFromEmbeddables,
getEmbeddable as getEmbeddableFromEmbeddables,
} from './embeddables';
import {
panels,
getPanel as getPanelFromPanels,
getPanelType as getPanelTypeFromPanels
} from './panels';
import {
view,
getViewMode as getViewModeFromView,
getFullScreenMode as getFullScreenModeFromView,
getMaximizedPanelId as getMaximizedPanelIdFromView
} from './view';
export const dashboard = combineReducers({
@ -25,18 +16,3 @@ export const dashboard = combineReducers({
panels,
embeddables,
});
export const getPanels = state => state.panels;
export const getPanel = (state, panelId) => getPanelFromPanels(getPanels(state), panelId);
export const getPanelType = (state, panelId) => getPanelTypeFromPanels(getPanels(state), panelId);
export const getEmbeddables = state => state.embeddables;
export const getEmbeddable = (state, panelId) => getEmbeddableFromEmbeddables(getEmbeddables(state), panelId);
export const getEmbeddableError = (state, panelId) => getEmbeddableErrorFromEmbeddables(getEmbeddables(state), panelId);
export const getEmbeddableTitle = (state, panelId) => getEmbeddableTitleFromEmbeddables(getEmbeddables(state), panelId);
export const getEmbeddableEditUrl = (state, panelId) => getEmbeddableEditUrlFromEmbeddables(getEmbeddables(state), panelId);
export const getView = state => state.view;
export const getViewMode = state => getViewModeFromView(getView(state));
export const getFullScreenMode = state => getFullScreenModeFromView(getView(state));
export const getMaximizedPanelId = state => getMaximizedPanelIdFromView(getView(state));

View file

@ -14,5 +14,3 @@ export const panel = handleActions({
version: undefined,
gridData: {}
});
export const getPanelType = state => state.type;

View file

@ -1,13 +1,16 @@
import { store } from '../../store';
import { updatePanel, updatePanels } from '../actions';
import { getPanel } from '../reducers';
import { getDashboard } from '../../reducers';
import {
getPanel,
getPanelType,
} from '../../selectors';
test('UpdatePanel updates a panel', () => {
store.dispatch(updatePanels([{ panelIndex: '1', gridData: {} }]));
store.dispatch(updatePanel({
panelIndex: '1',
type: 'mySpecialType',
gridData: {
x: 1,
y: 5,
@ -17,10 +20,14 @@ test('UpdatePanel updates a panel', () => {
}
}));
const panel = getPanel(getDashboard(store.getState()), '1');
const panel = getPanel(store.getState(), '1');
expect(panel.gridData.x).toBe(1);
expect(panel.gridData.y).toBe(5);
expect(panel.gridData.w).toBe(10);
expect(panel.gridData.h).toBe(1);
expect(panel.gridData.id).toBe('1');
});
test('getPanelType', () => {
expect(getPanelType(store.getState(), '1')).toBe('mySpecialType');
});

View file

@ -7,7 +7,7 @@ import {
updatePanels,
} from '../actions';
import { panel, getPanelType as getPanelTypeFromPanel } from './panel';
import { panel } from './panel';
export const panels = handleActions({
[updatePanels]: (state, { payload }) => _.cloneDeep(payload),
@ -23,6 +23,3 @@ export const panels = handleActions({
[action.payload.panelIndex]: panel(state[action.payload.panelIndex], action),
}),
}, {});
export const getPanel = (state, panelId) => state[panelId];
export const getPanelType = (state, panelId) => getPanelTypeFromPanel(getPanel(state, panelId));

View file

@ -22,8 +22,3 @@ export const view = handleActions({
viewMode: DashboardViewMode.VIEW,
maximizedPanelId: undefined
});
export const getViewMode = state => state.viewMode;
export const getFullScreenMode = state => state.isFullScreenMode;
export const getMaximizedPanelId = state => state.maximizedPanelId;

View file

@ -0,0 +1,57 @@
import { store } from '../../store';
import {
updateIsFullScreenMode,
updateViewMode,
maximizePanel,
minimizePanel,
} from '../actions';
import {
getFullScreenMode,
getViewMode,
getMaximizedPanelId,
} from '../../selectors';
import { DashboardViewMode } from '../dashboard_view_mode';
describe('isFullScreenMode', () => {
test('updates to true', () => {
store.dispatch(updateIsFullScreenMode(true));
const fullScreenMode = getFullScreenMode(store.getState());
expect(fullScreenMode).toBe(true);
});
test('updates to false', () => {
store.dispatch(updateIsFullScreenMode(false));
const fullScreenMode = getFullScreenMode(store.getState());
expect(fullScreenMode).toBe(false);
});
});
describe('viewMode', () => {
test('updates to EDIT', () => {
store.dispatch(updateViewMode(DashboardViewMode.EDIT));
const viewMode = getViewMode(store.getState());
expect(viewMode).toBe(DashboardViewMode.EDIT);
});
test('updates to VIEW', () => {
store.dispatch(updateViewMode(DashboardViewMode.VIEW));
const viewMode = getViewMode(store.getState());
expect(viewMode).toBe(DashboardViewMode.VIEW);
});
});
describe('maximizedPanelId', () => {
test('updates to an id when maximized', () => {
store.dispatch(maximizePanel('1'));
const maximizedId = getMaximizedPanelId(store.getState());
expect(maximizedId).toBe('1');
});
test('updates to an id when minimized', () => {
store.dispatch(minimizePanel());
const maximizedId = getMaximizedPanelId(store.getState());
expect(maximizedId).toBe(undefined);
});
});

View file

@ -0,0 +1,22 @@
/**
* @typedef {Object} EmbeddableState
* @property {string} title
* @property {string} editUrl
* @property {string|object} error
*/
/**
* @param embeddable {Embeddable}
* @return {string}
*/
export const getTitle = embeddable => embeddable.title;
/**
* @param embeddable {Embeddable}
* @return {string}
*/
export const getEditUrl = embeddable => embeddable.editUrl;
/**
* @param embeddable {Embeddable}
* @return {string}
*/
export const getError = embeddable => embeddable.error;

View file

@ -0,0 +1,34 @@
import {
getTitle,
getEditUrl,
getError,
} from './embeddable';
/**
* @typedef {Object.<string, EmbeddableState>} EmbeddablesState
*/
/**
* @param embeddables {EmbeddablesState}
* @param panelId {string}
* @return {Embeddable}
*/
export const getEmbeddable = (embeddables, panelId) => embeddables[panelId];
/**
* @param embeddables {EmbeddablesState}
* @param panelId {string}
* @return {string}
*/
export const getEmbeddableTitle = (embeddables, panelId) => getTitle(getEmbeddable(embeddables, panelId));
/**
* @param embeddables {EmbeddablesState}
* @param panelId {string}
* @return {string}
*/
export const getEmbeddableEditUrl = (embeddables, panelId) => getEditUrl(getEmbeddable(embeddables, panelId));
/**
* @param embeddables {EmbeddablesState}
* @param panelId {string}
* @return {string}
*/
export const getEmbeddableError = (embeddables, panelId) => getError(getEmbeddable(embeddables, panelId));

View file

@ -0,0 +1,96 @@
import {
getEmbeddableTitle as getEmbeddableTitleFromEmbeddables,
getEmbeddableEditUrl as getEmbeddableEditUrlFromEmbeddables,
getEmbeddableError as getEmbeddableErrorFromEmbeddables,
getEmbeddable as getEmbeddableFromEmbeddables,
} from './embeddables';
import {
getPanel as getPanelFromPanels,
getPanelType as getPanelTypeFromPanels
} from './panels';
import {
getViewMode as getViewModeFromView,
getFullScreenMode as getFullScreenModeFromView,
getMaximizedPanelId as getMaximizedPanelIdFromView
} from './view';
/**
* @typedef {Object} DashboardState
* @property {Object} PanelsState
* @property {Object} EmbeddablesState
* @property {Object} ViewState
*/
/**
* @param dashboard {DashboardState}
* @return {PanelsState}
*/
export const getPanels = dashboard => dashboard.panels;
/**
* @param dashboard {DashboardState}
* @param panelId {string}
* @return {PanelState}
*/
export const getPanel = (dashboard, panelId) => getPanelFromPanels(getPanels(dashboard), panelId);
/**
* @param dashboard {DashboardState}
* @param panelId {string}
* @return {string}
*/
export const getPanelType = (dashboard, panelId) => getPanelTypeFromPanels(getPanels(dashboard), panelId);
/**
* @param dashboard {DashboardState}
* @return {EmbeddablesState}
*/
export const getEmbeddables = dashboard => dashboard.embeddables;
/**
* @param dashboard {DashboardState}
* @param panelId {string}
* @return {EmbeddableState}
*/
export const getEmbeddable = (dashboard, panelId) => getEmbeddableFromEmbeddables(getEmbeddables(dashboard), panelId);
/**
* @param dashboard {DashboardState}
* @param panelId {string}
* @return {string|Object}
*/
export const getEmbeddableError =
(dashboard, panelId) => getEmbeddableErrorFromEmbeddables(getEmbeddables(dashboard), panelId);
/**
* @param dashboard {DashboardState}
* @param panelId {string}
* @return {string}
*/
export const getEmbeddableTitle =
(dashboard, panelId) => getEmbeddableTitleFromEmbeddables(getEmbeddables(dashboard), panelId);
/**
* @param dashboard {DashboardState}
* @param panelId {string}
* @return {string}
*/
export const getEmbeddableEditUrl =
(dashboard, panelId) => getEmbeddableEditUrlFromEmbeddables(getEmbeddables(dashboard), panelId);
/**
* @param dashboard {DashboardState}
* @return {ViewState}
*/
export const getView = dashboard => dashboard.view;
/**
* @param dashboard {DashboardState}
* @return {DashboardViewMode}
*/
export const getViewMode = dashboard => getViewModeFromView(getView(dashboard));
/**
* @param dashboard {DashboardState}
* @return {boolean}
*/
export const getFullScreenMode = dashboard => getFullScreenModeFromView(getView(dashboard));
/**
* @param dashboard {DashboardState}
* @return {string|undefined}
*/
export const getMaximizedPanelId = dashboard => getMaximizedPanelIdFromView(getView(dashboard));

View file

@ -0,0 +1,11 @@
/**
* NOTE: The PanelState jsdoc is defined in ../panel/panel_state. Right now whatever is stored on this tree is
* saved both to appstate and with the dashboard object. This coupling is subtle, fragile, and should be removed.
* TODO: make a function to translate the redux panel state into an object to be used for storage and/or appstate.
*/
/**
* @param panel {PanelState}
* @return {string}
*/
export const getPanelType = panel => panel.type;

View file

@ -0,0 +1,19 @@
import { getPanelType as getPanelTypeFromPanel } from './panel';
/**
* @typedef {Object.<string, PanelState>} PanelsState
*/
/**
* @param panels {PanelsState}
* @param panelId {string}
* @return {PanelState}
*/
export const getPanel = (panels, panelId) => panels[panelId];
/**
* @param panels {PanelsState}
* @param panelId {string}
* @return {string}
*/
export const getPanelType = (panels, panelId) => getPanelTypeFromPanel(getPanel(panels, panelId));

View file

@ -0,0 +1,22 @@
/**
* @typedef {Object} ViewState
* @property {DashboardViewMode} viewMode
* @property {boolean} isFullScreenMode
* @property {string|undefined} maximizedPanelId
*/
/**
* @param view {ViewState}
* @return {DashboardViewMode}
*/
export const getViewMode = view => view.viewMode;
/**
* @param view {ViewState}
* @return {boolean}
*/
export const getFullScreenMode = view => view.isFullScreenMode;
/**
* @param view {ViewState}
* @return {string|undefined}
*/
export const getMaximizedPanelId = view => view.maximizedPanelId;

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { DashboardViewport } from './dashboard_viewport';
import { getMaximizedPanelId, getPanels } from '../reducers';
import { getMaximizedPanelId, getPanels } from '../selectors';
const mapStateToProps = ({ dashboard }) => {
const maximizedPanelId = getMaximizedPanelId(dashboard);

View file

@ -8,5 +8,3 @@ import { dashboard } from './dashboard/reducers';
export const reducers = combineReducers({
dashboard
});
export const getDashboard = state => state.dashboard;

View file

@ -0,0 +1,19 @@
import * as DashboardSelectors from '../dashboard/selectors';
export const getDashboard = state => state.dashboard;
export const getPanels = state => DashboardSelectors.getPanels(getDashboard(state));
export const getPanel = (state, panelId) => DashboardSelectors.getPanel(getDashboard(state), panelId);
export const getPanelType = (state, panelId) => DashboardSelectors.getPanelType(getDashboard(state), panelId);
export const getEmbeddables = state => DashboardSelectors.getEmbeddables(getDashboard(state));
export const getEmbeddable = (state, panelId) => DashboardSelectors.getEmbeddable(getDashboard(state), panelId);
export const getEmbeddableError = (state, panelId) =>
DashboardSelectors.getEmbeddableError((getDashboard(state)), panelId);
export const getEmbeddableTitle = (state, panelId) => DashboardSelectors.getEmbeddableTitle(getDashboard(state), panelId);
export const getEmbeddableEditUrl = (state, panelId) => DashboardSelectors.getEmbeddableEditUrl(getDashboard(state), panelId);
export const getView = state => state.view;
export const getViewMode = state => DashboardSelectors.getViewMode(getDashboard(state));
export const getFullScreenMode = state => DashboardSelectors.getFullScreenMode(getDashboard(state));
export const getMaximizedPanelId = state => DashboardSelectors.getMaximizedPanelId(getDashboard(state));

View file

@ -0,0 +1 @@
export * from './dashboard_selectors';