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()