mirror of
https://github.com/apache/superset.git
synced 2026-04-19 16:14:52 +00:00
[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.
This commit is contained in:
committed by
Beto Dealmeida
parent
414a4bfc6f
commit
604524b671
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,16 +8,31 @@ import sinon from 'sinon';
|
|||||||
|
|
||||||
import App from '../../../src/SqlLab/components/App';
|
import App from '../../../src/SqlLab/components/App';
|
||||||
import TabbedSqlEditors from '../../../src/SqlLab/components/TabbedSqlEditors';
|
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 middlewares = [thunk];
|
||||||
const mockStore = configureStore(middlewares);
|
const mockStore = configureStore(middlewares);
|
||||||
const store = mockStore({ sqlLab: sqlLabReducer(undefined, {}), messageToasts: [] });
|
let store;
|
||||||
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
const bootstrapData = {
|
||||||
|
common: {
|
||||||
|
feature_flags: {
|
||||||
|
FOO_BAR: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
store = mockStore(getInitialState(bootstrapData), {});
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
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', () => {
|
it('is valid', () => {
|
||||||
@@ -25,14 +40,16 @@ describe('App', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handler resize', () => {
|
it('should handler resize', () => {
|
||||||
sinon.spy(wrapper.instance(), 'getHeight');
|
const inner = wrapper.dive();
|
||||||
wrapper.instance().handleResize();
|
sinon.spy(inner.instance(), 'getHeight');
|
||||||
expect(wrapper.instance().getHeight.callCount).to.equal(1);
|
inner.instance().handleResize();
|
||||||
wrapper.instance().getHeight.restore();
|
expect(inner.instance().getHeight.callCount).to.equal(1);
|
||||||
|
inner.instance().getHeight.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render', () => {
|
it('should render', () => {
|
||||||
expect(wrapper.find('.SqlLab')).to.have.length(1);
|
const inner = wrapper.dive();
|
||||||
expect(wrapper.find(TabbedSqlEditors)).to.have.length(1);
|
expect(inner.find('.SqlLab')).to.have.length(1);
|
||||||
|
expect(inner.find(TabbedSqlEditors)).to.have.length(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import QueryAutoRefresh from './QueryAutoRefresh';
|
|||||||
import QuerySearch from './QuerySearch';
|
import QuerySearch from './QuerySearch';
|
||||||
import ToastPresenter from '../../messageToasts/containers/ToastPresenter';
|
import ToastPresenter from '../../messageToasts/containers/ToastPresenter';
|
||||||
import * as Actions from '../actions';
|
import * as Actions from '../actions';
|
||||||
|
import { isFeatureEnabledCreator } from '../../featureFlags';
|
||||||
|
|
||||||
class App extends React.PureComponent {
|
class App extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -83,6 +84,10 @@ App.propTypes = {
|
|||||||
actions: PropTypes.object,
|
actions: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
isFeatureEnabled: isFeatureEnabledCreator(state),
|
||||||
|
});
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return {
|
return {
|
||||||
actions: bindActionCreators(Actions, dispatch),
|
actions: bindActionCreators(Actions, dispatch),
|
||||||
@@ -91,6 +96,6 @@ function mapDispatchToProps(dispatch) {
|
|||||||
|
|
||||||
export { App };
|
export { App };
|
||||||
export default connect(
|
export default connect(
|
||||||
null,
|
mapStateToProps,
|
||||||
mapDispatchToProps,
|
mapDispatchToProps,
|
||||||
)(App);
|
)(App);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default function getInitialState({ defaultDbId, ...restBootstrapData }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
featureFlags: restBootstrapData.common.feature_flags,
|
||||||
sqlLab: {
|
sqlLab: {
|
||||||
alerts: [],
|
alerts: [],
|
||||||
queries: {},
|
queries: {},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
getFromArr,
|
getFromArr,
|
||||||
addToArr,
|
addToArr,
|
||||||
} from '../reduxUtils';
|
} from '../reduxUtils';
|
||||||
|
import featureFlags from '../featureFlags';
|
||||||
import { t } from '../locales';
|
import { t } from '../locales';
|
||||||
|
|
||||||
export const sqlLabReducer = function (state = {}, action) {
|
export const sqlLabReducer = function (state = {}, action) {
|
||||||
@@ -267,6 +268,7 @@ export const sqlLabReducer = function (state = {}, action) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
|
featureFlags,
|
||||||
sqlLab: sqlLabReducer,
|
sqlLab: sqlLabReducer,
|
||||||
messageToasts,
|
messageToasts,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { isFeatureEnabledCreator } from '../../featureFlags';
|
||||||
import Dashboard from '../components/Dashboard';
|
import Dashboard from '../components/Dashboard';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -10,16 +11,19 @@ import {
|
|||||||
import { runQuery } from '../../chart/chartAction';
|
import { runQuery } from '../../chart/chartAction';
|
||||||
import getLoadStatsPerTopLevelComponent from '../util/logging/getLoadStatsPerTopLevelComponent';
|
import getLoadStatsPerTopLevelComponent from '../util/logging/getLoadStatsPerTopLevelComponent';
|
||||||
|
|
||||||
function mapStateToProps({
|
function mapStateToProps(state) {
|
||||||
datasources,
|
const {
|
||||||
sliceEntities,
|
datasources,
|
||||||
charts,
|
sliceEntities,
|
||||||
dashboardInfo,
|
charts,
|
||||||
dashboardState,
|
dashboardInfo,
|
||||||
dashboardLayout,
|
dashboardState,
|
||||||
impressionId,
|
dashboardLayout,
|
||||||
}) {
|
impressionId,
|
||||||
|
} = state;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
isFeatureEnabled: isFeatureEnabledCreator(state),
|
||||||
initMessages: dashboardInfo.common.flash_messages,
|
initMessages: dashboardInfo.common.flash_messages,
|
||||||
timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
|
timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
|
||||||
userId: dashboardInfo.userId,
|
userId: dashboardInfo.userId,
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ import { getScale } from '../../modules/CategoricalColorNamespace';
|
|||||||
|
|
||||||
export default function(bootstrapData) {
|
export default function(bootstrapData) {
|
||||||
const { user_id, datasources, common, editMode } = bootstrapData;
|
const { user_id, datasources, common, editMode } = bootstrapData;
|
||||||
delete common.locale;
|
|
||||||
delete common.language_pack;
|
|
||||||
|
|
||||||
const dashboard = { ...bootstrapData.dashboard_data };
|
const dashboard = { ...bootstrapData.dashboard_data };
|
||||||
let filters = {};
|
let filters = {};
|
||||||
@@ -140,6 +138,7 @@ export default function(bootstrapData) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
featureFlags: common.feature_flags,
|
||||||
datasources,
|
datasources,
|
||||||
sliceEntities: { ...initSliceEntities, slices, isLoading: false },
|
sliceEntities: { ...initSliceEntities, slices, isLoading: false },
|
||||||
charts: chartQueries,
|
charts: chartQueries,
|
||||||
@@ -159,7 +158,10 @@ export default function(bootstrapData) {
|
|||||||
dash_save_perm: dashboard.dash_save_perm,
|
dash_save_perm: dashboard.dash_save_perm,
|
||||||
superset_can_explore: dashboard.superset_can_explore,
|
superset_can_explore: dashboard.superset_can_explore,
|
||||||
slice_can_edit: dashboard.slice_can_edit,
|
slice_can_edit: dashboard.slice_can_edit,
|
||||||
common,
|
common: {
|
||||||
|
flash_messages: common.flash_messages,
|
||||||
|
conf: common.conf,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dashboardState: {
|
dashboardState: {
|
||||||
sliceIds: Array.from(sliceIds),
|
sliceIds: Array.from(sliceIds),
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import dashboardState from './dashboardState';
|
|||||||
import datasources from './datasources';
|
import datasources from './datasources';
|
||||||
import sliceEntities from './sliceEntities';
|
import sliceEntities from './sliceEntities';
|
||||||
import dashboardLayout from '../reducers/undoableDashboardLayout';
|
import dashboardLayout from '../reducers/undoableDashboardLayout';
|
||||||
|
import featureFlags from '../../featureFlags';
|
||||||
import messageToasts from '../../messageToasts/reducers';
|
import messageToasts from '../../messageToasts/reducers';
|
||||||
|
|
||||||
const dashboardInfo = (state = {}) => state;
|
const dashboardInfo = (state = {}) => state;
|
||||||
const impressionId = (state = '') => state;
|
const impressionId = (state = '') => state;
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
|
featureFlags,
|
||||||
charts,
|
charts,
|
||||||
datasources,
|
datasources,
|
||||||
dashboardInfo,
|
dashboardInfo,
|
||||||
|
|||||||
@@ -4,16 +4,12 @@ import { createStore, applyMiddleware, compose } from 'redux';
|
|||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
|
|
||||||
import shortid from 'shortid';
|
|
||||||
import { now } from '../modules/dates';
|
|
||||||
import { initEnhancer } from '../reduxUtils';
|
import { initEnhancer } from '../reduxUtils';
|
||||||
import { getChartKey } from './exploreUtils';
|
|
||||||
import ToastPresenter from '../messageToasts/containers/ToastPresenter';
|
import ToastPresenter from '../messageToasts/containers/ToastPresenter';
|
||||||
import { getControlsState, getFormDataFromControls } from './store';
|
|
||||||
import { initJQueryAjax } from '../modules/utils';
|
import { initJQueryAjax } from '../modules/utils';
|
||||||
import ExploreViewContainer from './components/ExploreViewContainer';
|
import ExploreViewContainer from './components/ExploreViewContainer';
|
||||||
|
import getInitialState from './reducers/getInitialState';
|
||||||
import rootReducer from './reducers/index';
|
import rootReducer from './reducers/index';
|
||||||
import getToastsFromPyFlashMessages from '../messageToasts/utils/getToastsFromPyFlashMessages';
|
|
||||||
|
|
||||||
import { appSetup } from '../common';
|
import { appSetup } from '../common';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
@@ -24,51 +20,7 @@ initJQueryAjax();
|
|||||||
|
|
||||||
const exploreViewContainer = document.getElementById('app');
|
const exploreViewContainer = document.getElementById('app');
|
||||||
const bootstrapData = JSON.parse(exploreViewContainer.getAttribute('data-bootstrap'));
|
const bootstrapData = JSON.parse(exploreViewContainer.getAttribute('data-bootstrap'));
|
||||||
const controls = getControlsState(bootstrapData, bootstrapData.form_data);
|
const initState = getInitialState(bootstrapData);
|
||||||
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 store = createStore(
|
const store = createStore(
|
||||||
rootReducer,
|
rootReducer,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { chartPropShape } from '../../dashboard/util/propShapes';
|
|||||||
import * as exploreActions from '../actions/exploreActions';
|
import * as exploreActions from '../actions/exploreActions';
|
||||||
import * as saveModalActions from '../actions/saveModalActions';
|
import * as saveModalActions from '../actions/saveModalActions';
|
||||||
import * as chartActions from '../../chart/chartAction';
|
import * as chartActions from '../../chart/chartAction';
|
||||||
|
import { isFeatureEnabledCreator } from '../../featureFlags';
|
||||||
import { Logger, ActionLog, EXPLORE_EVENT_NAMES, LOG_ACTIONS_MOUNT_EXPLORER } from '../../logger';
|
import { Logger, ActionLog, EXPLORE_EVENT_NAMES, LOG_ACTIONS_MOUNT_EXPLORER } from '../../logger';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
@@ -296,11 +297,13 @@ class ExploreViewContainer extends React.Component {
|
|||||||
|
|
||||||
ExploreViewContainer.propTypes = propTypes;
|
ExploreViewContainer.propTypes = propTypes;
|
||||||
|
|
||||||
function mapStateToProps({ explore, charts, impressionId }) {
|
function mapStateToProps(state) {
|
||||||
|
const { explore, charts, impressionId } = state;
|
||||||
const form_data = getFormDataFromControls(explore.controls);
|
const form_data = getFormDataFromControls(explore.controls);
|
||||||
const chartKey = Object.keys(charts)[0];
|
const chartKey = Object.keys(charts)[0];
|
||||||
const chart = charts[chartKey];
|
const chart = charts[chartKey];
|
||||||
return {
|
return {
|
||||||
|
isFeatureEnabled: isFeatureEnabledCreator(state),
|
||||||
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
||||||
datasource: explore.datasource,
|
datasource: explore.datasource,
|
||||||
datasource_type: explore.datasource.type,
|
datasource_type: explore.datasource.type,
|
||||||
|
|||||||
53
superset/assets/src/explore/reducers/getInitialState.js
Normal file
53
superset/assets/src/explore/reducers/getInitialState.js
Normal file
@@ -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 || []),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,11 +3,13 @@ import { combineReducers } from 'redux';
|
|||||||
import charts from '../../chart/chartReducer';
|
import charts from '../../chart/chartReducer';
|
||||||
import saveModal from './saveModalReducer';
|
import saveModal from './saveModalReducer';
|
||||||
import explore from './exploreReducer';
|
import explore from './exploreReducer';
|
||||||
|
import featureFlags from '../../featureFlags';
|
||||||
import messageToasts from '../../messageToasts/reducers';
|
import messageToasts from '../../messageToasts/reducers';
|
||||||
|
|
||||||
const impressionId = (state = '') => state;
|
const impressionId = (state = '') => state;
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
|
featureFlags,
|
||||||
charts,
|
charts,
|
||||||
saveModal,
|
saveModal,
|
||||||
explore,
|
explore,
|
||||||
|
|||||||
11
superset/assets/src/featureFlags.js
Normal file
11
superset/assets/src/featureFlags.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -170,6 +170,14 @@ LANGUAGES = {
|
|||||||
'pt_BR': {'flag': 'br', 'name': 'Brazilian Portuguese'},
|
'pt_BR': {'flag': 'br', 'name': 'Brazilian Portuguese'},
|
||||||
'ru': {'flag': 'ru', 'name': 'Russian'},
|
'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
|
# Image and file configuration
|
||||||
# ---------------------------------------------------
|
# ---------------------------------------------------
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ class BaseSupersetView(BaseView):
|
|||||||
'conf': {k: conf.get(k) for k in FRONTEND_CONF_KEYS},
|
'conf': {k: conf.get(k) for k in FRONTEND_CONF_KEYS},
|
||||||
'locale': locale,
|
'locale': locale,
|
||||||
'language_pack': get_language_pack(locale),
|
'language_pack': get_language_pack(locale),
|
||||||
|
'feature_flags': conf.get('FEATURE_FLAGS'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user