Skip to content

Commit

Permalink
[feat] Feature flag system via config (#5960)
Browse files Browse the repository at this point in the history
* [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.
  • Loading branch information
xtinec authored and betodealmeida committed Oct 1, 2018
1 parent 414a4bf commit 604524b
Show file tree
Hide file tree
Showing 17 changed files with 246 additions and 113 deletions.
Original file line number Diff line number Diff line change
@@ -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);
});
});

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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);
});
});
39 changes: 28 additions & 11 deletions superset/assets/spec/javascripts/sqllab/App_spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,48 @@ 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', () => {
expect(React.isValidElement(<App />)).to.equal(true);
});

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);
});
});
7 changes: 6 additions & 1 deletion superset/assets/src/SqlLab/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -83,6 +84,10 @@ App.propTypes = {
actions: PropTypes.object,
};

const mapStateToProps = state => ({
isFeatureEnabled: isFeatureEnabledCreator(state),
});

function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
Expand All @@ -91,6 +96,6 @@ function mapDispatchToProps(dispatch) {

export { App };
export default connect(
null,
mapStateToProps,
mapDispatchToProps,
)(App);
1 change: 1 addition & 0 deletions superset/assets/src/SqlLab/getInitialState.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default function getInitialState({ defaultDbId, ...restBootstrapData }) {
};

return {
featureFlags: restBootstrapData.common.feature_flags,
sqlLab: {
alerts: [],
queries: {},
Expand Down
2 changes: 2 additions & 0 deletions superset/assets/src/SqlLab/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getFromArr,
addToArr,
} from '../reduxUtils';
import featureFlags from '../featureFlags';
import { t } from '../locales';

export const sqlLabReducer = function (state = {}, action) {
Expand Down Expand Up @@ -267,6 +268,7 @@ export const sqlLabReducer = function (state = {}, action) {
};

export default combineReducers({
featureFlags,
sqlLab: sqlLabReducer,
messageToasts,
});
22 changes: 13 additions & 9 deletions superset/assets/src/dashboard/containers/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import { isFeatureEnabledCreator } from '../../featureFlags';
import Dashboard from '../components/Dashboard';

import {
Expand All @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions superset/assets/src/dashboard/reducers/getInitialState.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -140,6 +138,7 @@ export default function(bootstrapData) {
};

return {
featureFlags: common.feature_flags,
datasources,
sliceEntities: { ...initSliceEntities, slices, isLoading: false },
charts: chartQueries,
Expand All @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions superset/assets/src/dashboard/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 604524b

Please sign in to comment.