diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.ts index 99f5f729f99b2..b03cdd27965f2 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.ts @@ -23,28 +23,12 @@ import { } from './dashboard.helper'; describe('Dashboard load', () => { - beforeEach(() => { + before(() => { cy.login(); + cy.visit(WORLD_HEALTH_DASHBOARD); }); it('should load dashboard', () => { - cy.visit(WORLD_HEALTH_DASHBOARD); WORLD_HEALTH_CHARTS.forEach(waitForChartLoad); }); - - it('should load in edit mode', () => { - cy.visit(`${WORLD_HEALTH_DASHBOARD}?edit=true&standalone=true`); - cy.get('[data-test="discard-changes-button"]').should('be.visible'); - }); - - it('should load in standalone mode', () => { - cy.visit(`${WORLD_HEALTH_DASHBOARD}?edit=true&standalone=true`); - cy.get('#app-menu').should('not.exist'); - }); - - it('should load in edit/standalone mode', () => { - cy.visit(`${WORLD_HEALTH_DASHBOARD}?edit=true&standalone=true`); - cy.get('[data-test="discard-changes-button"]').should('be.visible'); - cy.get('#app-menu').should('not.exist'); - }); }); diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts index fbb1aea9d0614..f279b8e0643d8 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts @@ -61,12 +61,9 @@ describe('Nativefilters', () => { .click() .type('Country name'); - cy.get('.ant-modal') - .find('[data-test="datasource-input"]') - .click() - .type('wb_health_population'); + cy.get('.ant-modal').find('[data-test="datasource-input"]').click(); - cy.get('.ant-modal [data-test="datasource-input"] .Select__menu') + cy.get('[data-test="datasource-input"]') .contains('wb_health_population') .click(); @@ -158,12 +155,9 @@ describe('Nativefilters', () => { .click() .type('Country name'); - cy.get('.ant-modal') - .find('[data-test="datasource-input"]') - .click() - .type('wb_health_population'); + cy.get('.ant-modal').find('[data-test="datasource-input"]').click(); - cy.get('.ant-modal [data-test="datasource-input"] .Select__menu') + cy.get('[data-test="datasource-input"]') .contains('wb_health_population') .click(); @@ -193,10 +187,9 @@ describe('Nativefilters', () => { cy.get('.ant-modal') .find('[data-test="datasource-input"]') .last() - .click() - .type('wb_health_population'); + .click(); - cy.get('.ant-modal [data-test="datasource-input"] .Select__menu') + cy.get('[data-test="datasource-input"]') .last() .contains('wb_health_population') .click(); diff --git a/superset-frontend/src/chart/chartReducer.ts b/superset-frontend/src/chart/chartReducer.ts index d6e42dfa87f60..3f68c043428d7 100644 --- a/superset-frontend/src/chart/chartReducer.ts +++ b/superset-frontend/src/chart/chartReducer.ts @@ -18,10 +18,9 @@ */ /* eslint camelcase: 0 */ import { t } from '@superset-ui/core'; -import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate'; import { ChartState } from 'src/explore/types'; import { getFormDataFromControls } from 'src/explore/controlUtils'; -import { now } from 'src/modules/dates'; +import { now } from '../modules/dates'; import * as actions from './chartAction'; export const chart: ChartState = { @@ -193,9 +192,7 @@ export default function chartReducer( delete charts[key]; return charts; } - if (action.type === HYDRATE_DASHBOARD) { - return { ...action.data.charts }; - } + if (action.type in actionHandlers) { return { ...charts, diff --git a/superset-frontend/src/common/hooks/apiResources/dashboards.ts b/superset-frontend/src/common/hooks/apiResources/dashboards.ts deleted file mode 100644 index 0bb21f16bfb60..0000000000000 --- a/superset-frontend/src/common/hooks/apiResources/dashboards.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * 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 Dashboard from 'src/types/Dashboard'; -import { useApiV1Resource, useTransformedResource } from './apiResources'; - -export const useDashboard = (idOrSlug: string | number) => - useTransformedResource( - useApiV1Resource(`/api/v1/dashboard/${idOrSlug}`), - dashboard => ({ - ...dashboard, - metadata: JSON.parse(dashboard.json_metadata), - position_data: JSON.parse(dashboard.position_json), - }), - ); - -// gets the chart definitions for a dashboard -export const useDashboardCharts = (idOrSlug: string | number) => - useApiV1Resource(`/api/v1/dashboard/${idOrSlug}/charts`); - -// gets the datasets for a dashboard -// important: this endpoint only returns the fields in the dataset -// that are necessary for rendering the given dashboard -export const useDashboardDatasets = (idOrSlug: string | number) => - useApiV1Resource(`/api/v1/dashboard/${idOrSlug}/datasets`); diff --git a/superset-frontend/src/common/hooks/apiResources/index.ts b/superset-frontend/src/common/hooks/apiResources/index.ts index 5e63920731144..8befc73735770 100644 --- a/superset-frontend/src/common/hooks/apiResources/index.ts +++ b/superset-frontend/src/common/hooks/apiResources/index.ts @@ -26,5 +26,4 @@ export { // A central catalog of API Resource hooks. // Add new API hooks here, organized under // different files for different resource types. -export * from './charts'; -export * from './dashboards'; +export { useChartOwnerNames } from './charts'; diff --git a/superset-frontend/src/components/ErrorBoundary/index.jsx b/superset-frontend/src/components/ErrorBoundary/index.jsx index 0a1d0c7c46510..7bc00758afd98 100644 --- a/superset-frontend/src/components/ErrorBoundary/index.jsx +++ b/superset-frontend/src/components/ErrorBoundary/index.jsx @@ -38,7 +38,7 @@ export default class ErrorBoundary extends React.Component { } componentDidCatch(error, info) { - if (this.props.onError) this.props.onError(error, info); + this.props.onError(error, info); this.setState({ error, info }); } diff --git a/superset-frontend/src/dashboard/App.jsx b/superset-frontend/src/dashboard/App.jsx index 43d00f5a579c5..da06a0130fa31 100644 --- a/superset-frontend/src/dashboard/App.jsx +++ b/superset-frontend/src/dashboard/App.jsx @@ -25,28 +25,22 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import { DynamicPluginProvider } from 'src/components/DynamicPlugins'; import setupApp from '../setup/setupApp'; import setupPlugins from '../setup/setupPlugins'; -import DashboardPage from './containers/DashboardPage'; +import DashboardContainer from './containers/Dashboard'; import { theme } from '../preamble'; setupApp(); setupPlugins(); -const App = ({ store }) => { - const dashboardIdOrSlug = window.location.pathname.split('/')[3]; - return ( - - - - - - - - - - ); -}; +const App = ({ store }) => ( + + + + + + + + + +); export default hot(App); diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts index 2fc5541164da0..fca59f2bafd70 100644 --- a/superset-frontend/src/dashboard/actions/nativeFilters.ts +++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts @@ -19,16 +19,12 @@ import { makeApi } from '@superset-ui/core'; import { Dispatch } from 'redux'; -import { - Filter, - FilterConfiguration, -} from 'src/dashboard/components/nativeFilters/types'; +import { FilterConfiguration } from 'src/dashboard/components/nativeFilters/types'; import { DataMaskType, DataMaskStateWithId } from 'src/dataMask/types'; import { SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE, SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL, } from 'src/dataMask/actions'; -import { HYDRATE_DASHBOARD } from './hydrate'; import { dashboardInfoChanged } from './dashboardInfo'; import { DashboardInfo, FilterSet } from '../reducers/types'; @@ -109,18 +105,6 @@ export const setFilterConfiguration = ( } }; -type BootstrapData = { - nativeFilters: { - filters: Filter; - filtersState: object; - }; -}; - -export interface SetBooststapData { - type: typeof HYDRATE_DASHBOARD; - data: BootstrapData; -} - export const setFilterSetsConfiguration = ( filterSetsConfig: FilterSet[], ) => async (dispatch: Dispatch, getState: () => any) => { @@ -189,5 +173,4 @@ export type AnyFilterAction = | SetFilterSetsConfigBegin | SetFilterSetsConfigComplete | SetFilterSetsConfigFail - | SaveFilterSets - | SetBooststapData; + | SaveFilterSets; diff --git a/superset-frontend/src/dashboard/components/DashboardGrid.jsx b/superset-frontend/src/dashboard/components/DashboardGrid.jsx index 9fb0fb5fd55e5..6889c91ab3de2 100644 --- a/superset-frontend/src/dashboard/components/DashboardGrid.jsx +++ b/superset-frontend/src/dashboard/components/DashboardGrid.jsx @@ -123,6 +123,7 @@ class DashboardGrid extends React.PureComponent { width, isComponentVisible, } = this.props; + const columnPlusGutterWidth = (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT; diff --git a/superset-frontend/src/dashboard/components/SaveModal.tsx b/superset-frontend/src/dashboard/components/SaveModal.tsx index 1d3141df05f8e..0bbc327767558 100644 --- a/superset-frontend/src/dashboard/components/SaveModal.tsx +++ b/superset-frontend/src/dashboard/components/SaveModal.tsx @@ -140,7 +140,7 @@ class SaveModal extends React.PureComponent { // check refresh frequency is for current session or persist const refreshFrequency = shouldPersistRefreshFrequency ? currentRefreshFrequency - : dashboardInfo.metadata?.refresh_frequency; // eslint-disable camelcase + : dashboardInfo.metadata.refresh_frequency; // eslint-disable camelcase const data = { positions, diff --git a/superset-frontend/src/dashboard/containers/DashboardHeader.jsx b/superset-frontend/src/dashboard/containers/DashboardHeader.jsx index 6351561c71fc9..3ffd51a6bbe93 100644 --- a/superset-frontend/src/dashboard/containers/DashboardHeader.jsx +++ b/superset-frontend/src/dashboard/containers/DashboardHeader.jsx @@ -85,7 +85,7 @@ function mapStateToProps({ maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded, lastModifiedTime: Math.max( dashboardState.lastModifiedTime, - dashboardInfo.last_modified_time, + dashboardInfo.lastModifiedTime, ), editMode: !!dashboardState.editMode, slug: dashboardInfo.slug, diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx deleted file mode 100644 index 6e391e0b00c31..0000000000000 --- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/** - * 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 React, { useEffect, useState, FC } from 'react'; -import { useDispatch } from 'react-redux'; -import Loading from 'src/components/Loading'; -import ErrorBoundary from 'src/components/ErrorBoundary'; -import { - useDashboard, - useDashboardCharts, - useDashboardDatasets, -} from 'src/common/hooks/apiResources'; -import { ResourceStatus } from 'src/common/hooks/apiResources/apiResources'; -import { usePrevious } from 'src/common/hooks/usePrevious'; -import { hydrateDashboard } from 'src/dashboard/actions/hydrate'; -import DashboardContainer from 'src/dashboard/containers/Dashboard'; - -interface DashboardRouteProps { - dashboardIdOrSlug: string; -} - -const DashboardPage: FC = ({ - dashboardIdOrSlug, // eventually get from react router -}) => { - const dispatch = useDispatch(); - const [isLoaded, setLoaded] = useState(false); - const dashboardResource = useDashboard(dashboardIdOrSlug); - const chartsResource = useDashboardCharts(dashboardIdOrSlug); - const datasetsResource = useDashboardDatasets(dashboardIdOrSlug); - const isLoading = [dashboardResource, chartsResource, datasetsResource].some( - resource => resource.status === ResourceStatus.LOADING, - ); - const wasLoading = usePrevious(isLoading); - const error = [dashboardResource, chartsResource, datasetsResource].find( - resource => resource.status === ResourceStatus.ERROR, - )?.error; - useEffect(() => { - if ( - wasLoading && - dashboardResource.status === ResourceStatus.COMPLETE && - chartsResource.status === ResourceStatus.COMPLETE && - datasetsResource.status === ResourceStatus.COMPLETE - ) { - dispatch( - hydrateDashboard( - dashboardResource.result, - chartsResource.result, - datasetsResource.result, - ), - ); - setLoaded(true); - } - }, [ - dispatch, - wasLoading, - dashboardResource, - chartsResource, - datasetsResource, - ]); - - if (error) throw error; // caught in error boundary - - if (!isLoaded) return ; - return ; -}; - -const DashboardPageWithErrorBoundary = ({ - dashboardIdOrSlug, -}: DashboardRouteProps) => ( - - - -); - -export default DashboardPageWithErrorBoundary; diff --git a/superset-frontend/src/dashboard/index.jsx b/superset-frontend/src/dashboard/index.jsx index 1a287c072ecf3..5d696bd3f1e15 100644 --- a/superset-frontend/src/dashboard/index.jsx +++ b/superset-frontend/src/dashboard/index.jsx @@ -22,6 +22,7 @@ import thunk from 'redux-thunk'; import { createStore, applyMiddleware, compose } from 'redux'; import { initFeatureFlags } from 'src/featureFlags'; import { initEnhancer } from '../reduxUtils'; +import getInitialState from './reducers/getInitialState'; import rootReducer from './reducers/index'; import logger from '../middleware/loggerMiddleware'; import App from './App'; @@ -29,16 +30,10 @@ import App from './App'; const appContainer = document.getElementById('app'); const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap')); initFeatureFlags(bootstrapData.common.feature_flags); - -const initialState = { - user: bootstrapData.user, - common: bootstrapData.common, - datasources: bootstrapData.datasources, -}; - +const initState = getInitialState(bootstrapData); const store = createStore( rootReducer, - initialState, + initState, compose(applyMiddleware(thunk, logger), initEnhancer(false)), ); diff --git a/superset-frontend/src/dashboard/reducers/dashboardFilters.js b/superset-frontend/src/dashboard/reducers/dashboardFilters.js index d31af825717bd..f508c1bfe3868 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardFilters.js +++ b/superset-frontend/src/dashboard/reducers/dashboardFilters.js @@ -25,7 +25,6 @@ import { UPDATE_LAYOUT_COMPONENTS, UPDATE_DASHBOARD_FILTERS_SCOPE, } from '../actions/dashboardFilters'; -import { HYDRATE_DASHBOARD } from '../actions/hydrate'; import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox'; import { DASHBOARD_ROOT_ID } from '../util/constants'; import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata'; @@ -162,10 +161,6 @@ export default function dashboardFiltersReducer(dashboardFilters = {}, action) { return updatedFilters; } - if (action.type === HYDRATE_DASHBOARD) { - return action.data.dashboardFilters; - } - if (action.type in actionHandlers) { const updatedFilters = { ...dashboardFilters, @@ -173,6 +168,7 @@ export default function dashboardFiltersReducer(dashboardFilters = {}, action) { dashboardFilters[action.chartId], ), }; + if (CHANGE_FILTER_VALUE_ACTIONS.includes(action.type)) { buildActiveFilters({ dashboardFilters: updatedFilters }); } diff --git a/superset-frontend/src/dashboard/reducers/dashboardInfo.js b/superset-frontend/src/dashboard/reducers/dashboardInfo.js index fdd39fae12324..01346d7a4f29f 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardInfo.js +++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.js @@ -18,7 +18,6 @@ */ import { DASHBOARD_INFO_UPDATED } from '../actions/dashboardInfo'; -import { HYDRATE_DASHBOARD } from '../actions/hydrate'; export default function dashboardStateReducer(state = {}, action) { switch (action.type) { @@ -27,13 +26,7 @@ export default function dashboardStateReducer(state = {}, action) { ...state, ...action.newInfo, // server-side compare last_modified_time in second level - last_modified_time: Math.round(new Date().getTime() / 1000), - }; - case HYDRATE_DASHBOARD: - return { - ...state, - ...action.data.dashboardInfo, - // set async api call data + lastModifiedTime: Math.round(new Date().getTime() / 1000), }; default: return state; diff --git a/superset-frontend/src/dashboard/reducers/dashboardLayout.js b/superset-frontend/src/dashboard/reducers/dashboardLayout.js index 30ad33c62b6c1..ffc56132a83c5 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardLayout.js +++ b/superset-frontend/src/dashboard/reducers/dashboardLayout.js @@ -43,15 +43,7 @@ import { DASHBOARD_TITLE_CHANGED, } from '../actions/dashboardLayout'; -import { HYDRATE_DASHBOARD } from '../actions/hydrate'; - const actionHandlers = { - [HYDRATE_DASHBOARD](state, action) { - return { - ...action.data.dashboardLayout.present, - }; - }, - [UPDATE_COMPONENTS](state, action) { const { payload: { nextComponents }, diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js index 6f162084708f4..b948e2c4a3497 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.js @@ -36,13 +36,9 @@ import { SET_FOCUSED_FILTER_FIELD, UNSET_FOCUSED_FILTER_FIELD, } from '../actions/dashboardState'; -import { HYDRATE_DASHBOARD } from '../actions/hydrate'; export default function dashboardStateReducer(state = {}, action) { const actionHandlers = { - [HYDRATE_DASHBOARD]() { - return { ...state, ...action.data.dashboardState }; - }, [UPDATE_CSS]() { return { ...state, css: action.css }; }, diff --git a/superset-frontend/src/dashboard/reducers/datasources.js b/superset-frontend/src/dashboard/reducers/datasources.js index 616c3c134ffdb..0cf7e1bac4558 100644 --- a/superset-frontend/src/dashboard/reducers/datasources.js +++ b/superset-frontend/src/dashboard/reducers/datasources.js @@ -17,29 +17,22 @@ * under the License. */ import { SET_DATASOURCE } from '../actions/datasources'; -import { HYDRATE_DASHBOARD } from '../actions/hydrate'; export default function datasourceReducer(datasources = {}, action) { const actionHandlers = { - [HYDRATE_DASHBOARD]() { - return action.data.datasources; - }, [SET_DATASOURCE]() { return action.datasource; }, }; if (action.type in actionHandlers) { - if (action.key) { - return { - ...datasources, - [action.key]: actionHandlers[action.type]( - datasources[action.key], - action, - ), - }; - } - return actionHandlers[action.type](); + return { + ...datasources, + [action.key]: actionHandlers[action.type]( + datasources[action.key], + action, + ), + }; } return datasources; } diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/reducers/getInitialState.js similarity index 58% rename from superset-frontend/src/dashboard/actions/hydrate.js rename to superset-frontend/src/dashboard/reducers/getInitialState.js index 8065bf625fe3e..19ea54e789f1f 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -17,82 +17,46 @@ * under the License. */ /* eslint-disable camelcase */ -import { isString, keyBy } from 'lodash'; +import { isString } from 'lodash'; import shortid from 'shortid'; import { CategoricalColorNamespace } from '@superset-ui/core'; -import querystring from 'query-string'; -import { chart } from 'src/chart/chartReducer'; import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities'; import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters'; import { getParam } from 'src/modules/utils'; import { applyDefaultFormData } from 'src/explore/store'; import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters'; -import getPermissions from 'src/dashboard/util/getPermissions'; import { DASHBOARD_FILTER_SCOPE_GLOBAL, dashboardFilter, -} from 'src/dashboard/reducers/dashboardFilters'; +} from './dashboardFilters'; +import { chart } from '../../chart/chartReducer'; import { DASHBOARD_HEADER_ID, GRID_DEFAULT_CHART_WIDTH, GRID_COLUMN_COUNT, -} from 'src/dashboard/util/constants'; +} from '../util/constants'; import { DASHBOARD_HEADER_TYPE, CHART_TYPE, ROW_TYPE, -} from 'src/dashboard/util/componentTypes'; -import findFirstParentContainerId from 'src/dashboard/util/findFirstParentContainer'; -import getEmptyLayout from 'src/dashboard/util/getEmptyLayout'; -import getFilterConfigsFromFormdata from 'src/dashboard/util/getFilterConfigsFromFormdata'; -import getLocationHash from 'src/dashboard/util/getLocationHash'; -import newComponentFactory from 'src/dashboard/util/newComponentFactory'; -import { TIME_RANGE } from 'src/visualizations/FilterBox/FilterBox'; +} from '../util/componentTypes'; +import findFirstParentContainerId from '../util/findFirstParentContainer'; +import getEmptyLayout from '../util/getEmptyLayout'; +import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata'; +import getLocationHash from '../util/getLocationHash'; +import newComponentFactory from '../util/newComponentFactory'; +import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox'; -const reservedQueryParams = new Set(['standalone', 'edit']); - -/** - * Returns the url params that are used to customize queries - * in datasets built using sql lab. - * We may want to extract this to some kind of util in the future. - */ -const extractUrlParams = queryParams => - Object.entries(queryParams).reduce((acc, [key, value]) => { - if (reservedQueryParams.has(key)) return acc; - // if multiple url params share the same key (?foo=bar&foo=baz), they will appear as an array. - // Only one value can be used for a given query param, so we just take the first one. - if (Array.isArray(value)) { - return { - ...acc, - [key]: value[0], - }; - } - return { ...acc, [key]: value }; - }, {}); - -export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD'; - -export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( - dispatch, - getState, -) => { - const { user, common } = getState(); - const { metadata } = dashboardData; - const queryParams = querystring.parse(window.location.search); - const urlParams = extractUrlParams(queryParams); - const editMode = queryParams.edit === 'true'; +export default function getInitialState(bootstrapData) { + const { user_id, datasources, common, editMode, urlParams } = bootstrapData; + const dashboard = { ...bootstrapData.dashboard_data }; let preselectFilters = {}; - - chartData.forEach(chart => { - // eslint-disable-next-line no-param-reassign - chart.slice_id = chart.form_data.slice_id; - }); try { // allow request parameter overwrite dashboard metadata preselectFilters = JSON.parse( - getParam('preselect_filters') || metadata.default_filters, + getParam('preselect_filters') || dashboard.metadata.default_filters, ); } catch (e) { // @@ -100,12 +64,12 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( // Priming the color palette with user's label-color mapping provided in // the dashboard's JSON metadata - if (metadata?.label_colors) { - const scheme = metadata.color_scheme; - const namespace = metadata.color_namespace; - const colorMap = isString(metadata.label_colors) - ? JSON.parse(metadata.label_colors) - : metadata.label_colors; + if (dashboard.metadata && dashboard.metadata.label_colors) { + const scheme = dashboard.metadata.color_scheme; + const namespace = dashboard.metadata.color_namespace; + const colorMap = isString(dashboard.metadata.label_colors) + ? JSON.parse(dashboard.metadata.label_colors) + : dashboard.metadata.label_colors; Object.keys(colorMap).forEach(label => { CategoricalColorNamespace.getScale(scheme, namespace).setColor( label, @@ -115,11 +79,11 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( } // dashboard layout - const { position_data } = dashboardData; - // new dash: position_json could be {} or null + const { position_json: positionJson } = dashboard; + // new dash: positionJson could be {} or null const layout = - position_data && Object.keys(position_data).length > 0 - ? position_data + positionJson && Object.keys(positionJson).length > 0 + ? positionJson : getEmptyLayout(); // create a lookup to sync layout names with slice names @@ -136,13 +100,13 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( let newSlicesContainer; let newSlicesContainerWidth = 0; - const filterScopes = metadata?.filter_scopes || {}; + const filterScopes = dashboard.metadata.filter_scopes || {}; const chartQueries = {}; const dashboardFilters = {}; const slices = {}; const sliceIds = new Set(); - chartData.forEach(slice => { + dashboard.slices.forEach(slice => { const key = slice.slice_id; const form_data = { ...slice.form_data, @@ -276,7 +240,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( id: DASHBOARD_HEADER_ID, type: DASHBOARD_HEADER_TYPE, meta: { - text: dashboardData.dashboard_title, + text: dashboard.dashboard_title, }, }; @@ -295,67 +259,54 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( } const nativeFilters = getInitialNativeFilterState({ - filterConfig: metadata?.native_filter_configuration || [], - filterSetsConfig: metadata?.filter_sets_configuration || [], + filterConfig: dashboard.metadata.native_filter_configuration || [], + filterSetsConfig: dashboard.metadata.filter_sets_configuration || [], }); - const { roles } = getState().user; - - return dispatch({ - type: HYDRATE_DASHBOARD, - data: { - datasources: keyBy(datasourcesData, 'uid'), - sliceEntities: { ...initSliceEntities, slices, isLoading: false }, - charts: chartQueries, - // read-only data - dashboardInfo: { - ...dashboardData, - userId: String(user.userId), // legacy, please use state.user instead - dash_edit_perm: getPermissions('can_write', 'Dashboard', roles), - dash_save_perm: getPermissions('can_save_dash', 'Superset', roles), - dash_share_perm: getPermissions( - 'can_share_dashboard', - 'Superset', - roles, - ), - superset_can_explore: getPermissions('can_explore', 'Superset', roles), - superset_can_share: getPermissions( - 'can_share_chart', - 'Superset', - roles, - ), - superset_can_csv: getPermissions('can_csv', 'Superset', roles), - slice_can_edit: getPermissions('can_slice', 'Superset', roles), - common: { - // legacy, please use state.common instead - flash_messages: common.flash_messages, - conf: common.conf, - }, - }, - dashboardFilters, - nativeFilters, - dashboardState: { - sliceIds: Array.from(sliceIds), - directPathToChild, - directPathLastUpdated: Date.now(), - focusedFilterField: null, - expandedSlices: metadata?.expanded_slices || {}, - refreshFrequency: metadata?.refresh_frequency || 0, - // dashboard viewers can set refresh frequency for the current visit, - // only persistent refreshFrequency will be saved to backend - shouldPersistRefreshFrequency: false, - css: dashboardData.css || '', - colorNamespace: metadata?.color_namespace || null, - colorScheme: metadata?.color_scheme || null, - editMode: getPermissions('can_write', 'Dashboard', roles) && editMode, - isPublished: dashboardData.published, - hasUnsavedChanges: false, - maxUndoHistoryExceeded: false, - lastModifiedTime: dashboardData.changed_on, + return { + datasources, + sliceEntities: { ...initSliceEntities, slices, isLoading: false }, + charts: chartQueries, + // read-only data + dashboardInfo: { + id: dashboard.id, + slug: dashboard.slug, + metadata: dashboard.metadata, + userId: user_id, + dash_edit_perm: dashboard.dash_edit_perm, + dash_save_perm: dashboard.dash_save_perm, + superset_can_explore: dashboard.superset_can_explore, + superset_can_csv: dashboard.superset_can_csv, + slice_can_edit: dashboard.slice_can_edit, + common: { + flash_messages: common.flash_messages, + conf: common.conf, }, - dashboardLayout, - messageToasts: [], - impressionId: shortid.generate(), + lastModifiedTime: dashboard.last_modified_time, }, - }); -}; + dashboardFilters, + nativeFilters, + dashboardState: { + sliceIds: Array.from(sliceIds), + directPathToChild, + directPathLastUpdated: Date.now(), + focusedFilterField: null, + expandedSlices: dashboard.metadata.expanded_slices || {}, + refreshFrequency: dashboard.metadata.refresh_frequency || 0, + // dashboard viewers can set refresh frequency for the current visit, + // only persistent refreshFrequency will be saved to backend + shouldPersistRefreshFrequency: false, + css: dashboard.css || '', + colorNamespace: dashboard.metadata.color_namespace, + colorScheme: dashboard.metadata.color_scheme, + editMode: dashboard.dash_edit_perm && editMode, + isPublished: dashboard.published, + hasUnsavedChanges: false, + maxUndoHistoryExceeded: false, + lastModifiedTime: dashboard.last_modified_time, + }, + dashboardLayout, + messageToasts: [], + impressionId: shortid.generate(), + }; +} diff --git a/superset-frontend/src/dashboard/reducers/index.js b/superset-frontend/src/dashboard/reducers/index.js index 28804a7209069..481f1675ff4aa 100644 --- a/superset-frontend/src/dashboard/reducers/index.js +++ b/superset-frontend/src/dashboard/reducers/index.js @@ -32,8 +32,6 @@ import messageToasts from '../../messageToasts/reducers'; const impressionId = (state = '') => state; export default combineReducers({ - user: (state = null) => state, - common: (state = null) => state, charts, datasources, dashboardInfo, diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts index 8b8dc4fd7b593..d860cbede4d8d 100644 --- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts +++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts @@ -24,7 +24,6 @@ import { } from 'src/dashboard/actions/nativeFilters'; import { FilterSet, NativeFiltersState } from './types'; import { FilterConfiguration } from '../components/nativeFilters/types'; -import { HYDRATE_DASHBOARD } from '../actions/hydrate'; export function getInitialState({ filterSetsConfig, @@ -70,10 +69,6 @@ export default function nativeFilterReducer( ) { const { filterSets } = state; switch (action.type) { - case HYDRATE_DASHBOARD: - return { - filters: action.data.nativeFilters.filters, - }; case SAVE_FILTER_SETS: return { ...state, diff --git a/superset-frontend/src/dashboard/reducers/sliceEntities.js b/superset-frontend/src/dashboard/reducers/sliceEntities.js index 70b66db72475d..f34a0b61215e7 100644 --- a/superset-frontend/src/dashboard/reducers/sliceEntities.js +++ b/superset-frontend/src/dashboard/reducers/sliceEntities.js @@ -23,7 +23,6 @@ import { FETCH_ALL_SLICES_STARTED, SET_ALL_SLICES, } from '../actions/sliceEntities'; -import { HYDRATE_DASHBOARD } from '../actions/hydrate'; export const initSliceEntities = { slices: {}, @@ -37,11 +36,6 @@ export default function sliceEntitiesReducer( action, ) { const actionHandlers = { - [HYDRATE_DASHBOARD]() { - return { - ...action.data.sliceEntities, - }; - }, [FETCH_ALL_SLICES_STARTED]() { return { ...state, diff --git a/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js b/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js index 2edb51d00fae2..49e0186e2b49e 100644 --- a/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js +++ b/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js @@ -29,17 +29,13 @@ import { HANDLE_COMPONENT_DROP, } from '../actions/dashboardLayout'; -import { HYDRATE_DASHBOARD } from '../actions/hydrate'; - import dashboardLayout from './dashboardLayout'; export default undoable(dashboardLayout, { // +1 because length of history seems max out at limit - 1 // +1 again so we can detect if we've exceeded the limit limit: UNDO_LIMIT + 2, - ignoreInitialState: true, filter: includeAction([ - HYDRATE_DASHBOARD, UPDATE_COMPONENTS, DELETE_COMPONENT, CREATE_COMPONENT, diff --git a/superset-frontend/src/dashboard/util/getPermissions.ts b/superset-frontend/src/dashboard/util/getPermissions.ts deleted file mode 100644 index 0208fd68fd65f..0000000000000 --- a/superset-frontend/src/dashboard/util/getPermissions.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * 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 memoizeOne from 'memoize-one'; - -const findPermissions = (perm: string, view: string, roles: object) => { - const roleList = Object.entries(roles); - if (roleList.length === 0) return false; - let bool; - - roleList.forEach(([role, permissions]) => { - bool = Boolean( - permissions.find( - (permission: Array) => - permission[0] === perm && permission[1] === view, - ), - ); - }); - return bool; -}; - -const getPermissions = memoizeOne(findPermissions); - -export default getPermissions; diff --git a/superset-frontend/src/types/Dashboard.ts b/superset-frontend/src/types/Dashboard.ts deleted file mode 100644 index 9608cc1d08b18..0000000000000 --- a/superset-frontend/src/types/Dashboard.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * 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 Owner from './Owner'; -import Role from './Role'; - -type Dashboard = { - id: number; - slug: string; - url: string; - dashboard_title: string; - thumbnail_url: string; - published: boolean; - css: string; - json_metadata: string; - position_json: string; - changed_by_name: string; - changed_by: Owner; - changed_on: string; - charts: string[]; // just chart names, unfortunately... - owners: Owner[]; - roles: Role[]; -}; - -export default Dashboard; diff --git a/superset-frontend/src/types/Role.ts b/superset-frontend/src/types/Role.ts deleted file mode 100644 index 54f6876f493a6..0000000000000 --- a/superset-frontend/src/types/Role.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * 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. - */ -type Role = { - id: number; - name: string; -}; - -export default Role; diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py index ef11d8412e2b9..3853a7c224803 100644 --- a/superset/charts/schemas.py +++ b/superset/charts/schemas.py @@ -139,7 +139,6 @@ class ChartEntityResponseSchema(Schema): slice_name = fields.String(description=slice_name_description) cache_timeout = fields.Integer(description=cache_timeout_description) changed_on = fields.String(description=changed_on_description) - modified = fields.String() datasource = fields.String(description=datasource_name_description) description = fields.String(description=description_description) description_markeddown = fields.String( diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index 458e59c47dc99..9d6258e450b1b 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -307,7 +307,7 @@ def get_datasets(self, id_or_slug: str) -> Response: except DashboardNotFoundError: return self.response_404() - @expose("//charts", methods=["GET"]) + @expose("//charts", methods=["GET"]) @protect() @safe @statsd_metrics @@ -315,7 +315,7 @@ def get_datasets(self, id_or_slug: str) -> Response: action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_charts", log_to_statsd=False, ) - def get_charts(self, id_or_slug: str) -> Response: + def get_charts(self, pk: int) -> Response: """Gets the chart definitions for a given dashboard --- get: @@ -324,8 +324,8 @@ def get_charts(self, id_or_slug: str) -> Response: parameters: - in: path schema: - type: string - name: id_or_slug + type: integer + name: pk responses: 200: description: Dashboard chart definitions @@ -348,16 +348,8 @@ def get_charts(self, id_or_slug: str) -> Response: $ref: '#/components/responses/404' """ try: - charts = DashboardDAO.get_charts_for_dashboard(id_or_slug) + charts = DashboardDAO.get_charts_for_dashboard(pk) result = [self.chart_entity_response_schema.dump(chart) for chart in charts] - - if is_feature_enabled("REMOVE_SLICE_LEVEL_LABEL_COLORS"): - # dashboard metadata has dashboard-level label_colors, - # so remove slice-level label_colors from its form_data - for chart in result: - form_data = chart.get("form_data") - form_data.pop("label_colors", None) - return self.response(200, result=result) except DashboardNotFoundError: return self.response_404() diff --git a/superset/dashboards/dao.py b/superset/dashboards/dao.py index 800ee66c9070b..0951f47e3710f 100644 --- a/superset/dashboards/dao.py +++ b/superset/dashboards/dao.py @@ -82,12 +82,12 @@ def get_datasets_for_dashboard(id_or_slug: str) -> List[Any]: return data @staticmethod - def get_charts_for_dashboard(id_or_slug: str) -> List[Slice]: + def get_charts_for_dashboard(dashboard_id: int) -> List[Slice]: query = ( db.session.query(Dashboard) .outerjoin(Slice, Dashboard.slices) .outerjoin(Slice.table) - .filter(id_or_slug_filter(id_or_slug)) + .filter(Dashboard.id == dashboard_id) .options(contains_eager(Dashboard.slices)) ) # Apply dashboard base filters diff --git a/superset/views/core.py b/superset/views/core.py index af2cabe669b52..0093f50421f3c 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -800,7 +800,6 @@ def explore( # pylint: disable=too-many-locals,too-many-return-statements,too-m "slice": slc.data if slc else None, "standalone": standalone_mode, "user_id": user_id, - "user": bootstrap_user_data(g.user, include_perms=True), "forced_height": request.args.get("height"), "common": common_bootstrap_payload(), } @@ -1812,11 +1811,13 @@ def dashboard( # pylint: disable=too-many-locals if not dashboard: abort(404) + data = dashboard.full_data() + if config["ENABLE_ACCESS_REQUEST"]: - for datasource in dashboard.datasources: + for datasource in data["datasources"].values(): datasource = ConnectorRegistry.get_datasource( - datasource_type=datasource.type, - datasource_id=datasource.id, + datasource_type=datasource["type"], + datasource_id=datasource["id"], session=db.session(), ) if datasource and not security_manager.can_access_datasource( @@ -1835,6 +1836,10 @@ def dashboard( # pylint: disable=too-many-locals dash_edit_perm = check_ownership( dashboard, raise_if_false=False ) and security_manager.can_access("can_save_dash", "Superset") + dash_save_perm = security_manager.can_access("can_save_dash", "Superset") + superset_can_explore = security_manager.can_access("can_explore", "Superset") + superset_can_csv = security_manager.can_access("can_csv", "Superset") + slice_can_edit = security_manager.can_access("can_edit", "SliceModelView") standalone_mode = ReservedUrlParameters.is_standalone_mode() edit_mode = ( request.args.get(utils.ReservedUrlParameters.EDIT_MODE.value) == "true" @@ -1847,11 +1852,41 @@ def dashboard( # pylint: disable=too-many-locals edit_mode=edit_mode, ) + if is_feature_enabled("REMOVE_SLICE_LEVEL_LABEL_COLORS"): + # dashboard metadata has dashboard-level label_colors, + # so remove slice-level label_colors from its form_data + for slc in data["slices"]: + form_data = slc.get("form_data") + form_data.pop("label_colors", None) + + url_params = { + key: value + for key, value in request.args.items() + if key not in [param.value for param in utils.ReservedUrlParameters] + } + bootstrap_data = { - "user": bootstrap_user_data(g.user, include_perms=True), + "user_id": g.user.get_id(), "common": common_bootstrap_payload(), + "editMode": edit_mode, + "urlParams": url_params, + "dashboard_data": { + **data["dashboard"], + "standalone_mode": standalone_mode, + "dash_save_perm": dash_save_perm, + "dash_edit_perm": dash_edit_perm, + "superset_can_explore": superset_can_explore, + "superset_can_csv": superset_can_csv, + "slice_can_edit": slice_can_edit, + }, + "datasources": data["datasources"], } + if request.args.get("json") == "true": + return json_success( + json.dumps(bootstrap_data, default=utils.pessimistic_json_iso_dttm_ser) + ) + return self.render_template( "superset/dashboard.html", entry="dashboard", diff --git a/tests/dashboard_tests.py b/tests/dashboard_tests.py index 97d9e7b5440aa..f24340095b1a0 100644 --- a/tests/dashboard_tests.py +++ b/tests/dashboard_tests.py @@ -128,9 +128,24 @@ def test_new_dashboard(self): dash_count_before = db.session.query(func.count(Dashboard.id)).first()[0] url = "/dashboard/new/" resp = self.get_resp(url) + self.assertIn("[ untitled dashboard ]", resp) dash_count_after = db.session.query(func.count(Dashboard.id)).first()[0] self.assertEqual(dash_count_before + 1, dash_count_after) + @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") + def test_dashboard_modes(self): + self.login(username="admin") + dash = db.session.query(Dashboard).filter_by(slug="births").first() + url = dash.url + if dash.url.find("?") == -1: + url += "?" + else: + url += "&" + resp = self.get_resp(url + "edit=true&standalone=true") + self.assertIn("editMode": true", resp) + self.assertIn("standalone_mode": true", resp) + self.assertIn('', resp) + @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") def test_save_dash(self, username="admin"): self.login(username=username) @@ -175,6 +190,9 @@ def test_save_dash_with_filter(self, username="admin"): self.assertIn("world_health", new_url) self.assertNotIn("preselect_filters", new_url) + resp = self.get_resp(new_url) + self.assertIn("North America", resp) + @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices") def test_save_dash_with_invalid_filters(self, username="admin"): self.login(username=username) @@ -390,6 +408,8 @@ def test_public_user_dashboard_access(self): resp = self.get_resp("/api/v1/dashboard/") self.assertIn("/superset/dashboard/births/", resp) + self.assertIn("Births", self.get_resp("/superset/dashboard/births/")) + # Confirm that public doesn't have access to other datasets. resp = self.get_resp("/api/v1/chart/") self.assertNotIn("wb_health_population", resp) diff --git a/tests/dashboards/api_tests.py b/tests/dashboards/api_tests.py index 4a329e4776bb0..fadabe2c5fe0f 100644 --- a/tests/dashboards/api_tests.py +++ b/tests/dashboards/api_tests.py @@ -238,22 +238,6 @@ def test_get_dashboard_charts(self): data["result"][0]["slice_name"], dashboard.slices[0].slice_name ) - @pytest.mark.usefixtures("create_dashboards") - def test_get_dashboard_charts_by_slug(self): - """ - Dashboard API: Test getting charts belonging to a dashboard - """ - self.login(username="admin") - dashboard = self.dashboards[0] - uri = f"api/v1/dashboard/{dashboard.slug}/charts" - response = self.get_assert_metric(uri, "get_charts") - self.assertEqual(response.status_code, 200) - data = json.loads(response.data.decode("utf-8")) - self.assertEqual(len(data["result"]), 1) - self.assertEqual( - data["result"][0]["slice_name"], dashboard.slices[0].slice_name - ) - @pytest.mark.usefixtures("create_dashboards") def test_get_dashboard_charts_not_found(self): """