diff --git a/superset/assets/cypress/integration/dashboard/dashboard.helper.js b/superset/assets/cypress/integration/dashboard/dashboard.helper.js
index 7e3788fddc899..1c16a82901298 100644
--- a/superset/assets/cypress/integration/dashboard/dashboard.helper.js
+++ b/superset/assets/cypress/integration/dashboard/dashboard.helper.js
@@ -16,7 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
-export const WORLD_HEALTH_DASHBOARD = '/superset/dashboard/world_health';
+export const WORLD_HEALTH_DASHBOARD = '/superset/dashboard/world_health/';
+export const TABBED_DASHBOARD = '/superset/dashboard/tabbed_dash/';
export const CHECK_DASHBOARD_FAVORITE_ENDPOINT = '/superset/favstar/Dashboard/*/count';
diff --git a/superset/assets/cypress/integration/dashboard/index.test.js b/superset/assets/cypress/integration/dashboard/index.test.js
index 9763b91737161..cc608e7d5b93b 100644
--- a/superset/assets/cypress/integration/dashboard/index.test.js
+++ b/superset/assets/cypress/integration/dashboard/index.test.js
@@ -22,6 +22,7 @@ import DashboardFavStarTest from './fav_star';
import DashboardFilterTest from './filter';
import DashboardLoadTest from './load';
import DashboardSaveTest from './save';
+import DashboardTabsTest from './tabs';
describe('Dashboard', () => {
DashboardControlsTest();
@@ -30,4 +31,5 @@ describe('Dashboard', () => {
DashboardFilterTest();
DashboardLoadTest();
DashboardSaveTest();
+ DashboardTabsTest();
});
diff --git a/superset/assets/cypress/integration/dashboard/tabs.js b/superset/assets/cypress/integration/dashboard/tabs.js
new file mode 100644
index 0000000000000..5029a00852dba
--- /dev/null
+++ b/superset/assets/cypress/integration/dashboard/tabs.js
@@ -0,0 +1,157 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { TABBED_DASHBOARD } from './dashboard.helper';
+
+export default () => describe('tabs', () => {
+ let filterId;
+ let treemapId;
+ let linechartId;
+ let boxplotId;
+
+ // cypress can not handle window.scrollTo
+ // https://github.com/cypress-io/cypress/issues/2761
+ // add this exception handler to pass test
+ const handleException = () => {
+ // return false to prevent the error from
+ // failing this test
+ cy.on('uncaught:exception', () => false);
+ };
+
+ beforeEach(() => {
+ cy.server();
+ cy.login();
+
+ cy.visit(TABBED_DASHBOARD);
+
+ cy.get('#app').then((data) => {
+ const bootstrapData = JSON.parse(data[0].dataset.bootstrap);
+ const dashboard = bootstrapData.dashboard_data;
+ filterId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'filter_box')).slice_id;
+ boxplotId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'box_plot')).slice_id;
+ treemapId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'treemap')).slice_id;
+ linechartId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'line')).slice_id;
+
+ const filterFormdata = {
+ slice_id: filterId,
+ };
+ const filterRequest = `/superset/explore_json/?form_data=${JSON.stringify(filterFormdata)}`;
+ cy.route('POST', filterRequest).as('filterRequest');
+
+ const treemapFormdata = {
+ slice_id: treemapId,
+ };
+ const treemapRequest = `/superset/explore_json/?form_data=${JSON.stringify(treemapFormdata)}`;
+ cy.route('POST', treemapRequest).as('treemapRequest');
+
+ const linechartFormdata = {
+ slice_id: linechartId,
+ };
+ const linechartRequest = `/superset/explore_json/?form_data=${JSON.stringify(linechartFormdata)}`;
+ cy.route('POST', linechartRequest).as('linechartRequest');
+
+ const boxplotFormdata = {
+ slice_id: boxplotId,
+ };
+ const boxplotRequest = `/superset/explore_json/?form_data=${JSON.stringify(boxplotFormdata)}`;
+ cy.route('POST', boxplotRequest).as('boxplotRequest');
+ });
+ });
+
+ it('should load charts when tab is visible', () => {
+ // landing in first tab, should see 2 charts
+ cy.wait('@filterRequest');
+ cy.get('.grid-container .filter_box').should('be.exist');
+ cy.wait('@treemapRequest');
+ cy.get('.grid-container .treemap').should('be.exist');
+ cy.get('.grid-container .box_plot').should('not.be.exist');
+ cy.get('.grid-container .line').should('not.be.exist');
+
+ // click row level tab, see 1 more chart
+ cy.get('.tab-content ul.nav.nav-tabs li')
+ .last()
+ .find('.editable-title input')
+ .click();
+ cy.wait('@linechartRequest');
+ cy.get('.grid-container .line').should('be.exist');
+
+ // click top level tab, see 1 more chart
+ handleException();
+ cy.get('.dashboard-component-tabs')
+ .first()
+ .find('ul.nav.nav-tabs li')
+ .last()
+ .find('.editable-title input')
+ .click();
+ cy.wait('@boxplotRequest');
+ cy.get('.grid-container .box_plot').should('be.exist');
+ });
+
+ it('should send new queries when tab becomes visible', () => {
+ // landing in first tab
+ cy.wait('@filterRequest');
+ cy.wait('@treemapRequest');
+
+ // creating route and stubbing filtered route
+ cy.route('POST', '/superset/explore_json/*').as('updatedChartRequest');
+
+ // apply filter
+ cy.get('.Select-control')
+ .first()
+ .find('input')
+ .first()
+ .type('South Asia{enter}', { force: true });
+
+ // send new query from same tab
+ cy.wait('@updatedChartRequest')
+ .then((xhr) => {
+ const requestFormData = xhr.request.body;
+ const requestParams = JSON.parse(requestFormData.get('form_data'));
+ expect(requestParams.extra_filters[0])
+ .deep.eq({ col: 'region', op: 'in', val: ['South Asia'] });
+ });
+
+ // click row level tab, send 1 more query
+ cy.get('.tab-content ul.nav.nav-tabs li')
+ .last()
+ .click();
+ cy.wait('@updatedChartRequest')
+ .then((xhr) => {
+ const requestFormData = xhr.request.body;
+ const requestParams = JSON.parse(requestFormData.get('form_data'));
+ expect(requestParams.extra_filters[0])
+ .deep.eq({ col: 'region', op: 'in', val: ['South Asia'] });
+ });
+
+ // click top level tab, send 1 more query
+ handleException();
+ cy.get('.dashboard-component-tabs')
+ .first()
+ .find('ul.nav.nav-tabs li')
+ .last()
+ .find('.editable-title input')
+ .click();
+ cy.wait('@updatedChartRequest')
+ .then((xhr) => {
+ const requestFormData = xhr.request.body;
+ const requestParams = JSON.parse(requestFormData.get('form_data'));
+ expect(requestParams.extra_filters[0])
+ .deep.eq({ col: 'region', op: 'in', val: ['South Asia'] });
+ });
+ });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
index de637cd2ada9a..05d3675a55152 100644
--- a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
@@ -39,7 +39,7 @@ describe('Dashboard', () => {
actions: {
addSliceToDashboard() {},
removeSliceFromDashboard() {},
- postChartFormData() {},
+ triggerQuery() {},
logEvent() {},
},
initMessages: [],
@@ -82,15 +82,15 @@ describe('Dashboard', () => {
},
};
- it('should call postChartFormData for all non-exempt slices', () => {
+ it('should call triggerQuery for all non-exempt slices', () => {
const wrapper = setup({ charts: overrideCharts, slices: overrideSlices });
- const spy = sinon.spy(props.actions, 'postChartFormData');
+ const spy = sinon.spy(props.actions, 'triggerQuery');
wrapper.instance().refreshExcept('1001');
spy.restore();
expect(spy.callCount).toBe(Object.keys(overrideCharts).length - 1);
});
- it('should not call postChartFormData for filter_immune_slices', () => {
+ it('should not call triggerQuery for filter_immune_slices', () => {
const wrapper = setup({
charts: overrideCharts,
dashboardInfo: {
@@ -103,7 +103,7 @@ describe('Dashboard', () => {
},
},
});
- const spy = sinon.spy(props.actions, 'postChartFormData');
+ const spy = sinon.spy(props.actions, 'triggerQuery');
wrapper.instance().refreshExcept();
spy.restore();
expect(spy.callCount).toBe(0);
diff --git a/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx b/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx
index ee3a006a75066..bdaa6420f6412 100644
--- a/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx
+++ b/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx
@@ -19,6 +19,7 @@
import React from 'react';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
+import sinon from 'sinon';
import { shallow } from 'enzyme';
import getInitialState from 'src/explore/reducers/getInitialState';
@@ -58,7 +59,7 @@ describe('ExploreViewContainer', () => {
wrapper = shallow(, {
context: { store },
disableLifecycleMethods: true,
- });
+ }).dive();
});
it('renders', () => {
@@ -68,14 +69,37 @@ describe('ExploreViewContainer', () => {
});
it('renders QueryAndSaveButtons', () => {
- expect(wrapper.dive().find(QueryAndSaveBtns)).toHaveLength(1);
+ expect(wrapper.find(QueryAndSaveBtns)).toHaveLength(1);
});
it('renders ControlPanelsContainer', () => {
- expect(wrapper.dive().find(ControlPanelsContainer)).toHaveLength(1);
+ expect(wrapper.find(ControlPanelsContainer)).toHaveLength(1);
});
it('renders ChartContainer', () => {
- expect(wrapper.dive().find(ChartContainer)).toHaveLength(1);
+ expect(wrapper.find(ChartContainer)).toHaveLength(1);
+ });
+
+ describe('componentWillReceiveProps()', () => {
+ it('when controls change, should call resetControls', () => {
+ expect(wrapper.instance().props.controls.viz_type.value).toBe('table');
+ const resetControls = sinon.stub(wrapper.instance().props.actions, 'resetControls');
+ const triggerQuery = sinon.stub(wrapper.instance().props.actions, 'triggerQuery');
+
+ // triggers componentWillReceiveProps
+ wrapper.setProps({
+ controls: {
+ viz_type: {
+ value: 'bar',
+ },
+ },
+ });
+ expect(resetControls.callCount).toBe(1);
+ // exploreview container should not force chart run query
+ // it should be controlled by redux state.
+ expect(triggerQuery.callCount).toBe(0);
+ resetControls.reset();
+ triggerQuery.reset();
+ });
});
});
diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx
index cfc6ce8a97053..0218a17a4f8f5 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/chart/Chart.jsx
@@ -22,6 +22,7 @@ import { Alert } from 'react-bootstrap';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import { Logger, LOG_ACTIONS_RENDER_CHART_CONTAINER } from '../logger/LogUtils';
+import { safeStringify } from '../utils/safeStringify';
import Loading from '../components/Loading';
import RefreshChartOverlay from '../components/RefreshChartOverlay';
import StackTraceMessage from '../components/StackTraceMessage';
@@ -69,25 +70,38 @@ class Chart extends React.PureComponent {
super(props);
this.handleRenderContainerFailure = this.handleRenderContainerFailure.bind(this);
}
+
componentDidMount() {
if (this.props.triggerQuery) {
- if (this.props.chartId > 0 && isFeatureEnabled(FeatureFlag.CLIENT_CACHE)) {
- // Load saved chart with a GET request
- this.props.actions.getSavedChart(
- this.props.formData,
- false,
- this.props.timeout,
- this.props.chartId,
- );
- } else {
- // Create chart with POST request
- this.props.actions.postChartFormData(
- this.props.formData,
- false,
- this.props.timeout,
- this.props.chartId,
- );
- }
+ this.runQuery();
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.triggerQuery &&
+ safeStringify(prevProps.formData) !== safeStringify(this.props.formData)
+ ) {
+ this.runQuery();
+ }
+ }
+
+ runQuery() {
+ if (this.props.chartId > 0 && isFeatureEnabled(FeatureFlag.CLIENT_CACHE)) {
+ // Load saved chart with a GET request
+ this.props.actions.getSavedChart(
+ this.props.formData,
+ false,
+ this.props.timeout,
+ this.props.chartId,
+ );
+ } else {
+ // Create chart with POST request
+ this.props.actions.postChartFormData(
+ this.props.formData,
+ false,
+ this.props.timeout,
+ this.props.chartId,
+ );
}
}
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index b26cde6f21183..b5845597a9a75 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -30,7 +30,6 @@ import {
loadStatsPropShape,
} from '../util/propShapes';
import { areObjectsEqual } from '../../reduxUtils';
-import getFormDataWithExtraFilters from '../util/charts/getFormDataWithExtraFilters';
import { LOG_ACTIONS_MOUNT_DASHBOARD } from '../../logger/LogUtils';
import OmniContainer from '../../components/OmniContainer';
@@ -40,7 +39,7 @@ const propTypes = {
actions: PropTypes.shape({
addSliceToDashboard: PropTypes.func.isRequired,
removeSliceFromDashboard: PropTypes.func.isRequired,
- postChartFormData: PropTypes.func.isRequired,
+ triggerQuery: PropTypes.func.isRequired,
logEvent: PropTypes.func.isRequired,
}).isRequired,
dashboardInfo: dashboardInfoPropShape.isRequired,
@@ -149,21 +148,7 @@ class Dashboard extends React.PureComponent {
this.getAllCharts().forEach(chart => {
// filterKey is a string, immune array contains numbers
if (String(chart.id) !== filterKey && immune.indexOf(chart.id) === -1) {
- const updatedFormData = getFormDataWithExtraFilters({
- chart,
- dashboardMetadata: this.props.dashboardInfo.metadata,
- filters: this.props.dashboardState.filters,
- colorScheme: this.props.dashboardState.colorScheme,
- colorNamespace: this.props.dashboardState.colorNamespace,
- sliceId: chart.id,
- });
-
- this.props.actions.postChartFormData(
- updatedFormData,
- false,
- this.props.timeout,
- chart.id,
- );
+ this.props.actions.triggerQuery(true, chart.id);
}
});
}
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index 12c8ff3368cbd..b21dd50c4ff3f 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -221,6 +221,7 @@ class DashboardBuilder extends React.Component {
// see isValidChild for why tabs do not increment the depth of their children
depth={DASHBOARD_ROOT_DEPTH + 1} // (topLevelTabs ? 0 : 1)}
width={width}
+ isComponentVisible={index === tabIndex}
/>
))}
diff --git a/superset/assets/src/dashboard/components/DashboardGrid.jsx b/superset/assets/src/dashboard/components/DashboardGrid.jsx
index b036ad006c64b..0666f477380a5 100644
--- a/superset/assets/src/dashboard/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/components/DashboardGrid.jsx
@@ -30,6 +30,7 @@ const propTypes = {
editMode: PropTypes.bool.isRequired,
gridComponent: componentShape.isRequired,
handleComponentDrop: PropTypes.func.isRequired,
+ isComponentVisible: PropTypes.bool.isRequired,
resizeComponent: PropTypes.func.isRequired,
width: PropTypes.number.isRequired,
};
@@ -114,6 +115,7 @@ class DashboardGrid extends React.PureComponent {
depth,
editMode,
width,
+ isComponentVisible,
} = this.props;
const columnPlusGutterWidth =
@@ -154,6 +156,7 @@ class DashboardGrid extends React.PureComponent {
index={index}
availableColumnCount={GRID_COLUMN_COUNT}
columnWidth={columnWidth}
+ isComponentVisible={isComponentVisible}
onResizeStart={this.handleResizeStart}
onResize={this.handleResize}
onResizeStop={this.handleResizeStop}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
index ff1120835534e..9b1b5f1637b76 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
@@ -37,6 +37,7 @@ const propTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
updateSliceName: PropTypes.func.isRequired,
+ isComponentVisible: PropTypes.bool,
// from redux
chart: PropTypes.shape(chartPropShape).isRequired,
@@ -61,6 +62,7 @@ const propTypes = {
const defaultProps = {
isCached: false,
+ isComponentVisible: true,
};
// we use state + shouldComponentUpdate() logic to prevent perf-wrecking
@@ -99,19 +101,27 @@ class Chart extends React.Component {
return true;
}
- for (let i = 0; i < SHOULD_UPDATE_ON_PROP_CHANGES.length; i += 1) {
- const prop = SHOULD_UPDATE_ON_PROP_CHANGES[i];
- if (nextProps[prop] !== this.props[prop]) {
+ // allow chart update/re-render only if visible:
+ // under selected tab or no tab layout
+ if (nextProps.isComponentVisible) {
+ if (nextProps.chart.triggerQuery) {
return true;
}
- }
- if (
- nextProps.width !== this.props.width ||
- nextProps.height !== this.props.height
- ) {
- clearTimeout(this.resizeTimeout);
- this.resizeTimeout = setTimeout(this.resize, RESIZE_TIMEOUT);
+ for (let i = 0; i < SHOULD_UPDATE_ON_PROP_CHANGES.length; i += 1) {
+ const prop = SHOULD_UPDATE_ON_PROP_CHANGES[i];
+ if (nextProps[prop] !== this.props[prop]) {
+ return true;
+ }
+ }
+
+ if (
+ nextProps.width !== this.props.width ||
+ nextProps.height !== this.props.height
+ ) {
+ clearTimeout(this.resizeTimeout);
+ this.resizeTimeout = setTimeout(this.resize, RESIZE_TIMEOUT);
+ }
}
return false;
diff --git a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
index 836c0e726d5de..706023a71a2e4 100644
--- a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
@@ -109,6 +109,7 @@ class ChartHolder extends React.Component {
onResizeStop,
handleComponentDrop,
editMode,
+ isComponentVisible,
} = this.props;
// inherit the size of parent columns
@@ -163,6 +164,7 @@ class ChartHolder extends React.Component {
)}
sliceName={component.meta.sliceName || ''}
updateSliceName={this.handleUpdateSliceName}
+ isComponentVisible={isComponentVisible}
/>
{editMode && (
diff --git a/superset/assets/src/dashboard/components/gridComponents/Column.jsx b/superset/assets/src/dashboard/components/gridComponents/Column.jsx
index 7170e4ac864ac..78d272b551c0a 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Column.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Column.jsx
@@ -112,6 +112,7 @@ class Column extends React.PureComponent {
onResizeStop,
handleComponentDrop,
editMode,
+ isComponentVisible,
} = this.props;
const columnItems = columnComponent.children || [];
@@ -191,6 +192,7 @@ class Column extends React.PureComponent {
onResizeStart={onResizeStart}
onResize={onResize}
onResizeStop={onResizeStop}
+ isComponentVisible={isComponentVisible}
/>
))}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Row.jsx b/superset/assets/src/dashboard/components/gridComponents/Row.jsx
index 585e57ce91412..f9076bcf4ebf4 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Row.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Row.jsx
@@ -113,6 +113,7 @@ class Row extends React.PureComponent {
onResizeStop,
handleComponentDrop,
editMode,
+ isComponentVisible,
} = this.props;
const rowItems = rowComponent.children || [];
@@ -177,6 +178,7 @@ class Row extends React.PureComponent {
onResizeStart={onResizeStart}
onResize={onResize}
onResizeStop={onResizeStop}
+ isComponentVisible={isComponentVisible}
/>
))}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
index 49a0f187fb5d7..658a1f86d23ab 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
@@ -134,6 +134,7 @@ export default class Tab extends React.PureComponent {
onResize,
onResizeStop,
editMode,
+ isComponentVisible,
} = this.props;
return (
@@ -170,6 +171,7 @@ export default class Tab extends React.PureComponent {
onResizeStart={onResizeStart}
onResize={onResize}
onResizeStop={onResizeStop}
+ isComponentVisible={isComponentVisible}
/>
))}
{/* Make bottom of tab droppable */}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
index 2b8934e9f668c..dfa0cae37f24c 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
@@ -238,6 +238,7 @@ class Tabs extends React.PureComponent {
onResize={onResize}
onResizeStop={onResizeStop}
onDropOnTab={this.handleDropOnTab}
+ isComponentVisible={selectedTabIndex === tabIndex}
/>
)}
diff --git a/superset/assets/src/dashboard/containers/Dashboard.jsx b/superset/assets/src/dashboard/containers/Dashboard.jsx
index e5cf4fb59ea0e..c1609a9cfd66c 100644
--- a/superset/assets/src/dashboard/containers/Dashboard.jsx
+++ b/superset/assets/src/dashboard/containers/Dashboard.jsx
@@ -25,7 +25,7 @@ import {
addSliceToDashboard,
removeSliceFromDashboard,
} from '../actions/dashboardState';
-import { postChartFormData } from '../../chart/chartAction';
+import { triggerQuery } from '../../chart/chartAction';
import { logEvent } from '../../logger/actions';
import getLoadStatsPerTopLevelComponent from '../util/logging/getLoadStatsPerTopLevelComponent';
@@ -64,7 +64,7 @@ function mapDispatchToProps(dispatch) {
{
addSliceToDashboard,
removeSliceFromDashboard,
- postChartFormData,
+ triggerQuery,
logEvent,
},
dispatch,
diff --git a/superset/assets/src/dashboard/containers/DashboardComponent.jsx b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
index a1a1c375e957c..2bd306033d6dd 100644
--- a/superset/assets/src/dashboard/containers/DashboardComponent.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
@@ -48,6 +48,7 @@ const propTypes = {
const defaultProps = {
directPathToChild: [],
+ isComponentVisible: true,
};
function mapStateToProps(
diff --git a/superset/assets/src/explore/components/ExploreViewContainer.jsx b/superset/assets/src/explore/components/ExploreViewContainer.jsx
index 431f2527a090d..37d41e7eb7406 100644
--- a/superset/assets/src/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/src/explore/components/ExploreViewContainer.jsx
@@ -106,7 +106,6 @@ class ExploreViewContainer extends React.Component {
componentWillReceiveProps(nextProps) {
if (nextProps.controls.viz_type.value !== this.props.controls.viz_type.value) {
this.props.actions.resetControls();
- this.props.actions.triggerQuery(true, this.props.chart.id);
}
if (
nextProps.controls.datasource &&
diff --git a/superset/cli.py b/superset/cli.py
index 7f5fe1773f879..2c0e3446fb5eb 100755
--- a/superset/cli.py
+++ b/superset/cli.py
@@ -208,6 +208,9 @@ def load_examples_run(load_test_data):
print('Loading DECK.gl demo')
data.load_deck_dash()
+ print('Loading [Tabbed dashboard]')
+ data.load_tabbed_dashboard()
+
@app.cli.command()
@click.option('--load-test-data', '-t', is_flag=True, help='Load additional test data')
diff --git a/superset/data/__init__.py b/superset/data/__init__.py
index 5090effe5fc42..b36a3002f1f3e 100644
--- a/superset/data/__init__.py
+++ b/superset/data/__init__.py
@@ -28,5 +28,6 @@
from .paris import load_paris_iris_geojson # noqa
from .random_time_series import load_random_time_series_data # noqa
from .sf_population_polygons import load_sf_population_polygons # noqa
+from .tabbed_dashboard import load_tabbed_dashboard # noqa
from .unicode_test_data import load_unicode_test_data # noqa
from .world_bank import load_world_bank_health_n_pop # noqa
diff --git a/superset/data/tabbed_dashboard.py b/superset/data/tabbed_dashboard.py
new file mode 100644
index 0000000000000..4c81f8563e0c6
--- /dev/null
+++ b/superset/data/tabbed_dashboard.py
@@ -0,0 +1,324 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Loads datasets, dashboards and slices in a new superset instance"""
+# pylint: disable=C,R,W
+import json
+import os
+import textwrap
+
+import pandas as pd
+from sqlalchemy import DateTime, String
+
+from superset import db
+from superset.connectors.sqla.models import SqlMetric
+from superset.utils import core as utils
+from .helpers import (
+ config,
+ Dash,
+ DATA_FOLDER,
+ get_example_data,
+ get_slice_json,
+ merge_slice,
+ misc_dash_slices,
+ Slice,
+ TBL,
+ update_slice_ids,
+)
+
+
+def load_tabbed_dashboard():
+ """Creating a tabbed dashboard"""
+
+ print("Creating a dashboard with nested tabs")
+ slug = 'tabbed_dash'
+ dash = db.session.query(Dash).filter_by(slug=slug).first()
+
+ if not dash:
+ dash = Dash()
+
+ # reuse charts in "World's Bank Data and create
+ # new dashboard with nested tabs
+ tabbed_dash_slices = set()
+ tabbed_dash_slices.add('Region Filter')
+ tabbed_dash_slices.add('Growth Rate')
+ tabbed_dash_slices.add('Treemap')
+ tabbed_dash_slices.add('Box plot')
+
+ js = textwrap.dedent("""\
+ {
+ "CHART-c0EjR-OZ0n": {
+ "children": [],
+ "id": "CHART-c0EjR-OZ0n",
+ "meta": {
+ "chartId": 870,
+ "height": 50,
+ "sliceName": "Box plot",
+ "width": 4
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-lV0r00f4H1",
+ "TAB-NF3dlrWGS",
+ "ROW-7G2o5uDvfo"
+ ],
+ "type": "CHART"
+ },
+ "CHART-dxV7Il74hH": {
+ "children": [],
+ "id": "CHART-dxV7Il74hH",
+ "meta": {
+ "chartId": 797,
+ "height": 50,
+ "sliceName": "Treemap",
+ "width": 4
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-lV0r00f4H1",
+ "TAB-gcQJxApOZS",
+ "ROW-3PphCz4GD"
+ ],
+ "type": "CHART"
+ },
+ "CHART-jJ5Yj1Ptaz": {
+ "children": [],
+ "id": "CHART-jJ5Yj1Ptaz",
+ "meta": {
+ "chartId": 789,
+ "height": 50,
+ "sliceName": "World's Population",
+ "width": 4
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-lV0r00f4H1",
+ "TAB-NF3dlrWGS",
+ "TABS-CSjo6VfNrj",
+ "TAB-z81Q87PD7",
+ "ROW-G73z9PIHn"
+ ],
+ "type": "CHART"
+ },
+ "CHART-z4gmEuCqQ5": {
+ "children": [],
+ "id": "CHART-z4gmEuCqQ5",
+ "meta": {
+ "chartId": 788,
+ "height": 50,
+ "sliceName": "Region Filter",
+ "width": 4
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-lV0r00f4H1",
+ "TAB-NF3dlrWGS",
+ "TABS-CSjo6VfNrj",
+ "TAB-EcNm_wh922",
+ "ROW-LCjsdSetJ"
+ ],
+ "type": "CHART"
+ },
+ "DASHBOARD_VERSION_KEY": "v2",
+ "GRID_ID": {
+ "children": [],
+ "id": "GRID_ID",
+ "type": "GRID"
+ },
+ "HEADER_ID": {
+ "id": "HEADER_ID",
+ "meta": {
+ "text": "Tabbed Dashboard"
+ },
+ "type": "HEADER"
+ },
+ "ROOT_ID": {
+ "children": [
+ "TABS-lV0r00f4H1"
+ ],
+ "id": "ROOT_ID",
+ "type": "ROOT"
+ },
+ "ROW-3PphCz4GD": {
+ "children": [
+ "CHART-dxV7Il74hH"
+ ],
+ "id": "ROW-3PphCz4GD",
+ "meta": {
+ "background": "BACKGROUND_TRANSPARENT"
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-lV0r00f4H1",
+ "TAB-gcQJxApOZS"
+ ],
+ "type": "ROW"
+ },
+ "ROW-7G2o5uDvfo": {
+ "children": [
+ "CHART-c0EjR-OZ0n"
+ ],
+ "id": "ROW-7G2o5uDvfo",
+ "meta": {
+ "background": "BACKGROUND_TRANSPARENT"
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-lV0r00f4H1",
+ "TAB-NF3dlrWGS"
+ ],
+ "type": "ROW"
+ },
+ "ROW-G73z9PIHn": {
+ "children": [
+ "CHART-jJ5Yj1Ptaz"
+ ],
+ "id": "ROW-G73z9PIHn",
+ "meta": {
+ "background": "BACKGROUND_TRANSPARENT"
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-lV0r00f4H1",
+ "TAB-NF3dlrWGS",
+ "TABS-CSjo6VfNrj",
+ "TAB-z81Q87PD7"
+ ],
+ "type": "ROW"
+ },
+ "ROW-LCjsdSetJ": {
+ "children": [
+ "CHART-z4gmEuCqQ5"
+ ],
+ "id": "ROW-LCjsdSetJ",
+ "meta": {
+ "background": "BACKGROUND_TRANSPARENT"
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-lV0r00f4H1",
+ "TAB-NF3dlrWGS",
+ "TABS-CSjo6VfNrj",
+ "TAB-EcNm_wh922"
+ ],
+ "type": "ROW"
+ },
+ "TAB-EcNm_wh922": {
+ "children": [
+ "ROW-LCjsdSetJ"
+ ],
+ "id": "TAB-EcNm_wh922",
+ "meta": {
+ "text": "row tab 1"
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-lV0r00f4H1",
+ "TAB-NF3dlrWGS",
+ "TABS-CSjo6VfNrj"
+ ],
+ "type": "TAB"
+ },
+ "TAB-NF3dlrWGS": {
+ "children": [
+ "ROW-7G2o5uDvfo",
+ "TABS-CSjo6VfNrj"
+ ],
+ "id": "TAB-NF3dlrWGS",
+ "meta": {
+ "text": "Tab A"
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-lV0r00f4H1"
+ ],
+ "type": "TAB"
+ },
+ "TAB-gcQJxApOZS": {
+ "children": [
+ "ROW-3PphCz4GD"
+ ],
+ "id": "TAB-gcQJxApOZS",
+ "meta": {
+ "text": "Tab B"
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-lV0r00f4H1"
+ ],
+ "type": "TAB"
+ },
+ "TAB-z81Q87PD7": {
+ "children": [
+ "ROW-G73z9PIHn"
+ ],
+ "id": "TAB-z81Q87PD7",
+ "meta": {
+ "text": "row tab 2"
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-lV0r00f4H1",
+ "TAB-NF3dlrWGS",
+ "TABS-CSjo6VfNrj"
+ ],
+ "type": "TAB"
+ },
+ "TABS-CSjo6VfNrj": {
+ "children": [
+ "TAB-EcNm_wh922",
+ "TAB-z81Q87PD7"
+ ],
+ "id": "TABS-CSjo6VfNrj",
+ "meta": {},
+ "parents": [
+ "ROOT_ID",
+ "TABS-lV0r00f4H1",
+ "TAB-NF3dlrWGS"
+ ],
+ "type": "TABS"
+ },
+ "TABS-lV0r00f4H1": {
+ "children": [
+ "TAB-NF3dlrWGS",
+ "TAB-gcQJxApOZS"
+ ],
+ "id": "TABS-lV0r00f4H1",
+ "meta": {},
+ "parents": [
+ "ROOT_ID"
+ ],
+ "type": "TABS"
+ }
+ }
+ """)
+ pos = json.loads(js)
+ slices = [
+ db.session.query(Slice)
+ .filter_by(slice_name=name)
+ .first()
+ for name in tabbed_dash_slices
+ ]
+
+ slices = sorted(slices, key=lambda x: x.id)
+ update_slice_ids(pos, slices)
+ dash.position_json = json.dumps(pos, indent=4)
+ dash.slices = slices
+ dash.dashboard_title = 'Tabbed Dashboard'
+ dash.slug = slug
+
+ db.session.merge(dash)
+ db.session.commit()