superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From b...@apache.org
Subject [incubator-superset] branch master updated: [feat] Feature flag system via config (#5960)
Date Mon, 01 Oct 2018 18:47:25 GMT
This is an automated email from the ASF dual-hosted git repository.

beto pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 604524b  [feat] Feature flag system via config (#5960)
604524b is described below

commit 604524b671b4d68b7cc4be5d3116c0a49cff4d4d
Author: Christine Chambers <christine.d.hang@gmail.com>
AuthorDate: Mon Oct 1 11:47:06 2018 -0700

    [feat] Feature flag system via config (#5960)
    
    * [feat] Feature flag system via config
    
    Adding a feature flag system that is driven by superset_config.py. This change includes:
    - Server side changes to specify a dedicated FEATURE_FLAG dictionary for listing feature
flags. E.g.
    ```
    FEATURE_FLAGS = { 'SCOPED_FILTER': true }
    ```
    - Pass the new feature flags to client via bootstrap-data
    - Client side changes to inject feature flags into the redux state tree for dashboard,
explore view and SqlLab
    - Client side refactor/clean up so the feature flags can be properly tested. Also avoid
modifying incoming bootstrap data when creating initial state for the redux state tree
    - Re-enable tests that were previously disabled for ExploreViewContainer
    
    * Fix lint errors.
    
    * Remove the partial attempt to get reference to src working in tests (so we don't have
to write ../../../src and such in tests). This will in a separate PR.
---
 .../dashboard/containers/Dashboard_spec.jsx        | 39 ++++++++++++
 .../components/ExploreViewContainer_spec.js        | 38 ------------
 .../components/ExploreViewContainer_spec.jsx       | 69 ++++++++++++++++++++++
 .../assets/spec/javascripts/sqllab/App_spec.jsx    | 39 ++++++++----
 superset/assets/src/SqlLab/components/App.jsx      |  7 ++-
 superset/assets/src/SqlLab/getInitialState.js      |  1 +
 superset/assets/src/SqlLab/reducers.js             |  2 +
 .../assets/src/dashboard/containers/Dashboard.jsx  | 22 ++++---
 .../src/dashboard/reducers/getInitialState.js      |  8 ++-
 superset/assets/src/dashboard/reducers/index.js    |  2 +
 superset/assets/src/explore/App.jsx                | 52 +---------------
 .../explore/components/ExploreViewContainer.jsx    |  5 +-
 .../assets/src/explore/reducers/getInitialState.js | 53 +++++++++++++++++
 superset/assets/src/explore/reducers/index.js      |  2 +
 superset/assets/src/featureFlags.js                | 11 ++++
 superset/config.py                                 |  8 +++
 superset/views/base.py                             |  1 +
 17 files changed, 246 insertions(+), 113 deletions(-)

diff --git a/superset/assets/spec/javascripts/dashboard/containers/Dashboard_spec.jsx b/superset/assets/spec/javascripts/dashboard/containers/Dashboard_spec.jsx
new file mode 100644
index 0000000..78d781b
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/containers/Dashboard_spec.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import { shallow } from 'enzyme';
+import { expect } from 'chai';
+
+import Dashboard from '../../../../src/dashboard/containers/Dashboard';
+import getInitialState from '../../../../src/dashboard/reducers/getInitialState';
+
+describe('Dashboard Container', () => {
+  const middlewares = [thunk];
+  const mockStore = configureStore(middlewares);
+  let store;
+  let wrapper;
+
+  before(() => {
+    const bootstrapData = {
+      dashboard_data: {
+        slices: [],
+        metadata: {},
+      },
+      common: {
+        feature_flags: {
+          FOO_BAR: true,
+        },
+        conf: {},
+      },
+    };
+    store = mockStore(getInitialState(bootstrapData), {});
+  });
+
+  beforeEach(() => {
+    wrapper = shallow(<Dashboard />, { context: { store } });
+  });
+
+  it('should set feature flags', () => {
+    expect(wrapper.prop('isFeatureEnabled')('FOO_BAR')).to.equal(true);
+  });
+});
diff --git a/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.js
b/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.js
deleted file mode 100644
index e6340f2..0000000
--- a/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-// this test must be commented out because ChartContainer is now importing files
-// from visualizations/*.js which are also importing css files which breaks in the testing
env.
-
-// import React from 'react';
-// import { expect } from 'chai';
-// // import { shallow } from 'enzyme';
-
-// import ExploreViewContainer
-//   from '../../../../src/explore/components/ExploreViewContainer';
-// import QueryAndSaveBtns
-//   from '../../../../src/explore/components/QueryAndSaveBtns';
-// import ControlPanelsContainer
-//   from '../../../../src/explore/components/ControlPanelsContainer';
-// import ChartContainer
-//   from '../../../../src/explore/components/ChartContainer';
-
-// describe('ExploreViewContainer', () => {
-//   it('renders', () => {
-//     expect(
-//       React.isValidElement(<ExploreViewContainer />)
-//     ).to.equal(true);
-//   });
-
-//   it('renders QueryAndSaveButtons', () => {
-//     const wrapper = shallow(<ExploreViewContainer />);
-//     expect(wrapper.find(QueryAndSaveBtns)).to.have.length(1);
-//   });
-
-//   it('renders ControlPanelsContainer', () => {
-//     const wrapper = shallow(<ExploreViewContainer />);
-//     expect(wrapper.find(ControlPanelsContainer)).to.have.length(1);
-//   });
-
-//   it('renders ChartContainer', () => {
-//     const wrapper = shallow(<ExploreViewContainer />);
-//     expect(wrapper.find(ChartContainer)).to.have.length(1);
-//   });
-// });
diff --git a/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx
b/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx
new file mode 100644
index 0000000..525a85b
--- /dev/null
+++ b/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+
+import getInitialState from '../../../../src/explore/reducers/getInitialState';
+import ExploreViewContainer
+  from '../../../../src/explore/components/ExploreViewContainer';
+import QueryAndSaveBtns
+  from '../../../../src/explore/components/QueryAndSaveBtns';
+import ControlPanelsContainer
+  from '../../../../src/explore/components/ControlPanelsContainer';
+import ChartContainer
+  from '../../../../src/explore/components/ExploreChartPanel';
+
+describe('ExploreViewContainer', () => {
+  const middlewares = [thunk];
+  const mockStore = configureStore(middlewares);
+  let store;
+  let wrapper;
+
+  before(() => {
+    const bootstrapData = {
+      common: {
+        feature_flags: {
+          FOO_BAR: true,
+        },
+        conf: {},
+      },
+      datasource: {
+        columns: [],
+      },
+      form_data: {
+        datasource: {},
+      },
+    };
+    store = mockStore(getInitialState(bootstrapData), {});
+  });
+
+  beforeEach(() => {
+    wrapper = shallow(<ExploreViewContainer />, {
+      context: { store },
+      disableLifecycleMethods: true,
+    });
+  });
+
+  it('should set feature flags', () => {
+    expect(wrapper.prop('isFeatureEnabled')('FOO_BAR')).to.equal(true);
+  });
+
+  it('renders', () => {
+    expect(
+      React.isValidElement(<ExploreViewContainer />),
+    ).to.equal(true);
+  });
+
+  it('renders QueryAndSaveButtons', () => {
+    expect(wrapper.dive().find(QueryAndSaveBtns)).to.have.length(1);
+  });
+
+  it('renders ControlPanelsContainer', () => {
+    expect(wrapper.dive().find(ControlPanelsContainer)).to.have.length(1);
+  });
+
+  it('renders ChartContainer', () => {
+    expect(wrapper.dive().find(ChartContainer)).to.have.length(1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/sqllab/App_spec.jsx b/superset/assets/spec/javascripts/sqllab/App_spec.jsx
index 4e64d17..bee2569 100644
--- a/superset/assets/spec/javascripts/sqllab/App_spec.jsx
+++ b/superset/assets/spec/javascripts/sqllab/App_spec.jsx
@@ -8,16 +8,31 @@ import sinon from 'sinon';
 
 import App from '../../../src/SqlLab/components/App';
 import TabbedSqlEditors from '../../../src/SqlLab/components/TabbedSqlEditors';
-import { sqlLabReducer } from '../../../src/SqlLab/reducers';
+import getInitialState from '../../../src/SqlLab/getInitialState';
 
-describe('App', () => {
+describe('SqlLab App', () => {
   const middlewares = [thunk];
   const mockStore = configureStore(middlewares);
-  const store = mockStore({ sqlLab: sqlLabReducer(undefined, {}), messageToasts: [] });
-
+  let store;
   let wrapper;
+
+  before(() => {
+    const bootstrapData = {
+      common: {
+        feature_flags: {
+          FOO_BAR: true,
+        },
+      },
+    };
+    store = mockStore(getInitialState(bootstrapData), {});
+  });
+
   beforeEach(() => {
-    wrapper = shallow(<App />, { context: { store } }).dive();
+    wrapper = shallow(<App />, { context: { store } });
+  });
+
+  it('should set feature flags', () => {
+    expect(wrapper.prop('isFeatureEnabled')('FOO_BAR')).to.equal(true);
   });
 
   it('is valid', () => {
@@ -25,14 +40,16 @@ describe('App', () => {
   });
 
   it('should handler resize', () => {
-    sinon.spy(wrapper.instance(), 'getHeight');
-    wrapper.instance().handleResize();
-    expect(wrapper.instance().getHeight.callCount).to.equal(1);
-    wrapper.instance().getHeight.restore();
+    const inner = wrapper.dive();
+    sinon.spy(inner.instance(), 'getHeight');
+    inner.instance().handleResize();
+    expect(inner.instance().getHeight.callCount).to.equal(1);
+    inner.instance().getHeight.restore();
   });
 
   it('should render', () => {
-    expect(wrapper.find('.SqlLab')).to.have.length(1);
-    expect(wrapper.find(TabbedSqlEditors)).to.have.length(1);
+    const inner = wrapper.dive();
+    expect(inner.find('.SqlLab')).to.have.length(1);
+    expect(inner.find(TabbedSqlEditors)).to.have.length(1);
   });
 });
diff --git a/superset/assets/src/SqlLab/components/App.jsx b/superset/assets/src/SqlLab/components/App.jsx
index 8a0c084..19f8848 100644
--- a/superset/assets/src/SqlLab/components/App.jsx
+++ b/superset/assets/src/SqlLab/components/App.jsx
@@ -9,6 +9,7 @@ import QueryAutoRefresh from './QueryAutoRefresh';
 import QuerySearch from './QuerySearch';
 import ToastPresenter from '../../messageToasts/containers/ToastPresenter';
 import * as Actions from '../actions';
+import { isFeatureEnabledCreator } from '../../featureFlags';
 
 class App extends React.PureComponent {
   constructor(props) {
@@ -83,6 +84,10 @@ App.propTypes = {
   actions: PropTypes.object,
 };
 
+const mapStateToProps = state => ({
+  isFeatureEnabled: isFeatureEnabledCreator(state),
+});
+
 function mapDispatchToProps(dispatch) {
   return {
     actions: bindActionCreators(Actions, dispatch),
@@ -91,6 +96,6 @@ function mapDispatchToProps(dispatch) {
 
 export { App };
 export default connect(
-  null,
+  mapStateToProps,
   mapDispatchToProps,
 )(App);
diff --git a/superset/assets/src/SqlLab/getInitialState.js b/superset/assets/src/SqlLab/getInitialState.js
index c33ff1e..b914220 100644
--- a/superset/assets/src/SqlLab/getInitialState.js
+++ b/superset/assets/src/SqlLab/getInitialState.js
@@ -14,6 +14,7 @@ export default function getInitialState({ defaultDbId, ...restBootstrapData
}) {
   };
 
   return {
+    featureFlags: restBootstrapData.common.feature_flags,
     sqlLab: {
       alerts: [],
       queries: {},
diff --git a/superset/assets/src/SqlLab/reducers.js b/superset/assets/src/SqlLab/reducers.js
index 537f4be..7916b72 100644
--- a/superset/assets/src/SqlLab/reducers.js
+++ b/superset/assets/src/SqlLab/reducers.js
@@ -13,6 +13,7 @@ import {
   getFromArr,
   addToArr,
 } from '../reduxUtils';
+import featureFlags from '../featureFlags';
 import { t } from '../locales';
 
 export const sqlLabReducer = function (state = {}, action) {
@@ -267,6 +268,7 @@ export const sqlLabReducer = function (state = {}, action) {
 };
 
 export default combineReducers({
+  featureFlags,
   sqlLab: sqlLabReducer,
   messageToasts,
 });
diff --git a/superset/assets/src/dashboard/containers/Dashboard.jsx b/superset/assets/src/dashboard/containers/Dashboard.jsx
index d43e448..9507dcd 100644
--- a/superset/assets/src/dashboard/containers/Dashboard.jsx
+++ b/superset/assets/src/dashboard/containers/Dashboard.jsx
@@ -1,6 +1,7 @@
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
+import { isFeatureEnabledCreator } from '../../featureFlags';
 import Dashboard from '../components/Dashboard';
 
 import {
@@ -10,16 +11,19 @@ import {
 import { runQuery } from '../../chart/chartAction';
 import getLoadStatsPerTopLevelComponent from '../util/logging/getLoadStatsPerTopLevelComponent';
 
-function mapStateToProps({
-  datasources,
-  sliceEntities,
-  charts,
-  dashboardInfo,
-  dashboardState,
-  dashboardLayout,
-  impressionId,
-}) {
+function mapStateToProps(state) {
+  const {
+    datasources,
+    sliceEntities,
+    charts,
+    dashboardInfo,
+    dashboardState,
+    dashboardLayout,
+    impressionId,
+  } = state;
+
   return {
+    isFeatureEnabled: isFeatureEnabledCreator(state),
     initMessages: dashboardInfo.common.flash_messages,
     timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
     userId: dashboardInfo.userId,
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index 2a4a5e2..523db9f 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -22,8 +22,6 @@ import { getScale } from '../../modules/CategoricalColorNamespace';
 
 export default function(bootstrapData) {
   const { user_id, datasources, common, editMode } = bootstrapData;
-  delete common.locale;
-  delete common.language_pack;
 
   const dashboard = { ...bootstrapData.dashboard_data };
   let filters = {};
@@ -140,6 +138,7 @@ export default function(bootstrapData) {
   };
 
   return {
+    featureFlags: common.feature_flags,
     datasources,
     sliceEntities: { ...initSliceEntities, slices, isLoading: false },
     charts: chartQueries,
@@ -159,7 +158,10 @@ export default function(bootstrapData) {
       dash_save_perm: dashboard.dash_save_perm,
       superset_can_explore: dashboard.superset_can_explore,
       slice_can_edit: dashboard.slice_can_edit,
-      common,
+      common: {
+        flash_messages: common.flash_messages,
+        conf: common.conf,
+      },
     },
     dashboardState: {
       sliceIds: Array.from(sliceIds),
diff --git a/superset/assets/src/dashboard/reducers/index.js b/superset/assets/src/dashboard/reducers/index.js
index a5be96f..28751ea 100644
--- a/superset/assets/src/dashboard/reducers/index.js
+++ b/superset/assets/src/dashboard/reducers/index.js
@@ -5,12 +5,14 @@ import dashboardState from './dashboardState';
 import datasources from './datasources';
 import sliceEntities from './sliceEntities';
 import dashboardLayout from '../reducers/undoableDashboardLayout';
+import featureFlags from '../../featureFlags';
 import messageToasts from '../../messageToasts/reducers';
 
 const dashboardInfo = (state = {}) => state;
 const impressionId = (state = '') => state;
 
 export default combineReducers({
+  featureFlags,
   charts,
   datasources,
   dashboardInfo,
diff --git a/superset/assets/src/explore/App.jsx b/superset/assets/src/explore/App.jsx
index d46b494..002eb26 100644
--- a/superset/assets/src/explore/App.jsx
+++ b/superset/assets/src/explore/App.jsx
@@ -4,16 +4,12 @@ import { createStore, applyMiddleware, compose } from 'redux';
 import { Provider } from 'react-redux';
 import thunk from 'redux-thunk';
 
-import shortid from 'shortid';
-import { now } from '../modules/dates';
 import { initEnhancer } from '../reduxUtils';
-import { getChartKey } from './exploreUtils';
 import ToastPresenter from '../messageToasts/containers/ToastPresenter';
-import { getControlsState, getFormDataFromControls } from './store';
 import { initJQueryAjax } from '../modules/utils';
 import ExploreViewContainer from './components/ExploreViewContainer';
+import getInitialState from './reducers/getInitialState';
 import rootReducer from './reducers/index';
-import getToastsFromPyFlashMessages from '../messageToasts/utils/getToastsFromPyFlashMessages';
 
 import { appSetup } from '../common';
 import './main.css';
@@ -24,51 +20,7 @@ initJQueryAjax();
 
 const exploreViewContainer = document.getElementById('app');
 const bootstrapData = JSON.parse(exploreViewContainer.getAttribute('data-bootstrap'));
-const controls = getControlsState(bootstrapData, bootstrapData.form_data);
-const rawFormData = { ...bootstrapData.form_data };
-
-delete bootstrapData.form_data;
-delete bootstrapData.common.locale;
-delete bootstrapData.common.language_pack;
-
-// Initial state
-const bootstrappedState = {
-  ...bootstrapData,
-  rawFormData,
-  controls,
-  filterColumnOpts: [],
-  isDatasourceMetaLoading: false,
-  isStarred: false,
-};
-const slice = bootstrappedState.slice;
-const sliceFormData = slice
-  ? getFormDataFromControls(getControlsState(bootstrapData, slice.form_data))
-  : null;
-const chartKey = getChartKey(bootstrappedState);
-const initState = {
-  charts: {
-    [chartKey]: {
-      id: chartKey,
-      chartAlert: null,
-      chartStatus: 'loading',
-      chartUpdateEndTime: null,
-      chartUpdateStartTime: now(),
-      latestQueryFormData: getFormDataFromControls(controls),
-      sliceFormData,
-      queryRequest: null,
-      queryResponse: null,
-      triggerQuery: true,
-      lastRendered: 0,
-    },
-  },
-  saveModal: {
-    dashboards: [],
-    saveModalAlert: null,
-  },
-  explore: bootstrappedState,
-  impressionId: shortid.generate(),
-  messageToasts: getToastsFromPyFlashMessages((bootstrapData.common || {}).flash_messages
|| []),
-};
+const initState = getInitialState(bootstrapData);
 
 const store = createStore(
   rootReducer,
diff --git a/superset/assets/src/explore/components/ExploreViewContainer.jsx b/superset/assets/src/explore/components/ExploreViewContainer.jsx
index 2476615..34f165d 100644
--- a/superset/assets/src/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/src/explore/components/ExploreViewContainer.jsx
@@ -15,6 +15,7 @@ import { chartPropShape } from '../../dashboard/util/propShapes';
 import * as exploreActions from '../actions/exploreActions';
 import * as saveModalActions from '../actions/saveModalActions';
 import * as chartActions from '../../chart/chartAction';
+import { isFeatureEnabledCreator } from '../../featureFlags';
 import { Logger, ActionLog, EXPLORE_EVENT_NAMES, LOG_ACTIONS_MOUNT_EXPLORER } from '../../logger';
 
 const propTypes = {
@@ -296,11 +297,13 @@ class ExploreViewContainer extends React.Component {
 
 ExploreViewContainer.propTypes = propTypes;
 
-function mapStateToProps({ explore, charts, impressionId }) {
+function mapStateToProps(state) {
+  const { explore, charts, impressionId } = state;
   const form_data = getFormDataFromControls(explore.controls);
   const chartKey = Object.keys(charts)[0];
   const chart = charts[chartKey];
   return {
+    isFeatureEnabled: isFeatureEnabledCreator(state),
     isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
     datasource: explore.datasource,
     datasource_type: explore.datasource.type,
diff --git a/superset/assets/src/explore/reducers/getInitialState.js b/superset/assets/src/explore/reducers/getInitialState.js
new file mode 100644
index 0000000..28910bf
--- /dev/null
+++ b/superset/assets/src/explore/reducers/getInitialState.js
@@ -0,0 +1,53 @@
+import shortid from 'shortid';
+
+import getToastsFromPyFlashMessages from '../../messageToasts/utils/getToastsFromPyFlashMessages';
+import { now } from '../../modules/dates';
+import { getChartKey } from '../exploreUtils';
+import { getControlsState, getFormDataFromControls } from '../store';
+
+export default function (bootstrapData) {
+  const controls = getControlsState(bootstrapData, bootstrapData.form_data);
+  const rawFormData = { ...bootstrapData.form_data };
+  const bootstrappedState = {
+    ...bootstrapData,
+    common: {
+      flash_messages: bootstrapData.common.flash_messages,
+      conf: bootstrapData.common.conf,
+    },
+    rawFormData,
+    controls,
+    filterColumnOpts: [],
+    isDatasourceMetaLoading: false,
+    isStarred: false,
+  };
+  const slice = bootstrappedState.slice;
+  const sliceFormData = slice
+    ? getFormDataFromControls(getControlsState(bootstrapData, slice.form_data))
+    : null;
+  const chartKey = getChartKey(bootstrappedState);
+  return {
+    featureFlags: bootstrapData.common.feature_flags,
+    charts: {
+      [chartKey]: {
+        id: chartKey,
+        chartAlert: null,
+        chartStatus: 'loading',
+        chartUpdateEndTime: null,
+        chartUpdateStartTime: now(),
+        latestQueryFormData: getFormDataFromControls(controls),
+        sliceFormData,
+        queryRequest: null,
+        queryResponse: null,
+        triggerQuery: true,
+        lastRendered: 0,
+      },
+    },
+    saveModal: {
+      dashboards: [],
+      saveModalAlert: null,
+    },
+    explore: bootstrappedState,
+    impressionId: shortid.generate(),
+    messageToasts: getToastsFromPyFlashMessages((bootstrapData.common || {}).flash_messages
|| []),
+  };
+}
diff --git a/superset/assets/src/explore/reducers/index.js b/superset/assets/src/explore/reducers/index.js
index 461eb0f..1226f17 100644
--- a/superset/assets/src/explore/reducers/index.js
+++ b/superset/assets/src/explore/reducers/index.js
@@ -3,11 +3,13 @@ import { combineReducers } from 'redux';
 import charts from '../../chart/chartReducer';
 import saveModal from './saveModalReducer';
 import explore from './exploreReducer';
+import featureFlags from '../../featureFlags';
 import messageToasts from '../../messageToasts/reducers';
 
 const impressionId = (state = '') => state;
 
 export default combineReducers({
+  featureFlags,
   charts,
   saveModal,
   explore,
diff --git a/superset/assets/src/featureFlags.js b/superset/assets/src/featureFlags.js
new file mode 100644
index 0000000..1dc6635
--- /dev/null
+++ b/superset/assets/src/featureFlags.js
@@ -0,0 +1,11 @@
+// A higher-order function that takes the redux state tree and returns a
+// `isFeatureEnabled` function which takes a feature and returns whether it is enabled.
+// Note that we assume the featureFlags subtree is at the root of the redux state tree.
+export function isFeatureEnabledCreator(state) {
+  return feature => !!state.featureFlags[feature];
+}
+
+// Feature flags are not altered throughout the life time of the app
+export default function featureFlagsReducer(state = {}) {
+  return state;
+}
diff --git a/superset/config.py b/superset/config.py
index 80f6f85..d00bfd5 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -170,6 +170,14 @@ LANGUAGES = {
     'pt_BR': {'flag': 'br', 'name': 'Brazilian Portuguese'},
     'ru': {'flag': 'ru', 'name': 'Russian'},
 }
+
+# ---------------------------------------------------
+# Feature flags
+# ---------------------------------------------------
+# Feature flags that are on by default go here. Their
+# values can be overridden by those in super_config.py
+FEATURE_FLAGS = {}
+
 # ---------------------------------------------------
 # Image and file configuration
 # ---------------------------------------------------
diff --git a/superset/views/base.py b/superset/views/base.py
index 2f56ae7..42de225 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -108,6 +108,7 @@ class BaseSupersetView(BaseView):
             'conf': {k: conf.get(k) for k in FRONTEND_CONF_KEYS},
             'locale': locale,
             'language_pack': get_language_pack(locale),
+            'feature_flags': conf.get('FEATURE_FLAGS'),
         }
 
 


Mime
View raw message