diff --git a/superset/assets/cypress/integration/dashboard/controls.js b/superset/assets/cypress/integration/dashboard/controls.js index fe5581590c..77974a0b8c 100644 --- a/superset/assets/cypress/integration/dashboard/controls.js +++ b/superset/assets/cypress/integration/dashboard/controls.js @@ -38,8 +38,8 @@ export default () => describe('top-level controls', () => { .forEach((slice) => { const sliceRequest = `getJson_${slice.slice_id}`; sliceRequests.push(`@${sliceRequest}`); - const formData = `{"slice_id":${slice.slice_id},"viz_type":"${slice.form_data.viz_type}"}`; - cy.route('GET', `/superset/explore_json/?form_data=${formData}`).as(sliceRequest); + const formData = `{"slice_id":${slice.slice_id}}`; + cy.route('POST', `/superset/explore_json/?form_data=${formData}`).as(sliceRequest); const forceRefresh = `postJson_${slice.slice_id}_force`; forceRefreshRequests.push(`@${forceRefresh}`); diff --git a/superset/assets/cypress/integration/dashboard/edit_mode.js b/superset/assets/cypress/integration/dashboard/edit_mode.js index 280b6aae41..79198e60af 100644 --- a/superset/assets/cypress/integration/dashboard/edit_mode.js +++ b/superset/assets/cypress/integration/dashboard/edit_mode.js @@ -28,9 +28,9 @@ export default () => describe('edit mode', () => { const bootstrapData = JSON.parse(data[0].dataset.bootstrap); const dashboard = bootstrapData.dashboard_data; const boxplotChartId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'box_plot')).slice_id; - const formData = `{"slice_id":${boxplotChartId},"viz_type":"box_plot"}`; + const formData = `{"slice_id":${boxplotChartId}}`; const boxplotRequest = `/superset/explore_json/?form_data=${formData}`; - cy.route('GET', boxplotRequest).as('boxplotRequest'); + cy.route('POST', boxplotRequest).as('boxplotRequest'); }); cy.get('.dashboard-header').contains('Edit dashboard').click(); diff --git a/superset/assets/cypress/integration/dashboard/filter.js b/superset/assets/cypress/integration/dashboard/filter.js index f37c8c849e..97b40a4150 100644 --- a/superset/assets/cypress/integration/dashboard/filter.js +++ b/superset/assets/cypress/integration/dashboard/filter.js @@ -39,9 +39,9 @@ export default () => describe('dashboard filter', () => { it('should apply filter', () => { const aliases = []; - const formData = `{"slice_id":${filterId},"viz_type":"filter_box"}`; + const formData = `{"slice_id":${filterId}}`; const filterRoute = `/superset/explore_json/?form_data=${formData}`; - cy.route('GET', filterRoute).as('fetchFilter'); + cy.route('POST', filterRoute).as('fetchFilter'); cy.wait('@fetchFilter'); sliceIds .filter(id => (parseInt(id, 10) !== filterId)) diff --git a/superset/assets/cypress/integration/dashboard/load.js b/superset/assets/cypress/integration/dashboard/load.js index 79daa30422..c6427c7320 100644 --- a/superset/assets/cypress/integration/dashboard/load.js +++ b/superset/assets/cypress/integration/dashboard/load.js @@ -34,8 +34,8 @@ export default () => describe('load', () => { // then define routes and create alias for each requests slices.forEach((slice) => { const alias = `getJson_${slice.slice_id}`; - const formData = `{"slice_id":${slice.slice_id},"viz_type":"${slice.form_data.viz_type}"}`; - cy.route('GET', `/superset/explore_json/?form_data=${formData}`).as(alias); + const formData = `{"slice_id":${slice.slice_id}}`; + cy.route('POST', `/superset/explore_json/?form_data=${formData}`).as(alias); aliases.push(`@${alias}`); }); }); diff --git a/superset/assets/cypress/integration/dashboard/save.js b/superset/assets/cypress/integration/dashboard/save.js index 1d26ac219a..772862d5b8 100644 --- a/superset/assets/cypress/integration/dashboard/save.js +++ b/superset/assets/cypress/integration/dashboard/save.js @@ -56,9 +56,9 @@ export default () => describe('save', () => { cy.wait('@copyRequest'); // should have box_plot chart - const formData = `{"slice_id":${boxplotChartId},"viz_type":"box_plot"}`; + const formData = `{"slice_id":${boxplotChartId}}`; const boxplotRequest = `/superset/explore_json/?form_data=${formData}`; - cy.route('GET', boxplotRequest).as('boxplotRequest'); + cy.route('POST', boxplotRequest).as('boxplotRequest'); cy.wait('@boxplotRequest'); cy.get('.grid-container .box_plot').should('be.exist'); diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx index c0d391673f..cfc6ce8a97 100644 --- a/superset/assets/src/chart/Chart.jsx +++ b/superset/assets/src/chart/Chart.jsx @@ -20,6 +20,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { Alert } from 'react-bootstrap'; +import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; import { Logger, LOG_ACTIONS_RENDER_CHART_CONTAINER } from '../logger/LogUtils'; import Loading from '../components/Loading'; import RefreshChartOverlay from '../components/RefreshChartOverlay'; @@ -70,7 +71,7 @@ class Chart extends React.PureComponent { } componentDidMount() { if (this.props.triggerQuery) { - if (this.props.chartId > 0) { + if (this.props.chartId > 0 && isFeatureEnabled(FeatureFlag.CLIENT_CACHE)) { // Load saved chart with a GET request this.props.actions.getSavedChart( this.props.formData, diff --git a/superset/assets/src/chart/chartAction.js b/superset/assets/src/chart/chartAction.js index 3909dadd85..dc9eb322d4 100644 --- a/superset/assets/src/chart/chartAction.js +++ b/superset/assets/src/chart/chartAction.js @@ -21,6 +21,7 @@ /* eslint no-param-reassign: ["error", { "props": false }] */ import { t } from '@superset-ui/translation'; import { SupersetClient } from '@superset-ui/connection'; +import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; import { getExploreUrlAndPayload, getAnnotationJsonUrl } from '../explore/exploreUtils'; import { requiresQuery, ANNOTATION_SOURCE_TYPES } from '../modules/AnnotationTypes'; import { addDangerToast } from '../messageToasts/actions'; @@ -194,7 +195,9 @@ export function exploreJSON(formData, force = false, timeout = 60, key, method) }; } - const clientMethod = method === 'GET' ? SupersetClient.get : SupersetClient.post; + const clientMethod = method === 'GET' && isFeatureEnabled(FeatureFlag.CLIENT_CACHE) + ? SupersetClient.get + : SupersetClient.post; const queryPromise = clientMethod(querySettings) .then(({ json }) => { dispatch(logEvent(LOG_ACTIONS_LOAD_CHART, { diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx index 92d3b1df62..8f3f831e3f 100644 --- a/superset/assets/src/dashboard/components/Header.jsx +++ b/superset/assets/src/dashboard/components/Header.jsx @@ -222,7 +222,7 @@ class Header extends React.PureComponent { colorScheme, colorNamespace, ); - const labelColors = scale.getColorMap(); + const labelColors = colorScheme ? scale.getColorMap() : {}; const data = { positions, expanded_slices: expandedSlices, diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx index 1873f0c313..d926bc8aa5 100644 --- a/superset/assets/src/dashboard/components/SaveModal.jsx +++ b/superset/assets/src/dashboard/components/SaveModal.jsx @@ -110,7 +110,7 @@ class SaveModal extends React.PureComponent { colorScheme, colorNamespace, ); - const labelColors = scale.getColorMap(); + const labelColors = colorScheme ? scale.getColorMap() : {}; const data = { positions, css, diff --git a/superset/assets/src/featureFlags.ts b/superset/assets/src/featureFlags.ts index 54eee94829..8638a5471f 100644 --- a/superset/assets/src/featureFlags.ts +++ b/superset/assets/src/featureFlags.ts @@ -21,6 +21,7 @@ export enum FeatureFlag { SCOPED_FILTER = 'SCOPED_FILTER', OMNIBAR = 'OMNIBAR', + CLIENT_CACHE = 'CLIENT_CACHE', } export type FeatureFlagMap = { diff --git a/superset/config.py b/superset/config.py index b402fec9aa..df14b0f2c1 100644 --- a/superset/config.py +++ b/superset/config.py @@ -200,7 +200,10 @@ LANGUAGES = { # For example, DEFAULT_FEATURE_FLAGS = { 'FOO': True, 'BAR': False } here # and FEATURE_FLAGS = { 'BAR': True, 'BAZ': True } in superset_config.py # will result in combined feature flags of { 'FOO': True, 'BAR': True, 'BAZ': True } -DEFAULT_FEATURE_FLAGS = {} +DEFAULT_FEATURE_FLAGS = { + # Experimental feature introducing a client (browser) cache + 'CLIENT_CACHE': False, +} # A function that receives a dict of all feature flags # (DEFAULT_FEATURE_FLAGS merged with FEATURE_FLAGS) diff --git a/superset/models/core.py b/superset/models/core.py index f9a2a70a12..fb5850c0fa 100644 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -749,6 +749,10 @@ class Database(Model, AuditMixinNullable, ImportMixin): def table_cache_timeout(self): return self.metadata_cache_timeout.get('table_cache_timeout') + @property + def default_schemas(self): + return self.get_extra().get('default_schemas', []) + @classmethod def get_password_masked_url_from_uri(cls, uri): url = make_url(uri) diff --git a/superset/views/core.py b/superset/views/core.py index 569708ab7b..e22acb7410 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1590,6 +1590,16 @@ class Superset(BaseSupersetView): table_names = [tn for tn in table_names if substr in tn] view_names = [vn for vn in view_names if substr in vn] + if not schema and database.default_schemas: + def get_schema(tbl_or_view_name): + return tbl_or_view_name.split('.')[0] if '.' in tbl_or_view_name else None + + user_schema = g.user.email.split('@')[0] + valid_schemas = set(database.default_schemas + [user_schema]) + + table_names = [tn for tn in table_names if get_schema(tn) in valid_schemas] + view_names = [vn for vn in view_names if get_schema(vn) in valid_schemas] + max_items = config.get('MAX_TABLE_NAMES') or len(table_names) total_items = len(table_names) + len(view_names) max_tables = len(table_names) diff --git a/superset/viz.py b/superset/viz.py index 24a773c6b2..123c361d1b 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -1112,16 +1112,22 @@ class NVD3TimeSeriesViz(NVD3Viz): series_title = series_title + (title_suffix,) values = [] + non_nan_cnt = 0 for ds in df.index: if ds in ys: d = { 'x': ds, 'y': ys[ds], } + if not np.isnan(ys[ds]): + non_nan_cnt += 1 else: d = {} values.append(d) + if non_nan_cnt == 0: + continue + d = { 'key': series_title, 'values': values, @@ -1224,7 +1230,9 @@ class NVD3TimeSeriesViz(NVD3Viz): comparison_type = fd.get('comparison_type') or 'values' df = self.process_data(df) if comparison_type == 'values': - chart_data = self.to_series(df) + # Filter out series with all NaN + chart_data = self.to_series(df.dropna(axis=1, how='all')) + for i, (label, df2) in enumerate(self._extra_chart_data): chart_data.extend( self.to_series(