From dcfb60472b80faea9060571d4f2cc8f803e52a95 Mon Sep 17 00:00:00 2001 From: vera-liu Date: Wed, 21 Sep 2016 11:38:07 -0700 Subject: [PATCH] Created store and reducers (#1108) * Created store and reducers * Added spec * Modifications based on comments --- caravel/assets/javascripts/SqlLab/index.jsx | 10 +- caravel/assets/javascripts/SqlLab/reducers.js | 61 +-------- .../explorev2/actions/exploreActions.js | 71 ++++++++++ .../explorev2/components/ChartContainer.jsx | 17 ++- .../components/ControlPanelsContainer.jsx | 17 ++- .../components/ExploreViewContainer.jsx | 38 +++--- .../assets/javascripts/explorev2/index.jsx | 22 +++- .../explorev2/reducers/exploreReducer.js | 65 ++++++++++ .../javascripts/explorev2/stores/store.js | 32 +++++ caravel/assets/package.json | 2 +- .../explore/components/actions_spec.js | 121 ++++++++++++++++++ caravel/assets/utils/common.js | 13 ++ caravel/assets/utils/reducerUtils.js | 53 ++++++++ 13 files changed, 419 insertions(+), 103 deletions(-) create mode 100644 caravel/assets/javascripts/explorev2/actions/exploreActions.js create mode 100644 caravel/assets/javascripts/explorev2/reducers/exploreReducer.js create mode 100644 caravel/assets/javascripts/explorev2/stores/store.js create mode 100644 caravel/assets/spec/javascripts/explore/components/actions_spec.js create mode 100644 caravel/assets/utils/reducerUtils.js diff --git a/caravel/assets/javascripts/SqlLab/index.jsx b/caravel/assets/javascripts/SqlLab/index.jsx index 6c9bd25e8e810..e0a7fa18c0c2b 100644 --- a/caravel/assets/javascripts/SqlLab/index.jsx +++ b/caravel/assets/javascripts/SqlLab/index.jsx @@ -10,20 +10,14 @@ import TabbedSqlEditors from './components/TabbedSqlEditors'; import QueryAutoRefresh from './components/QueryAutoRefresh'; import Alerts from './components/Alerts'; -import { bindActionCreators, compose, createStore } from 'redux'; +import { bindActionCreators, createStore } from 'redux'; import { connect, Provider } from 'react-redux'; import { initialState, sqlLabReducer } from './reducers'; -import persistState from 'redux-localstorage'; +import { enhancer } from '../../utils/common'; require('./main.css'); -let enhancer = compose(persistState()); -if (process.env.NODE_ENV === 'dev') { - enhancer = compose( - persistState(), window.devToolsExtension && window.devToolsExtension() - ); -} let store = createStore(sqlLabReducer, initialState, enhancer); // jquery hack to highlight the navbar menu diff --git a/caravel/assets/javascripts/SqlLab/reducers.js b/caravel/assets/javascripts/SqlLab/reducers.js index dcdeac9c47428..5dce1877a4f0b 100644 --- a/caravel/assets/javascripts/SqlLab/reducers.js +++ b/caravel/assets/javascripts/SqlLab/reducers.js @@ -1,6 +1,9 @@ import shortid from 'shortid'; import * as actions from './actions'; import { now } from '../modules/dates'; +import + { addToArr, alterInArr, removeFromArr, addToObject, alterInObject } + from '../../utils/reducerUtils'; const defaultQueryEditor = { id: shortid.generate(), @@ -23,58 +26,6 @@ export const initialState = { queriesLastUpdate: 0, }; -function addToObject(state, arrKey, obj) { - const newObject = Object.assign({}, state[arrKey]); - const copiedObject = Object.assign({}, obj); - - if (!copiedObject.id) { - copiedObject.id = shortid.generate(); - } - newObject[copiedObject.id] = copiedObject; - return Object.assign({}, state, { [arrKey]: newObject }); -} - -function alterInObject(state, arrKey, obj, alterations) { - const newObject = Object.assign({}, state[arrKey]); - newObject[obj.id] = (Object.assign({}, newObject[obj.id], alterations)); - return Object.assign({}, state, { [arrKey]: newObject }); -} - -function alterInArr(state, arrKey, obj, alterations) { - // Finds an item in an array in the state and replaces it with a - // new object with an altered property - const idKey = 'id'; - const newArr = []; - state[arrKey].forEach((arrItem) => { - if (obj[idKey] === arrItem[idKey]) { - newArr.push(Object.assign({}, arrItem, alterations)); - } else { - newArr.push(arrItem); - } - }); - return Object.assign({}, state, { [arrKey]: newArr }); -} - -function removeFromArr(state, arrKey, obj, idKey = 'id') { - const newArr = []; - state[arrKey].forEach((arrItem) => { - if (!(obj[idKey] === arrItem[idKey])) { - newArr.push(arrItem); - } - }); - return Object.assign({}, state, { [arrKey]: newArr }); -} - -function addToArr(state, arrKey, obj) { - const newObj = Object.assign({}, obj); - if (!newObj.id) { - newObj.id = shortid.generate(); - } - const newState = {}; - newState[arrKey] = [...state[arrKey], newObj]; - return Object.assign({}, state, newState); -} - export const sqlLabReducer = function (state, action) { const actionHandlers = { [actions.ADD_QUERY_EDITOR]() { @@ -115,7 +66,8 @@ export const sqlLabReducer = function (state, action) { [actions.START_QUERY]() { const newState = addToObject(state, 'queries', action.query); const sqlEditor = { id: action.query.sqlEditorId }; - return alterInArr(newState, 'queryEditors', sqlEditor, { latestQueryId: action.query.id }); + return alterInArr( + newState, 'queryEditors', sqlEditor, { latestQueryId: action.query.id }); }, [actions.STOP_QUERY]() { return alterInObject(state, 'queries', action.query, { state: 'stopped' }); @@ -156,7 +108,8 @@ export const sqlLabReducer = function (state, action) { return alterInArr(state, 'queryEditors', action.queryEditor, { sql: action.sql }); }, [actions.QUERY_EDITOR_SET_AUTORUN]() { - return alterInArr(state, 'queryEditors', action.queryEditor, { autorun: action.autorun }); + return alterInArr( + state, 'queryEditors', action.queryEditor, { autorun: action.autorun }); }, [actions.ADD_WORKSPACE_QUERY]() { return addToArr(state, 'workspaceQueries', action.query); diff --git a/caravel/assets/javascripts/explorev2/actions/exploreActions.js b/caravel/assets/javascripts/explorev2/actions/exploreActions.js new file mode 100644 index 0000000000000..52aeeb278b0a7 --- /dev/null +++ b/caravel/assets/javascripts/explorev2/actions/exploreActions.js @@ -0,0 +1,71 @@ +export const SET_DATASOURCE = 'SET_DATASOURCE'; +export const SET_VIZTYPE = 'SET_VIZTYPE'; +export const SET_TIME_FILTER = 'SET_TIME_FILTER'; +export const SET_GROUPBY = 'SET_GROUPBY'; +export const ADD_COLUMN = 'ADD_COLUMN'; +export const REMOVE_COLUMN = 'REMOVE_COLUMN'; +export const ADD_ORDERING = 'ADD_ORDERING'; +export const REMOVE_ORDERING = 'REMOVE_ORDERING'; +export const SET_TIME_STAMP = 'SET_TIME_STAMP'; +export const SET_ROW_LIMIT = 'SET_ROW_LIMIT'; +export const TOGGLE_SEARCHBOX = 'TOGGLE_SEARCHBOX'; +export const SET_SQL = 'SET_SQL'; +export const ADD_FILTER = 'ADD_FILTER'; +export const SET_FILTER = 'SET_FILTER'; +export const REMOVE_FILTER = 'REMOVE_FILTER'; + +export function setDatasource(datasourceId) { + return { type: SET_DATASOURCE, datasourceId }; +} + +export function setVizType(vizType) { + return { type: SET_VIZTYPE, vizType }; +} + +export function setTimeFilter(timeFilter) { + return { type: SET_TIME_FILTER, timeFilter }; +} + +export function setGroupBy(groupBy) { + return { type: SET_GROUPBY, groupBy }; +} + +export function addColumn(column) { + return { type: ADD_COLUMN, column }; +} + +export function removeColumn(column) { + return { type: REMOVE_COLUMN, column }; +} + +export function addOrdering(ordering) { + return { type: ADD_ORDERING, ordering }; +} + +export function removeOrdering(ordering) { + return { type: REMOVE_ORDERING, ordering }; +} + +export function setTimeStamp(timeStampFormat) { + return { type: SET_TIME_STAMP, timeStampFormat }; +} + +export function setRowLimit(rowLimit) { + return { type: SET_ROW_LIMIT, rowLimit }; +} + +export function toggleSearchBox(searchBox) { + return { type: TOGGLE_SEARCHBOX, searchBox }; +} + +export function setSQL(sql) { + return { type: SET_SQL, sql }; +} + +export function addFilter(filter) { + return { type: ADD_FILTER, filter }; +} + +export function removeFilter(filter) { + return { type: REMOVE_FILTER, filter }; +} diff --git a/caravel/assets/javascripts/explorev2/components/ChartContainer.jsx b/caravel/assets/javascripts/explorev2/components/ChartContainer.jsx index 9915264dc7a53..3666e923a201a 100644 --- a/caravel/assets/javascripts/explorev2/components/ChartContainer.jsx +++ b/caravel/assets/javascripts/explorev2/components/ChartContainer.jsx @@ -1,12 +1,11 @@ import React from 'react'; import { Panel } from 'react-bootstrap'; -export default class ChartContainer extends React.Component { - render() { - return ( - - chart goes here - - ); - } -} +const ChartContainer = function () { + return ( + + chart goes here + + ); +}; +export default ChartContainer; diff --git a/caravel/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx b/caravel/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx index b0e7efa2627bd..20b8b34075a89 100644 --- a/caravel/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx +++ b/caravel/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx @@ -1,12 +1,11 @@ import React from 'react'; import { Panel } from 'react-bootstrap'; -export default class ControlPanelsContainer extends React.Component { - render() { - return ( - - control panels here - - ); - } -} +const ControlPanelsContainer = function () { + return ( + + control panels here + + ); +}; +export default ControlPanelsContainer; diff --git a/caravel/assets/javascripts/explorev2/components/ExploreViewContainer.jsx b/caravel/assets/javascripts/explorev2/components/ExploreViewContainer.jsx index 8d7e4f26cffa3..f6b6b52faae3e 100644 --- a/caravel/assets/javascripts/explorev2/components/ExploreViewContainer.jsx +++ b/caravel/assets/javascripts/explorev2/components/ExploreViewContainer.jsx @@ -3,24 +3,24 @@ import ChartContainer from './ChartContainer'; import ControlPanelsContainer from './ControlPanelsContainer'; import QueryAndSaveButtons from './QueryAndSaveButtons'; -export default class ExploreViewContainer extends React.Component { - render() { - return ( -
-
-
- { console.log('clicked query') }} - /> -

- -
-
- -
+const ExploreViewContainer = function () { + return ( +
+
+
+ { console.log('clicked query'); }} + /> +

+ +
+
+
- ); - } -} +
+ ); +}; + +export default ExploreViewContainer; diff --git a/caravel/assets/javascripts/explorev2/index.jsx b/caravel/assets/javascripts/explorev2/index.jsx index 2ca33b10be5e6..53e8a64035f45 100644 --- a/caravel/assets/javascripts/explorev2/index.jsx +++ b/caravel/assets/javascripts/explorev2/index.jsx @@ -2,12 +2,28 @@ import React from 'react'; import ReactDOM from 'react-dom'; import ExploreViewContainer from './components/ExploreViewContainer'; +import { createStore } from 'redux'; +import { Provider } from 'react-redux'; +import { enhancer } from '../../utils/common'; + +import { initialState } from './stores/store'; + const exploreViewContainer = document.getElementById('js-explore-view-container'); const bootstrapData = exploreViewContainer.getAttribute('data-bootstrap'); +import { exploreReducer } from './reducers/exploreReducer'; + +const bootstrappedState = Object.assign(initialState, { + datasources: bootstrapData.datasources, + viz: bootstrapData.viz, +}); +const store = createStore(exploreReducer, bootstrappedState, enhancer); + ReactDOM.render( - , + + + , exploreViewContainer ); diff --git a/caravel/assets/javascripts/explorev2/reducers/exploreReducer.js b/caravel/assets/javascripts/explorev2/reducers/exploreReducer.js new file mode 100644 index 0000000000000..e7c0de6d49a8e --- /dev/null +++ b/caravel/assets/javascripts/explorev2/reducers/exploreReducer.js @@ -0,0 +1,65 @@ +import * as actions from '../actions/exploreActions'; +import { addToArr, removeFromArr } from '../../../utils/reducerUtils'; + +export const exploreReducer = function (state, action) { + const actionHandlers = { + [actions.SET_DATASOURCE]() { + return Object.assign({}, state, { datasourceId: action.datasourceId }); + }, + [actions.SET_VIZTYPE]() { + return Object.assign({}, state, { vizType: action.vizType }); + }, + [actions.SET_TIME_FILTER]() { + return Object.assign({}, state, { timeFilter: action.timeFilter }); + }, + [actions.SET_GROUPBY]() { + return Object.assign({}, state, { groupBy: action.groupBy }); + }, + [actions.ADD_COLUMN]() { + return Object.assign({}, state, { columns: [...state.columns, action.column] }); + }, + [actions.REMOVE_COLUMN]() { + const newColumns = []; + state.columns.forEach((c) => { + if (c !== action.column) { + newColumns.push(c); + } + }); + return Object.assign({}, state, { columns: newColumns }); + }, + [actions.ADD_ORDERING]() { + return Object.assign({}, state, { orderings: [...state.orderings, action.ordering] }); + }, + [actions.REMOVE_ORDERING]() { + const newOrderings = []; + state.orderings.forEach((o) => { + if (o !== action.ordering) { + newOrderings.push(o); + } + }); + return Object.assign({}, state, { orderings: newOrderings }); + }, + [actions.SET_TIME_STAMP]() { + return Object.assign({}, state, { timeStampFormat: action.timeStampFormat }); + }, + [actions.SET_ROW_LIMIT]() { + return Object.assign({}, state, { rowLimit: action.rowLimit }); + }, + [actions.TOGGLE_SEARCHBOX]() { + return Object.assign({}, state, { searchBox: action.searchBox }); + }, + [actions.SET_SQL]() { + return Object.assign({}, state, { SQL: action.sql }); + }, + [actions.ADD_FILTER]() { + return addToArr(state, 'filters', action.filter); + }, + [actions.REMOVE_FILTER]() { + return removeFromArr(state, 'filters', action.filter); + }, + }; + if (action.type in actionHandlers) { + return actionHandlers[action.type](); + } + return state; +}; diff --git a/caravel/assets/javascripts/explorev2/stores/store.js b/caravel/assets/javascripts/explorev2/stores/store.js new file mode 100644 index 0000000000000..5dcfd2d387a9e --- /dev/null +++ b/caravel/assets/javascripts/explorev2/stores/store.js @@ -0,0 +1,32 @@ +const defaultTimeFilter = { + timeColumn: null, + timeGrain: null, + since: null, + until: null, +}; + +const defaultGroupBy = { + groupByColumn: [], + metrics: [], +}; + +const defaultSql = { + where: '', + having: '', +}; + +export const initialState = { + datasources: null, + datasourceId: null, + viz: null, + vizType: null, + timeFilter: defaultTimeFilter, + groupBy: defaultGroupBy, + columns: [], + orderings: [], + timeStampFormat: null, + rowLimit: null, + searchBox: false, + SQL: defaultSql, + filters: [], +}; diff --git a/caravel/assets/package.json b/caravel/assets/package.json index 321def76482ad..ceea555445880 100644 --- a/caravel/assets/package.json +++ b/caravel/assets/package.json @@ -101,9 +101,9 @@ "eslint-plugin-jsx-a11y": "^2.0.1", "eslint-plugin-react": "^5.2.2", "exports-loader": "^0.6.3", - "istanbul": "^1.0.0-alpha", "file-loader": "^0.8.5", "imports-loader": "^0.6.5", + "istanbul": "^1.0.0-alpha", "jsdom": "^8.0.1", "json-loader": "^0.5.4", "less": "^2.6.1", diff --git a/caravel/assets/spec/javascripts/explore/components/actions_spec.js b/caravel/assets/spec/javascripts/explore/components/actions_spec.js new file mode 100644 index 0000000000000..eebfbb144142e --- /dev/null +++ b/caravel/assets/spec/javascripts/explore/components/actions_spec.js @@ -0,0 +1,121 @@ +import { expect } from 'chai'; +import shortid from 'shortid'; +import * as actions from '../../../../javascripts/explorev2/actions/exploreActions'; +import { initialState } from '../../../../javascripts/explorev2/stores/store'; +import { exploreReducer } from '../../../../javascripts/explorev2/reducers/exploreReducer'; + +describe('reducers', () => { + + it('should return new state with datasource id', () => { + const newState = exploreReducer(initialState, actions.setDatasource(1)); + expect(newState.datasourceId).to.equal(1); + }); + + it('should return new state with viz type', () => { + const newState = exploreReducer(initialState, actions.setVizType('bar')); + expect(newState.vizType).to.equal('bar'); + }); + + it('should return new state with time filter', () => { + const newTimeFilter = { + timeColumn: 1, + timeGrain: 1, + since: 1, + until: 2, + }; + const newState = exploreReducer(initialState, actions.setTimeFilter(newTimeFilter)); + expect(newState.timeFilter).to.deep.equal(newTimeFilter); + }); + + it('should return new state with group by', () => { + const newGroupBy = { + groupByColumn: ['col1'], + metrics: ['sum_value'], + }; + const newState = exploreReducer(initialState, actions.setGroupBy(newGroupBy)); + expect(newState.groupBy).to.deep.equal(newGroupBy); + }); + + it('should return new state with added column', () => { + const newColumn = 'col'; + const newState = exploreReducer(initialState, actions.addColumn(newColumn)); + expect(newState.columns).to.deep.equal([newColumn]); + }); + + it('should return new state with removed column', () => { + const testState = { initialState, columns: ['col1', 'col2'] }; + const remColumn = 'col1'; + const newState = exploreReducer(testState, actions.removeColumn(remColumn)); + expect(newState.columns).to.deep.equal(['col2']); + }); + + it('should return new state with added ordering', () => { + const newOrdering = 'ord'; + const newState = exploreReducer(initialState, actions.addOrdering(newOrdering)); + expect(newState.orderings).to.deep.equal(['ord']); + }); + + it('should return new state with removed ordering', () => { + const testState = { initialState, orderings: ['ord1', 'ord2'] }; + const remOrdering = 'ord1'; + const newState = exploreReducer(testState, actions.removeOrdering(remOrdering)); + expect(newState.orderings).to.deep.equal(['ord2']); + }); + + it('should return new state with time stamp', () => { + const newState = exploreReducer(initialState, actions.setTimeStamp(1)); + expect(newState.timeStampFormat).to.equal(1); + }); + + it('should return new state with row limit', () => { + const newState = exploreReducer(initialState, actions.setRowLimit(10)); + expect(newState.rowLimit).to.equal(10); + }); + + it('should return new state with search box toggled', () => { + const newState = exploreReducer(initialState, actions.toggleSearchBox(true)); + expect(newState.searchBox).to.equal(true); + }); + + it('should return new state with new sql', () => { + const newSql = { + where: 'where clause', + having: 'having clause', + }; + const newState = exploreReducer(initialState, actions.setSQL(newSql)); + expect(newState.SQL).to.equal(newSql); + }); + + it('should return new state with added filter', () => { + const newFilter = { + id: shortid.generate(), + eq: 'value', + op: 'in', + col: 'vals', + }; + const newState = exploreReducer(initialState, actions.addFilter(newFilter)); + expect(newState.filters).to.deep.equal([newFilter]); + }); + + it('should return new state with removed filter', () => { + const filter1 = { + id: shortid.generate(), + eq: 'value', + op: 'in', + col: 'vals1', + }; + const filter2 = { + id: shortid.generate(), + eq: 'value', + op: 'not in', + col: 'vals2', + }; + const testState = { + initialState, + filters: [filter1, filter2], + }; + const newState = exploreReducer(testState, actions.removeFilter(filter1)); + expect(newState.filters).to.deep.equal([filter2]); + }); + +}); diff --git a/caravel/assets/utils/common.js b/caravel/assets/utils/common.js index a6aa001e41d80..5e98b996bfb5a 100644 --- a/caravel/assets/utils/common.js +++ b/caravel/assets/utils/common.js @@ -1,4 +1,7 @@ /* eslint global-require: 0 */ +import persistState from 'redux-localstorage'; +import { compose } from 'redux'; + const d3 = window.d3 || require('d3'); export const EARTH_CIRCUMFERENCE_KM = 40075.16; @@ -26,3 +29,13 @@ export function rgbLuminance(r, g, b) { // Formula: https://en.wikipedia.org/wiki/Relative_luminance return (LUMINANCE_RED_WEIGHT * r) + (LUMINANCE_GREEN_WEIGHT * g) + (LUMINANCE_BLUE_WEIGHT * b); } + +export function getDevEnhancer() { + let enhancer = compose(persistState()); + if (process.env.NODE_ENV === 'dev') { + enhancer = compose( + persistState(), window.devToolsExtension && window.devToolsExtension() + ); + } + return enhancer; +} diff --git a/caravel/assets/utils/reducerUtils.js b/caravel/assets/utils/reducerUtils.js new file mode 100644 index 0000000000000..e233c2413b9a0 --- /dev/null +++ b/caravel/assets/utils/reducerUtils.js @@ -0,0 +1,53 @@ +import shortid from 'shortid'; + +export function addToObject(state, arrKey, obj) { + const newObject = Object.assign({}, state[arrKey]); + const copiedObject = Object.assign({}, obj); + + if (!copiedObject.id) { + copiedObject.id = shortid.generate(); + } + newObject[copiedObject.id] = copiedObject; + return Object.assign({}, state, { [arrKey]: newObject }); +} + +export function alterInObject(state, arrKey, obj, alterations) { + const newObject = Object.assign({}, state[arrKey]); + newObject[obj.id] = (Object.assign({}, newObject[obj.id], alterations)); + return Object.assign({}, state, { [arrKey]: newObject }); +} + +export function alterInArr(state, arrKey, obj, alterations) { + // Finds an item in an array in the state and replaces it with a + // new object with an altered property + const idKey = 'id'; + const newArr = []; + state[arrKey].forEach((arrItem) => { + if (obj[idKey] === arrItem[idKey]) { + newArr.push(Object.assign({}, arrItem, alterations)); + } else { + newArr.push(arrItem); + } + }); + return Object.assign({}, state, { [arrKey]: newArr }); +} + +export function removeFromArr(state, arrKey, obj, idKey = 'id') { + const newArr = []; + state[arrKey].forEach((arrItem) => { + if (!(obj[idKey] === arrItem[idKey])) { + newArr.push(arrItem); + } + }); + return Object.assign({}, state, { [arrKey]: newArr }); +} + +export function addToArr(state, arrKey, obj) { + const newObj = Object.assign({}, obj); + if (!newObj.id) { + newObj.id = shortid.generate(); + } + const newState = {}; + newState[arrKey] = [...state[arrKey], newObj]; + return Object.assign({}, state, newState); +}