From 837544617aeb94db757444fb555cf3408f34d719 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Fri, 5 Feb 2021 12:20:53 -0800 Subject: [PATCH 01/59] add hook for future async api calls --- superset-frontend/src/dashboard/App.jsx | 30 +++++++++++-------- .../src/dashboard/components/Dashboard.jsx | 4 +-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/superset-frontend/src/dashboard/App.jsx b/superset-frontend/src/dashboard/App.jsx index da06a0130fa31..77770e9314364 100644 --- a/superset-frontend/src/dashboard/App.jsx +++ b/superset-frontend/src/dashboard/App.jsx @@ -17,30 +17,34 @@ * under the License. */ import { hot } from 'react-hot-loader/root'; -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Provider } from 'react-redux'; -import { ThemeProvider } from '@superset-ui/core'; -import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; +import { supersetTheme, ThemeProvider } from '@superset-ui/core'; import { DynamicPluginProvider } from 'src/components/DynamicPlugins'; import setupApp from '../setup/setupApp'; import setupPlugins from '../setup/setupPlugins'; import DashboardContainer from './containers/Dashboard'; -import { theme } from '../preamble'; setupApp(); setupPlugins(); -const App = ({ store }) => ( - - - +const App = ({ store }) => { + const [bootstrapData, setBootstrapData] = useState(''); + + useEffect(() => { + const appContainer = document.getElementById('app'); + setBootstrapData(appContainer?.getAttribute('data-bootstrap') || ''); + }, []); + + return ( + + - + - - -); + + ); +}; export default hot(App); diff --git a/superset-frontend/src/dashboard/components/Dashboard.jsx b/superset-frontend/src/dashboard/components/Dashboard.jsx index 3f3a79284aceb..6ca8b5021274c 100644 --- a/superset-frontend/src/dashboard/components/Dashboard.jsx +++ b/superset-frontend/src/dashboard/components/Dashboard.jsx @@ -93,9 +93,7 @@ class Dashboard extends React.PureComponent { } componentDidMount() { - const appContainer = document.getElementById('app'); - const bootstrapData = appContainer?.getAttribute('data-bootstrap') || ''; - const { dashboardState, layout } = this.props; + const { dashboardState, layout, bootstrapData } = this.props; const eventData = { is_edit_mode: dashboardState.editMode, mount_duration: Logger.getTimestamp(), From 49d8edc4b61a668f407d433553daa7e2ea06ea0e Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Sun, 7 Feb 2021 21:06:57 -0800 Subject: [PATCH 02/59] test to see conflict --- superset-frontend/src/dashboard/App.jsx | 29 +++++++------------ .../src/dashboard/components/Dashboard.jsx | 4 ++- superset-frontend/src/dashboard/index.jsx | 19 +++++++++++- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/superset-frontend/src/dashboard/App.jsx b/superset-frontend/src/dashboard/App.jsx index 77770e9314364..d76ef9374392f 100644 --- a/superset-frontend/src/dashboard/App.jsx +++ b/superset-frontend/src/dashboard/App.jsx @@ -17,7 +17,7 @@ * under the License. */ import { hot } from 'react-hot-loader/root'; -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { Provider } from 'react-redux'; import { supersetTheme, ThemeProvider } from '@superset-ui/core'; import { DynamicPluginProvider } from 'src/components/DynamicPlugins'; @@ -28,23 +28,14 @@ import DashboardContainer from './containers/Dashboard'; setupApp(); setupPlugins(); -const App = ({ store }) => { - const [bootstrapData, setBootstrapData] = useState(''); - - useEffect(() => { - const appContainer = document.getElementById('app'); - setBootstrapData(appContainer?.getAttribute('data-bootstrap') || ''); - }, []); - - return ( - - - - - - - - ); -}; +const App = ({ store }) => ( + + + + + + + +); export default hot(App); diff --git a/superset-frontend/src/dashboard/components/Dashboard.jsx b/superset-frontend/src/dashboard/components/Dashboard.jsx index 6ca8b5021274c..3f3a79284aceb 100644 --- a/superset-frontend/src/dashboard/components/Dashboard.jsx +++ b/superset-frontend/src/dashboard/components/Dashboard.jsx @@ -93,7 +93,9 @@ class Dashboard extends React.PureComponent { } componentDidMount() { - const { dashboardState, layout, bootstrapData } = this.props; + const appContainer = document.getElementById('app'); + const bootstrapData = appContainer?.getAttribute('data-bootstrap') || ''; + const { dashboardState, layout } = this.props; const eventData = { is_edit_mode: dashboardState.editMode, mount_duration: Logger.getTimestamp(), diff --git a/superset-frontend/src/dashboard/index.jsx b/superset-frontend/src/dashboard/index.jsx index 9fe82346c3247..afc3ac129ce49 100644 --- a/superset-frontend/src/dashboard/index.jsx +++ b/superset-frontend/src/dashboard/index.jsx @@ -45,13 +45,30 @@ const asyncEventMiddleware = initAsyncEvents({ actions.chartUpdateFailed(response, componentId), }); +/*const asyncFunctionMiddleware = store => next => action => { + if (typeof action === 'function') { + return action(store.dispatch, store.getState); + } + return next(action); +};*/ + const store = createStore( rootReducer, initState, compose( - applyMiddleware(thunk, logger, asyncEventMiddleware), + applyMiddleware( + thunk, + logger, + asyncEventMiddleware, + // asyncFunctionMiddleware, + ), initEnhancer(false), ), ); +//store.dispatch((dispatch, getState) => { + // make API call + // dispatch({ type: 'SET_BOOTSTRAP_DATA', bootstrapData }); +//}); + ReactDOM.render(, document.getElementById('app')); From 52731c7d757cbc3a41afd3eb965cbfb2a27252b9 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Mon, 8 Feb 2021 16:40:08 -0800 Subject: [PATCH 03/59] add async middleware and update reducers --- superset-frontend/src/dashboard/App.jsx | 17 +++++++----- .../src/dashboard/actions/bootstrapData.js | 26 +++++++++++++++++++ .../dashboard/components/DashboardBuilder.jsx | 8 ++++-- superset-frontend/src/dashboard/index.jsx | 16 ++++++------ .../dashboard/reducers/dashboardFilters.js | 4 +++ .../src/dashboard/reducers/dashboardInfo.js | 8 ++++++ .../src/dashboard/reducers/dashboardLayout.js | 8 ++++++ .../src/dashboard/reducers/dashboardState.js | 4 +++ .../src/dashboard/reducers/datasources.js | 4 +++ .../src/dashboard/reducers/getInitialState.js | 1 + .../src/dashboard/reducers/nativeFilters.ts | 6 +++++ .../src/dashboard/reducers/sliceEntities.js | 6 +++++ 12 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 superset-frontend/src/dashboard/actions/bootstrapData.js diff --git a/superset-frontend/src/dashboard/App.jsx b/superset-frontend/src/dashboard/App.jsx index d76ef9374392f..da06a0130fa31 100644 --- a/superset-frontend/src/dashboard/App.jsx +++ b/superset-frontend/src/dashboard/App.jsx @@ -19,22 +19,27 @@ import { hot } from 'react-hot-loader/root'; import React from 'react'; import { Provider } from 'react-redux'; -import { supersetTheme, ThemeProvider } from '@superset-ui/core'; +import { ThemeProvider } from '@superset-ui/core'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; import { DynamicPluginProvider } from 'src/components/DynamicPlugins'; import setupApp from '../setup/setupApp'; import setupPlugins from '../setup/setupPlugins'; import DashboardContainer from './containers/Dashboard'; +import { theme } from '../preamble'; setupApp(); setupPlugins(); const App = ({ store }) => ( - - - - - + + + + + + + ); diff --git a/superset-frontend/src/dashboard/actions/bootstrapData.js b/superset-frontend/src/dashboard/actions/bootstrapData.js new file mode 100644 index 0000000000000..e779cdaf50914 --- /dev/null +++ b/superset-frontend/src/dashboard/actions/bootstrapData.js @@ -0,0 +1,26 @@ +/** + * 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. + */ +export const SET_BOOTSTRAP_DATA = 'SET_BOOTSTRAP_DATA'; + +export function setBoostrapData(data) { + return { + type: SET_BOOTSTRAP_DATA, + data, + }; +} diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx index 308ecfa8eee11..8470014466654 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx @@ -137,7 +137,10 @@ class DashboardBuilder extends React.Component { static getRootLevelTabsComponent(dashboardLayout) { const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; - const rootChildId = dashboardRoot.children[0]; + const rootChildId = dashboardRoot?.children[0]; + console.log('rootChildId', rootChildId); + console.log('this.props', this.props); + if (!rootChildId) return {}; return rootChildId === DASHBOARD_GRID_ID ? dashboardLayout[DASHBOARD_ROOT_ID] : dashboardLayout[rootChildId]; @@ -222,7 +225,8 @@ class DashboardBuilder extends React.Component { } = this.props; const { tabIndex } = this.state; const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; - const rootChildId = dashboardRoot.children[0]; + console.log('dashboardRoot', dashboardRoot); + const rootChildId = dashboardRoot?.children[0]; const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId]; diff --git a/superset-frontend/src/dashboard/index.jsx b/superset-frontend/src/dashboard/index.jsx index afc3ac129ce49..8c330aeef1c5b 100644 --- a/superset-frontend/src/dashboard/index.jsx +++ b/superset-frontend/src/dashboard/index.jsx @@ -45,30 +45,30 @@ const asyncEventMiddleware = initAsyncEvents({ actions.chartUpdateFailed(response, componentId), }); -/*const asyncFunctionMiddleware = store => next => action => { +const asyncFunctionMiddleware = store => next => action => { if (typeof action === 'function') { return action(store.dispatch, store.getState); } return next(action); -};*/ +}; const store = createStore( rootReducer, - initState, + // initState, compose( applyMiddleware( thunk, logger, asyncEventMiddleware, - // asyncFunctionMiddleware, + asyncFunctionMiddleware, ), initEnhancer(false), ), ); - -//store.dispatch((dispatch, getState) => { +store.dispatch((dispatch, getState) => { // make API call - // dispatch({ type: 'SET_BOOTSTRAP_DATA', bootstrapData }); -//}); + console.log('----- i hit in store.dispatch --------') + dispatch({ type: 'SET_BOOTSTRAP_DATA', initState }); +}); ReactDOM.render(, document.getElementById('app')); diff --git a/superset-frontend/src/dashboard/reducers/dashboardFilters.js b/superset-frontend/src/dashboard/reducers/dashboardFilters.js index f508c1bfe3868..a4dc2dd2bd395 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardFilters.js +++ b/superset-frontend/src/dashboard/reducers/dashboardFilters.js @@ -25,6 +25,7 @@ import { UPDATE_LAYOUT_COMPONENTS, UPDATE_DASHBOARD_FILTERS_SCOPE, } from '../actions/dashboardFilters'; +import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox'; import { DASHBOARD_ROOT_ID } from '../util/constants'; import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata'; @@ -53,6 +54,9 @@ const CHANGE_FILTER_VALUE_ACTIONS = [ADD_FILTER, REMOVE_FILTER, CHANGE_FILTER]; export default function dashboardFiltersReducer(dashboardFilters = {}, action) { const actionHandlers = { + [SET_BOOTSTRAP_DATA]() { + return { dashboardFilters: action.initState.dashboardFilters }; + }, [ADD_FILTER]() { const { chartId, component, form_data } = action; const { columns, labels } = getFilterConfigsFromFormdata(form_data); diff --git a/superset-frontend/src/dashboard/reducers/dashboardInfo.js b/superset-frontend/src/dashboard/reducers/dashboardInfo.js index 01346d7a4f29f..29c1f68cdca54 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardInfo.js +++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.js @@ -18,8 +18,10 @@ */ import { DASHBOARD_INFO_UPDATED } from '../actions/dashboardInfo'; +import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; export default function dashboardStateReducer(state = {}, action) { + console.log('action', action.data); switch (action.type) { case DASHBOARD_INFO_UPDATED: return { @@ -28,6 +30,12 @@ export default function dashboardStateReducer(state = {}, action) { // server-side compare last_modified_time in second level lastModifiedTime: Math.round(new Date().getTime() / 1000), }; + case SET_BOOTSTRAP_DATA: + return { + ...state, + ...action.initState.dashboardInfo, + // set async api call data + }; default: return state; } diff --git a/superset-frontend/src/dashboard/reducers/dashboardLayout.js b/superset-frontend/src/dashboard/reducers/dashboardLayout.js index ffc56132a83c5..a4b79e9277941 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardLayout.js +++ b/superset-frontend/src/dashboard/reducers/dashboardLayout.js @@ -43,7 +43,15 @@ import { DASHBOARD_TITLE_CHANGED, } from '../actions/dashboardLayout'; +import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; + const actionHandlers = { + [SET_BOOTSTRAP_DATA](state, action) { + return { + ...state, + ...action.initState.dashbaordLayout, + }; + }, [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 b948e2c4a3497..e12f0ed0d6c32 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.js @@ -36,9 +36,13 @@ import { SET_FOCUSED_FILTER_FIELD, UNSET_FOCUSED_FILTER_FIELD, } from '../actions/dashboardState'; +import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; export default function dashboardStateReducer(state = {}, action) { const actionHandlers = { + [SET_BOOTSTRAP_DATA]() { + return { ...state, ...action.initState.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 0cf7e1bac4558..01d4bc7a65548 100644 --- a/superset-frontend/src/dashboard/reducers/datasources.js +++ b/superset-frontend/src/dashboard/reducers/datasources.js @@ -17,9 +17,13 @@ * under the License. */ import { SET_DATASOURCE } from '../actions/datasources'; +import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; export default function datasourceReducer(datasources = {}, action) { const actionHandlers = { + [SET_BOOTSTRAP_DATA]() { + return action.initState.datasource; + }, [SET_DATASOURCE]() { return action.datasource; }, diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index 11579976064f1..2730c4c91136b 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -263,6 +263,7 @@ export default function getInitialState(bootstrapData) { dashboard.metadata.filter_configuration || [], ); + // console.log('nativeFilter', nativeFilters) return { datasources, sliceEntities: { ...initSliceEntities, slices, isLoading: false }, diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts index f0cdd8ace6c18..1d88795b23abe 100644 --- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts +++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts @@ -23,6 +23,7 @@ import { } from 'src/dashboard/actions/nativeFilters'; import { NativeFiltersState, NativeFilterState } from './types'; import { FilterConfiguration } from '../components/nativeFilters/types'; +import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; export function getInitialFilterState(id: string): NativeFilterState { return { @@ -52,6 +53,11 @@ export default function nativeFilterReducer( ) { const { filters, filtersState } = state; switch (action.type) { + case SET_BOOTSTRAP_DATA: + return { + filters: action.initState.nativeFilters.filters, + filtersState: action.initState.nativeFilters.filtersState, + }; case SET_EXTRA_FORM_DATA: return { filters, diff --git a/superset-frontend/src/dashboard/reducers/sliceEntities.js b/superset-frontend/src/dashboard/reducers/sliceEntities.js index f34a0b61215e7..7e82583d81a9e 100644 --- a/superset-frontend/src/dashboard/reducers/sliceEntities.js +++ b/superset-frontend/src/dashboard/reducers/sliceEntities.js @@ -23,6 +23,7 @@ import { FETCH_ALL_SLICES_STARTED, SET_ALL_SLICES, } from '../actions/sliceEntities'; +import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; export const initSliceEntities = { slices: {}, @@ -36,6 +37,11 @@ export default function sliceEntitiesReducer( action, ) { const actionHandlers = { + [SET_BOOTSTRAP_DATA]() { + return { + ...action.initState.sliceEntities, + }; + }, [FETCH_ALL_SLICES_STARTED]() { return { ...state, From 8050697a9d8221c18ae833cc39f0ce1991fb0e42 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Tue, 16 Feb 2021 16:11:25 -0800 Subject: [PATCH 04/59] working async dashboard load --- superset-frontend/src/chart/chartReducer.js | 5 +- superset-frontend/src/dashboard/App.jsx | 7 ++- .../src/dashboard/actions/bootstrapData.js | 2 +- .../src/dashboard/actions/nativeFilters.ts | 16 +++++- .../dashboard/components/DashboardBuilder.jsx | 1 + .../dashboard/components/DashboardGrid.jsx | 1 - .../dashboard/components/DashboardRoute.tsx | 57 +++++++++++++++++++ .../components/FiltersBadge/selectors.ts | 2 +- superset-frontend/src/dashboard/index.jsx | 23 +------- .../dashboard/reducers/dashboardFilters.js | 8 +-- .../src/dashboard/reducers/dashboardInfo.js | 3 +- .../src/dashboard/reducers/dashboardLayout.js | 4 +- .../src/dashboard/reducers/dashboardState.js | 2 +- .../src/dashboard/reducers/datasources.js | 19 ++++--- .../src/dashboard/reducers/getInitialState.js | 2 - .../src/dashboard/reducers/nativeFilters.ts | 4 +- .../src/dashboard/reducers/sliceEntities.js | 2 +- .../reducers/undoableDashboardLayout.js | 3 + 18 files changed, 110 insertions(+), 51 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/DashboardRoute.tsx diff --git a/superset-frontend/src/chart/chartReducer.js b/superset-frontend/src/chart/chartReducer.js index e502bb58dd9ea..217da446f803d 100644 --- a/superset-frontend/src/chart/chartReducer.js +++ b/superset-frontend/src/chart/chartReducer.js @@ -21,6 +21,7 @@ import { t } from '@superset-ui/core'; import { getFormDataFromControls } from 'src/explore/controlUtils'; import { now } from '../modules/dates'; import * as actions from './chartAction'; +import { SET_BOOTSTRAP_DATA } from '../dashboard/actions/bootstrapData'; export const chart = { id: 0, @@ -191,7 +192,9 @@ export default function chartReducer(charts = {}, action) { delete charts[key]; return charts; } - + if (action.type === SET_BOOTSTRAP_DATA) { + return { ...action.data.charts }; + } if (action.type in actionHandlers) { return { ...charts, diff --git a/superset-frontend/src/dashboard/App.jsx b/superset-frontend/src/dashboard/App.jsx index da06a0130fa31..efe62974f9c15 100644 --- a/superset-frontend/src/dashboard/App.jsx +++ b/superset-frontend/src/dashboard/App.jsx @@ -17,7 +17,7 @@ * under the License. */ import { hot } from 'react-hot-loader/root'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Provider } from 'react-redux'; import { ThemeProvider } from '@superset-ui/core'; import { DndProvider } from 'react-dnd'; @@ -25,6 +25,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import { DynamicPluginProvider } from 'src/components/DynamicPlugins'; import setupApp from '../setup/setupApp'; import setupPlugins from '../setup/setupPlugins'; +import DashboardRoute from './components/DashboardRoute'; import DashboardContainer from './containers/Dashboard'; import { theme } from '../preamble'; @@ -36,7 +37,9 @@ const App = ({ store }) => ( - + + + diff --git a/superset-frontend/src/dashboard/actions/bootstrapData.js b/superset-frontend/src/dashboard/actions/bootstrapData.js index e779cdaf50914..63b98b9cd849e 100644 --- a/superset-frontend/src/dashboard/actions/bootstrapData.js +++ b/superset-frontend/src/dashboard/actions/bootstrapData.js @@ -18,7 +18,7 @@ */ export const SET_BOOTSTRAP_DATA = 'SET_BOOTSTRAP_DATA'; -export function setBoostrapData(data) { +export default function setBootstrapData(data) { return { type: SET_BOOTSTRAP_DATA, data, diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts index 09a2e2c0a50f8..337599143770e 100644 --- a/superset-frontend/src/dashboard/actions/nativeFilters.ts +++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts @@ -23,6 +23,7 @@ import { Filter, FilterConfiguration, } from 'src/dashboard/components/nativeFilters/types'; +import { SET_BOOTSTRAP_DATA } from './bootstrapData'; import { dashboardInfoChanged } from './dashboardInfo'; import { CurrentFilterState } from '../reducers/types'; import { SelectedValues } from '../components/nativeFilters/FilterConfigModal/types'; @@ -95,6 +96,18 @@ export const setFilterConfiguration = ( } }; +type Data = { + nativeFilters: { + filters: Filter; + filtersState: object; + }; +}; + +export interface SetBooststapData { + type: typeof SET_BOOTSTRAP_DATA; + data: Data; +} + export const SET_EXTRA_FORM_DATA = 'SET_EXTRA_FORM_DATA'; export interface SetExtraFormData { type: typeof SET_EXTRA_FORM_DATA; @@ -139,4 +152,5 @@ export type AnyFilterAction = | SetFilterConfigComplete | SetFilterConfigFail | SetExtraFormData - | SetFilterState; + | SetFilterState + | SetBooststapData; diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx index 8470014466654..c09f0f5728e44 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx @@ -225,6 +225,7 @@ class DashboardBuilder extends React.Component { } = this.props; const { tabIndex } = this.state; const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; + console.log('dashboardLayout', dashboardLayout) console.log('dashboardRoot', dashboardRoot); const rootChildId = dashboardRoot?.children[0]; const topLevelTabs = diff --git a/superset-frontend/src/dashboard/components/DashboardGrid.jsx b/superset-frontend/src/dashboard/components/DashboardGrid.jsx index 6889c91ab3de2..9fb0fb5fd55e5 100644 --- a/superset-frontend/src/dashboard/components/DashboardGrid.jsx +++ b/superset-frontend/src/dashboard/components/DashboardGrid.jsx @@ -123,7 +123,6 @@ 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/DashboardRoute.tsx b/superset-frontend/src/dashboard/components/DashboardRoute.tsx new file mode 100644 index 0000000000000..c936cb8f1095d --- /dev/null +++ b/superset-frontend/src/dashboard/components/DashboardRoute.tsx @@ -0,0 +1,57 @@ +/** + * 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 { connect } from 'react-redux'; +import { AnyAction, bindActionCreators, Dispatch } from 'redux'; +import setBootstrapData from 'src/dashboard/actions/bootstrapData'; +import Loading from 'src/components/Loading'; +import getInitialState from '../reducers/getInitialState'; + +interface DashboardRouteProps { + actions: { + setBootstrapData: (arg0: object) => void; + }; +} +const DashboardRoute: FC = ({ children, actions }) => { + const appContainer = document.getElementById('app'); + const bootstrapData = appContainer?.getAttribute('data-bootstrap'); + const bootstrapDataJson = JSON.parse(bootstrapData || ''); + const [loaded, setLoaded] = useState(false); + const initState = getInitialState(bootstrapDataJson); + useEffect(() => { + actions.setBootstrapData(initState); + // setLoaded + setLoaded(true); + }, []); + if (!loaded) return ; + return <>{children} ; +}; + +function mapDispatchToProps(dispatch: Dispatch) { + return { + actions: bindActionCreators( + { + setBootstrapData, + }, + dispatch, + ), + }; +} + +export default connect(null, mapDispatchToProps)(DashboardRoute); diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts index 97bf3b699bdd7..4b3da0201ffcc 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts +++ b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts @@ -151,7 +151,7 @@ export const selectIndicatorsForChart = ( // so grab the columns from the applied/rejected filters const appliedColumns = getAppliedColumns(chart); const rejectedColumns = getRejectedColumns(chart); - + console.log('filter', filters) const indicators = Object.values(filters) .filter(filter => filter.chartId !== chartId) .reduce( diff --git a/superset-frontend/src/dashboard/index.jsx b/superset-frontend/src/dashboard/index.jsx index 8c330aeef1c5b..35f9c5d42b414 100644 --- a/superset-frontend/src/dashboard/index.jsx +++ b/superset-frontend/src/dashboard/index.jsx @@ -22,18 +22,15 @@ 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 initAsyncEvents from '../middleware/asyncEvent'; import logger from '../middleware/loggerMiddleware'; import * as actions from '../chart/chartAction'; - import App from './App'; const appContainer = document.getElementById('app'); const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap')); initFeatureFlags(bootstrapData.common.feature_flags); -const initState = getInitialState(bootstrapData); const asyncEventMiddleware = initAsyncEvents({ config: bootstrapData.common.conf, @@ -45,30 +42,12 @@ const asyncEventMiddleware = initAsyncEvents({ actions.chartUpdateFailed(response, componentId), }); -const asyncFunctionMiddleware = store => next => action => { - if (typeof action === 'function') { - return action(store.dispatch, store.getState); - } - return next(action); -}; - const store = createStore( rootReducer, - // initState, compose( - applyMiddleware( - thunk, - logger, - asyncEventMiddleware, - asyncFunctionMiddleware, - ), + applyMiddleware(thunk, logger, asyncEventMiddleware), initEnhancer(false), ), ); -store.dispatch((dispatch, getState) => { - // make API call - console.log('----- i hit in store.dispatch --------') - dispatch({ type: 'SET_BOOTSTRAP_DATA', initState }); -}); ReactDOM.render(, document.getElementById('app')); diff --git a/superset-frontend/src/dashboard/reducers/dashboardFilters.js b/superset-frontend/src/dashboard/reducers/dashboardFilters.js index a4dc2dd2bd395..70b3eefbf02a6 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardFilters.js +++ b/superset-frontend/src/dashboard/reducers/dashboardFilters.js @@ -54,9 +54,6 @@ const CHANGE_FILTER_VALUE_ACTIONS = [ADD_FILTER, REMOVE_FILTER, CHANGE_FILTER]; export default function dashboardFiltersReducer(dashboardFilters = {}, action) { const actionHandlers = { - [SET_BOOTSTRAP_DATA]() { - return { dashboardFilters: action.initState.dashboardFilters }; - }, [ADD_FILTER]() { const { chartId, component, form_data } = action; const { columns, labels } = getFilterConfigsFromFormdata(form_data); @@ -165,6 +162,10 @@ export default function dashboardFiltersReducer(dashboardFilters = {}, action) { return updatedFilters; } + if (action.type === SET_BOOTSTRAP_DATA) { + return action.data.dashboardFilters; + } + if (action.type in actionHandlers) { const updatedFilters = { ...dashboardFilters, @@ -172,7 +173,6 @@ 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 29c1f68cdca54..cf569f167b5c7 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardInfo.js +++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.js @@ -21,7 +21,6 @@ import { DASHBOARD_INFO_UPDATED } from '../actions/dashboardInfo'; import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; export default function dashboardStateReducer(state = {}, action) { - console.log('action', action.data); switch (action.type) { case DASHBOARD_INFO_UPDATED: return { @@ -33,7 +32,7 @@ export default function dashboardStateReducer(state = {}, action) { case SET_BOOTSTRAP_DATA: return { ...state, - ...action.initState.dashboardInfo, + ...action.data.dashboardInfo, // set async api call data }; default: diff --git a/superset-frontend/src/dashboard/reducers/dashboardLayout.js b/superset-frontend/src/dashboard/reducers/dashboardLayout.js index a4b79e9277941..22e68a7c4a92e 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardLayout.js +++ b/superset-frontend/src/dashboard/reducers/dashboardLayout.js @@ -48,10 +48,10 @@ import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; const actionHandlers = { [SET_BOOTSTRAP_DATA](state, action) { return { - ...state, - ...action.initState.dashbaordLayout, + ...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 e12f0ed0d6c32..31c1884bb7fb2 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.js @@ -41,7 +41,7 @@ import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; export default function dashboardStateReducer(state = {}, action) { const actionHandlers = { [SET_BOOTSTRAP_DATA]() { - return { ...state, ...action.initState.dashboardState }; + 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 01d4bc7a65548..0e6682b5d3282 100644 --- a/superset-frontend/src/dashboard/reducers/datasources.js +++ b/superset-frontend/src/dashboard/reducers/datasources.js @@ -22,7 +22,7 @@ import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; export default function datasourceReducer(datasources = {}, action) { const actionHandlers = { [SET_BOOTSTRAP_DATA]() { - return action.initState.datasource; + return action.data.datasources; }, [SET_DATASOURCE]() { return action.datasource; @@ -30,13 +30,16 @@ export default function datasourceReducer(datasources = {}, action) { }; if (action.type in actionHandlers) { - return { - ...datasources, - [action.key]: actionHandlers[action.type]( - datasources[action.key], - action, - ), - }; + if (action.key) { + return { + ...datasources, + [action.key]: actionHandlers[action.type]( + datasources[action.key], + action, + ), + }; + } + return actionHandlers[action.type](); } return datasources; } diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index 2730c4c91136b..ff16a0cbd11fa 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -50,7 +50,6 @@ import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox'; export default function getInitialState(bootstrapData) { const { user_id, datasources, common, editMode, urlParams } = bootstrapData; - const dashboard = { ...bootstrapData.dashboard_data }; let preselectFilters = {}; try { @@ -263,7 +262,6 @@ export default function getInitialState(bootstrapData) { dashboard.metadata.filter_configuration || [], ); - // console.log('nativeFilter', nativeFilters) return { datasources, sliceEntities: { ...initSliceEntities, slices, isLoading: false }, diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts index 1d88795b23abe..ff8a2aad3d280 100644 --- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts +++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts @@ -55,8 +55,8 @@ export default function nativeFilterReducer( switch (action.type) { case SET_BOOTSTRAP_DATA: return { - filters: action.initState.nativeFilters.filters, - filtersState: action.initState.nativeFilters.filtersState, + filters: action.data.nativeFilters.filters, + filtersState: action.data.nativeFilters.filtersState, }; case SET_EXTRA_FORM_DATA: return { diff --git a/superset-frontend/src/dashboard/reducers/sliceEntities.js b/superset-frontend/src/dashboard/reducers/sliceEntities.js index 7e82583d81a9e..34a302f541fc9 100644 --- a/superset-frontend/src/dashboard/reducers/sliceEntities.js +++ b/superset-frontend/src/dashboard/reducers/sliceEntities.js @@ -39,7 +39,7 @@ export default function sliceEntitiesReducer( const actionHandlers = { [SET_BOOTSTRAP_DATA]() { return { - ...action.initState.sliceEntities, + ...action.data.sliceEntities, }; }, [FETCH_ALL_SLICES_STARTED]() { diff --git a/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js b/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js index 49e0186e2b49e..bc1aa8dc361f7 100644 --- a/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js +++ b/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js @@ -29,6 +29,8 @@ import { HANDLE_COMPONENT_DROP, } from '../actions/dashboardLayout'; +import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; + import dashboardLayout from './dashboardLayout'; export default undoable(dashboardLayout, { @@ -36,6 +38,7 @@ export default undoable(dashboardLayout, { // +1 again so we can detect if we've exceeded the limit limit: UNDO_LIMIT + 2, filter: includeAction([ + SET_BOOTSTRAP_DATA, UPDATE_COMPONENTS, DELETE_COMPONENT, CREATE_COMPONENT, From d247e96500fd9e670a8a5b40082de3a46764aae4 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Wed, 17 Feb 2021 14:40:44 -0800 Subject: [PATCH 05/59] implement getcharts api --- superset-frontend/src/dashboard/App.jsx | 31 ++++++++++--------- .../dashboard/components/DashboardBuilder.jsx | 4 --- .../dashboard/components/DashboardRoute.tsx | 22 ++++++++++--- .../components/FiltersBadge/selectors.ts | 1 - .../src/dashboard/reducers/getInitialState.js | 8 +++-- 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/superset-frontend/src/dashboard/App.jsx b/superset-frontend/src/dashboard/App.jsx index efe62974f9c15..9050f275f0462 100644 --- a/superset-frontend/src/dashboard/App.jsx +++ b/superset-frontend/src/dashboard/App.jsx @@ -17,7 +17,7 @@ * under the License. */ import { hot } from 'react-hot-loader/root'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { Provider } from 'react-redux'; import { ThemeProvider } from '@superset-ui/core'; import { DndProvider } from 'react-dnd'; @@ -32,18 +32,21 @@ import { theme } from '../preamble'; setupApp(); setupPlugins(); -const App = ({ store }) => ( - - - - - - - - - - - -); +const App = ({ store }) => { + const dashboardId = window.location.pathname.split('/')[3]; + return ( + + + + + + + + + + + + ); +}; export default hot(App); diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx index c09f0f5728e44..0b527dfcd5712 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx @@ -138,8 +138,6 @@ class DashboardBuilder extends React.Component { static getRootLevelTabsComponent(dashboardLayout) { const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; const rootChildId = dashboardRoot?.children[0]; - console.log('rootChildId', rootChildId); - console.log('this.props', this.props); if (!rootChildId) return {}; return rootChildId === DASHBOARD_GRID_ID ? dashboardLayout[DASHBOARD_ROOT_ID] @@ -225,8 +223,6 @@ class DashboardBuilder extends React.Component { } = this.props; const { tabIndex } = this.state; const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; - console.log('dashboardLayout', dashboardLayout) - console.log('dashboardRoot', dashboardRoot); const rootChildId = dashboardRoot?.children[0]; const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId]; diff --git a/superset-frontend/src/dashboard/components/DashboardRoute.tsx b/superset-frontend/src/dashboard/components/DashboardRoute.tsx index c936cb8f1095d..d927d9806d83f 100644 --- a/superset-frontend/src/dashboard/components/DashboardRoute.tsx +++ b/superset-frontend/src/dashboard/components/DashboardRoute.tsx @@ -19,6 +19,7 @@ import React, { useEffect, useState, FC } from 'react'; import { connect } from 'react-redux'; import { AnyAction, bindActionCreators, Dispatch } from 'redux'; +import { SupersetClient, getClientErrorObject } from '@superset-ui/core'; import setBootstrapData from 'src/dashboard/actions/bootstrapData'; import Loading from 'src/components/Loading'; import getInitialState from '../reducers/getInitialState'; @@ -27,17 +28,28 @@ interface DashboardRouteProps { actions: { setBootstrapData: (arg0: object) => void; }; + dashboardId: string; } -const DashboardRoute: FC = ({ children, actions }) => { +const DashboardRoute: FC = ({ + children, + actions, + dashboardId, +}) => { const appContainer = document.getElementById('app'); const bootstrapData = appContainer?.getAttribute('data-bootstrap'); const bootstrapDataJson = JSON.parse(bootstrapData || ''); const [loaded, setLoaded] = useState(false); - const initState = getInitialState(bootstrapDataJson); + useEffect(() => { - actions.setBootstrapData(initState); - // setLoaded - setLoaded(true); + SupersetClient.get({ endpoint: `/api/v1/dashboard/${dashboardId}/charts` }) + .then(r => { + const initState = getInitialState(bootstrapDataJson, r.json.result); + actions.setBootstrapData(initState); + setLoaded(true); + }) + .catch(err => { + console.log('err', err); + }); }, []); if (!loaded) return ; return <>{children} ; diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts index 4b3da0201ffcc..c3323ce7ee714 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts +++ b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts @@ -151,7 +151,6 @@ export const selectIndicatorsForChart = ( // so grab the columns from the applied/rejected filters const appliedColumns = getAppliedColumns(chart); const rejectedColumns = getRejectedColumns(chart); - console.log('filter', filters) const indicators = Object.values(filters) .filter(filter => filter.chartId !== chartId) .reduce( diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index ff16a0cbd11fa..dea5778f4771a 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -48,10 +48,14 @@ import getLocationHash from '../util/getLocationHash'; import newComponentFactory from '../util/newComponentFactory'; import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox'; -export default function getInitialState(bootstrapData) { +export default function getInitialState(bootstrapData, chartdata) { 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( @@ -105,7 +109,7 @@ export default function getInitialState(bootstrapData) { const dashboardFilters = {}; const slices = {}; const sliceIds = new Set(); - dashboard.slices.forEach(slice => { + chartdata.forEach(slice => { const key = slice.slice_id; const form_data = { ...slice.form_data, From 706845be236a5acb52280d57d4bc882142e3d8bd Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Thu, 18 Feb 2021 18:16:34 -0800 Subject: [PATCH 06/59] add user permissions to explore and dashboard bootstrap data --- superset/views/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/superset/views/core.py b/superset/views/core.py index 85222eb057c02..b1f34ab3d9f53 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -803,6 +803,7 @@ def explore( # pylint: disable=too-many-locals,too-many-return-statements "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(), } @@ -1861,6 +1862,7 @@ def dashboard( # pylint: disable=too-many-locals bootstrap_data = { "user_id": g.user.get_id(), + "user": bootstrap_user_data(g.user, include_perms=True), "common": common_bootstrap_payload(), "editMode": edit_mode, "urlParams": url_params, From 0010bbbbb9ba5790c0c73b65b08a2239faff3aa4 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Fri, 19 Feb 2021 13:30:06 -0800 Subject: [PATCH 07/59] integrate api calls with getinitial state --- .../dashboard/components/DashboardRoute.tsx | 36 ++++++++++---- .../src/dashboard/reducers/getInitialState.js | 48 ++++++++++--------- 2 files changed, 53 insertions(+), 31 deletions(-) diff --git a/superset-frontend/src/dashboard/components/DashboardRoute.tsx b/superset-frontend/src/dashboard/components/DashboardRoute.tsx index d927d9806d83f..18809a34f17b1 100644 --- a/superset-frontend/src/dashboard/components/DashboardRoute.tsx +++ b/superset-frontend/src/dashboard/components/DashboardRoute.tsx @@ -19,7 +19,7 @@ import React, { useEffect, useState, FC } from 'react'; import { connect } from 'react-redux'; import { AnyAction, bindActionCreators, Dispatch } from 'redux'; -import { SupersetClient, getClientErrorObject } from '@superset-ui/core'; +import { SupersetClient } from '@superset-ui/core'; import setBootstrapData from 'src/dashboard/actions/bootstrapData'; import Loading from 'src/components/Loading'; import getInitialState from '../reducers/getInitialState'; @@ -30,10 +30,25 @@ interface DashboardRouteProps { }; dashboardId: string; } +const getData = (id: string) => { + const batch = [ + SupersetClient.get({ endpoint: `/api/v1/dashboard/${id}/charts` }), + SupersetClient.get({ endpoint: `/api/v1/dashboard/${id}` }), + ]; + return Promise.all(batch) + .then(([chartRes, dashboardRes]) => ({ + chartRes: chartRes.json.result, + dashboardRes: dashboardRes.json.result, + })) + .catch(err => { + console.log('err', err); + }); +}; + const DashboardRoute: FC = ({ children, actions, - dashboardId, + dashboardId, // eventually get from react router }) => { const appContainer = document.getElementById('app'); const bootstrapData = appContainer?.getAttribute('data-bootstrap'); @@ -41,16 +56,19 @@ const DashboardRoute: FC = ({ const [loaded, setLoaded] = useState(false); useEffect(() => { - SupersetClient.get({ endpoint: `/api/v1/dashboard/${dashboardId}/charts` }) - .then(r => { - const initState = getInitialState(bootstrapDataJson, r.json.result); + getData(dashboardId).then(data => { + if (data) { + const initState = getInitialState( + bootstrapDataJson, + data.chartRes, + data.dashboardRes, + ); actions.setBootstrapData(initState); setLoaded(true); - }) - .catch(err => { - console.log('err', err); - }); + } + }); }, []); + if (!loaded) return ; return <>{children} ; }; diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index dea5778f4771a..601e9fdaa0635 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -48,18 +48,22 @@ import getLocationHash from '../util/getLocationHash'; import newComponentFactory from '../util/newComponentFactory'; import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox'; -export default function getInitialState(bootstrapData, chartdata) { +export default function getInitialState(bootstrapData, chartData, dashboardData) { + // user_id and datasources need exploration for spa const { user_id, datasources, common, editMode, urlParams } = bootstrapData; const dashboard = { ...bootstrapData.dashboard_data }; + // console.log('dashboard', dashboard, { common, editMode, datasources, urlParams }) + const metadata = JSON.parse(dashboardData.json_metadata); let preselectFilters = {}; - chartdata.forEach(chart => { + + 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') || dashboard.metadata.default_filters, + getParam('preselect_filters') || metadata.default_filters, ); } catch (e) { // @@ -67,12 +71,12 @@ export default function getInitialState(bootstrapData, chartdata) { // Priming the color palette with user's label-color mapping provided in // the dashboard's JSON metadata - 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; + if (metadata && 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; Object.keys(colorMap).forEach(label => { CategoricalColorNamespace.getScale(scheme, namespace).setColor( label, @@ -82,7 +86,7 @@ export default function getInitialState(bootstrapData, chartdata) { } // dashboard layout - const { position_json: positionJson } = dashboard; + const positionJson = JSON.parse(dashboardData.position_json); // new dash: positionJson could be {} or null const layout = positionJson && Object.keys(positionJson).length > 0 @@ -103,13 +107,13 @@ export default function getInitialState(bootstrapData, chartdata) { let newSlicesContainer; let newSlicesContainerWidth = 0; - const filterScopes = dashboard.metadata.filter_scopes || {}; + const filterScopes = metadata.filter_scopes || {}; const chartQueries = {}; const dashboardFilters = {}; const slices = {}; const sliceIds = new Set(); - chartdata.forEach(slice => { + chartData.forEach(slice => { const key = slice.slice_id; const form_data = { ...slice.form_data, @@ -272,9 +276,9 @@ export default function getInitialState(bootstrapData, chartdata) { charts: chartQueries, // read-only data dashboardInfo: { - id: dashboard.id, - slug: dashboard.slug, - metadata: dashboard.metadata, + id: dashboardData.id, + slug: dashboardData.slug, + metadata, userId: user_id, dash_edit_perm: dashboard.dash_edit_perm, dash_save_perm: dashboard.dash_save_perm, @@ -294,19 +298,19 @@ export default function getInitialState(bootstrapData, chartdata) { directPathToChild, directPathLastUpdated: Date.now(), focusedFilterField: null, - expandedSlices: dashboard.metadata.expanded_slices || {}, - refreshFrequency: dashboard.metadata.refresh_frequency || 0, + 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: dashboard.css || '', - colorNamespace: dashboard.metadata.color_namespace, - colorScheme: dashboard.metadata.color_scheme, + css: dashboardData.css || '', + colorNamespace: metadata.color_namespace, + colorScheme: metadata.color_scheme, editMode: dashboard.dash_edit_perm && editMode, - isPublished: dashboard.published, + isPublished: dashboardData.published, hasUnsavedChanges: false, maxUndoHistoryExceeded: false, - lastModifiedTime: dashboard.last_modified_time, + lastModifiedTime: dashboardData.changed_on, }, dashboardLayout, messageToasts: [], From 82b6984237db1b07fec1839e2238dd9b5e3c56c0 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Mon, 22 Feb 2021 16:33:48 -0800 Subject: [PATCH 08/59] update namings --- superset-frontend/src/chart/chartReducer.js | 4 ++-- .../src/dashboard/actions/bootstrapData.js | 6 +++--- .../src/dashboard/actions/nativeFilters.ts | 4 ++-- .../src/dashboard/components/DashboardRoute.tsx | 8 ++++---- .../src/dashboard/reducers/dashboardFilters.js | 4 ++-- .../src/dashboard/reducers/dashboardInfo.js | 4 ++-- .../src/dashboard/reducers/dashboardLayout.js | 4 ++-- .../src/dashboard/reducers/dashboardState.js | 4 ++-- .../src/dashboard/reducers/datasources.js | 4 ++-- .../src/dashboard/reducers/getInitialState.js | 10 ++++++++-- .../src/dashboard/reducers/nativeFilters.ts | 4 ++-- .../src/dashboard/reducers/sliceEntities.js | 4 ++-- .../src/dashboard/reducers/undoableDashboardLayout.js | 4 ++-- 13 files changed, 35 insertions(+), 29 deletions(-) diff --git a/superset-frontend/src/chart/chartReducer.js b/superset-frontend/src/chart/chartReducer.js index 217da446f803d..cc26a32cc7cf7 100644 --- a/superset-frontend/src/chart/chartReducer.js +++ b/superset-frontend/src/chart/chartReducer.js @@ -21,7 +21,7 @@ import { t } from '@superset-ui/core'; import { getFormDataFromControls } from 'src/explore/controlUtils'; import { now } from '../modules/dates'; import * as actions from './chartAction'; -import { SET_BOOTSTRAP_DATA } from '../dashboard/actions/bootstrapData'; +import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../dashboard/actions/bootstrapData'; export const chart = { id: 0, @@ -192,7 +192,7 @@ export default function chartReducer(charts = {}, action) { delete charts[key]; return charts; } - if (action.type === SET_BOOTSTRAP_DATA) { + if (action.type === LOAD_DASHBOARD_BOOTSTRAPDATA) { return { ...action.data.charts }; } if (action.type in actionHandlers) { diff --git a/superset-frontend/src/dashboard/actions/bootstrapData.js b/superset-frontend/src/dashboard/actions/bootstrapData.js index 63b98b9cd849e..db3e6720f88a1 100644 --- a/superset-frontend/src/dashboard/actions/bootstrapData.js +++ b/superset-frontend/src/dashboard/actions/bootstrapData.js @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -export const SET_BOOTSTRAP_DATA = 'SET_BOOTSTRAP_DATA'; +export const LOAD_DASHBOARD_BOOTSTRAPDATA = 'LOAD_DASHBOARD_BOOTSTRAPDATA'; -export default function setBootstrapData(data) { +export default function setDashboardData(data) { return { - type: SET_BOOTSTRAP_DATA, + type: LOAD_DASHBOARD_BOOTSTRAPDATA, data, }; } diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts index 337599143770e..d0c5ef9e45ef6 100644 --- a/superset-frontend/src/dashboard/actions/nativeFilters.ts +++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts @@ -23,7 +23,7 @@ import { Filter, FilterConfiguration, } from 'src/dashboard/components/nativeFilters/types'; -import { SET_BOOTSTRAP_DATA } from './bootstrapData'; +import { LOAD_DASHBOARD_BOOTSTRAPDATA } from './bootstrapData'; import { dashboardInfoChanged } from './dashboardInfo'; import { CurrentFilterState } from '../reducers/types'; import { SelectedValues } from '../components/nativeFilters/FilterConfigModal/types'; @@ -104,7 +104,7 @@ type Data = { }; export interface SetBooststapData { - type: typeof SET_BOOTSTRAP_DATA; + type: typeof LOAD_DASHBOARD_BOOTSTRAPDATA; data: Data; } diff --git a/superset-frontend/src/dashboard/components/DashboardRoute.tsx b/superset-frontend/src/dashboard/components/DashboardRoute.tsx index 18809a34f17b1..c317f29cc3864 100644 --- a/superset-frontend/src/dashboard/components/DashboardRoute.tsx +++ b/superset-frontend/src/dashboard/components/DashboardRoute.tsx @@ -20,13 +20,13 @@ import React, { useEffect, useState, FC } from 'react'; import { connect } from 'react-redux'; import { AnyAction, bindActionCreators, Dispatch } from 'redux'; import { SupersetClient } from '@superset-ui/core'; -import setBootstrapData from 'src/dashboard/actions/bootstrapData'; +import setInitialState from 'src/dashboard/actions/bootstrapData'; import Loading from 'src/components/Loading'; import getInitialState from '../reducers/getInitialState'; interface DashboardRouteProps { actions: { - setBootstrapData: (arg0: object) => void; + setInitialState: (arg0: object) => void; }; dashboardId: string; } @@ -63,7 +63,7 @@ const DashboardRoute: FC = ({ data.chartRes, data.dashboardRes, ); - actions.setBootstrapData(initState); + actions.setInitialState(initState); setLoaded(true); } }); @@ -77,7 +77,7 @@ function mapDispatchToProps(dispatch: Dispatch) { return { actions: bindActionCreators( { - setBootstrapData, + setInitialState, }, dispatch, ), diff --git a/superset-frontend/src/dashboard/reducers/dashboardFilters.js b/superset-frontend/src/dashboard/reducers/dashboardFilters.js index 70b3eefbf02a6..01c0e27cbb289 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardFilters.js +++ b/superset-frontend/src/dashboard/reducers/dashboardFilters.js @@ -25,7 +25,7 @@ import { UPDATE_LAYOUT_COMPONENTS, UPDATE_DASHBOARD_FILTERS_SCOPE, } from '../actions/dashboardFilters'; -import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; +import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox'; import { DASHBOARD_ROOT_ID } from '../util/constants'; import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata'; @@ -162,7 +162,7 @@ export default function dashboardFiltersReducer(dashboardFilters = {}, action) { return updatedFilters; } - if (action.type === SET_BOOTSTRAP_DATA) { + if (action.type === LOAD_DASHBOARD_BOOTSTRAPDATA) { return action.data.dashboardFilters; } diff --git a/superset-frontend/src/dashboard/reducers/dashboardInfo.js b/superset-frontend/src/dashboard/reducers/dashboardInfo.js index cf569f167b5c7..87803eb1068a9 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardInfo.js +++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.js @@ -18,7 +18,7 @@ */ import { DASHBOARD_INFO_UPDATED } from '../actions/dashboardInfo'; -import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; +import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; export default function dashboardStateReducer(state = {}, action) { switch (action.type) { @@ -29,7 +29,7 @@ export default function dashboardStateReducer(state = {}, action) { // server-side compare last_modified_time in second level lastModifiedTime: Math.round(new Date().getTime() / 1000), }; - case SET_BOOTSTRAP_DATA: + case LOAD_DASHBOARD_BOOTSTRAPDATA: return { ...state, ...action.data.dashboardInfo, diff --git a/superset-frontend/src/dashboard/reducers/dashboardLayout.js b/superset-frontend/src/dashboard/reducers/dashboardLayout.js index 22e68a7c4a92e..4972a45fe8814 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardLayout.js +++ b/superset-frontend/src/dashboard/reducers/dashboardLayout.js @@ -43,10 +43,10 @@ import { DASHBOARD_TITLE_CHANGED, } from '../actions/dashboardLayout'; -import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; +import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; const actionHandlers = { - [SET_BOOTSTRAP_DATA](state, action) { + [LOAD_DASHBOARD_BOOTSTRAPDATA](state, action) { return { ...action.data.dashboardLayout.present, }; diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js index 31c1884bb7fb2..482d832d61829 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.js @@ -36,11 +36,11 @@ import { SET_FOCUSED_FILTER_FIELD, UNSET_FOCUSED_FILTER_FIELD, } from '../actions/dashboardState'; -import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; +import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; export default function dashboardStateReducer(state = {}, action) { const actionHandlers = { - [SET_BOOTSTRAP_DATA]() { + [LOAD_DASHBOARD_BOOTSTRAPDATA]() { return { ...state, ...action.data.dashboardState }; }, [UPDATE_CSS]() { diff --git a/superset-frontend/src/dashboard/reducers/datasources.js b/superset-frontend/src/dashboard/reducers/datasources.js index 0e6682b5d3282..adfde757e5d60 100644 --- a/superset-frontend/src/dashboard/reducers/datasources.js +++ b/superset-frontend/src/dashboard/reducers/datasources.js @@ -17,11 +17,11 @@ * under the License. */ import { SET_DATASOURCE } from '../actions/datasources'; -import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; +import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; export default function datasourceReducer(datasources = {}, action) { const actionHandlers = { - [SET_BOOTSTRAP_DATA]() { + [LOAD_DASHBOARD_BOOTSTRAPDATA]() { return action.data.datasources; }, [SET_DATASOURCE]() { diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index 601e9fdaa0635..4c1e648e928ac 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -48,11 +48,17 @@ import getLocationHash from '../util/getLocationHash'; import newComponentFactory from '../util/newComponentFactory'; import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox'; -export default function getInitialState(bootstrapData, chartData, dashboardData) { +export default function getInitialState( + bootstrapData, + chartData, + dashboardData, +) { // user_id and datasources need exploration for spa + console.log('dashboarddata', dashboardData) const { user_id, datasources, common, editMode, urlParams } = bootstrapData; const dashboard = { ...bootstrapData.dashboard_data }; - // console.log('dashboard', dashboard, { common, editMode, datasources, urlParams }) + console.log('chartData', chartData) + console.log('dashboard', dashboard, { common, editMode, datasources, urlParams }) const metadata = JSON.parse(dashboardData.json_metadata); let preselectFilters = {}; diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts index ff8a2aad3d280..0054f304b67ab 100644 --- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts +++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts @@ -23,7 +23,7 @@ import { } from 'src/dashboard/actions/nativeFilters'; import { NativeFiltersState, NativeFilterState } from './types'; import { FilterConfiguration } from '../components/nativeFilters/types'; -import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; +import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; export function getInitialFilterState(id: string): NativeFilterState { return { @@ -53,7 +53,7 @@ export default function nativeFilterReducer( ) { const { filters, filtersState } = state; switch (action.type) { - case SET_BOOTSTRAP_DATA: + case LOAD_DASHBOARD_BOOTSTRAPDATA: return { filters: action.data.nativeFilters.filters, filtersState: action.data.nativeFilters.filtersState, diff --git a/superset-frontend/src/dashboard/reducers/sliceEntities.js b/superset-frontend/src/dashboard/reducers/sliceEntities.js index 34a302f541fc9..d4a0f2132c229 100644 --- a/superset-frontend/src/dashboard/reducers/sliceEntities.js +++ b/superset-frontend/src/dashboard/reducers/sliceEntities.js @@ -23,7 +23,7 @@ import { FETCH_ALL_SLICES_STARTED, SET_ALL_SLICES, } from '../actions/sliceEntities'; -import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; +import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; export const initSliceEntities = { slices: {}, @@ -37,7 +37,7 @@ export default function sliceEntitiesReducer( action, ) { const actionHandlers = { - [SET_BOOTSTRAP_DATA]() { + [LOAD_DASHBOARD_BOOTSTRAPDATA]() { return { ...action.data.sliceEntities, }; diff --git a/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js b/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js index bc1aa8dc361f7..30e39735f63d1 100644 --- a/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js +++ b/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js @@ -29,7 +29,7 @@ import { HANDLE_COMPONENT_DROP, } from '../actions/dashboardLayout'; -import { SET_BOOTSTRAP_DATA } from '../actions/bootstrapData'; +import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; import dashboardLayout from './dashboardLayout'; @@ -38,7 +38,7 @@ export default undoable(dashboardLayout, { // +1 again so we can detect if we've exceeded the limit limit: UNDO_LIMIT + 2, filter: includeAction([ - SET_BOOTSTRAP_DATA, + LOAD_DASHBOARD_BOOTSTRAPDATA, UPDATE_COMPONENTS, DELETE_COMPONENT, CREATE_COMPONENT, From 02a2dd1dbd84970c560dde169b69df6e0199a190 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Mon, 22 Feb 2021 22:10:53 -0800 Subject: [PATCH 09/59] accept an id or a slug in the dashboard charts api --- superset/dashboards/api.py | 10 +++++----- superset/dashboards/dao.py | 10 ++++++++-- tests/dashboards/api_tests.py | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index 76787117fd61c..8a0a4b638dd91 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -222,7 +222,7 @@ def __init__(self) -> None: self.include_route_methods = self.include_route_methods | {"thumbnail"} super().__init__() - @expose("//charts", methods=["GET"]) + @expose("//charts", methods=["GET"]) @protect() @safe @statsd_metrics @@ -230,7 +230,7 @@ def __init__(self) -> None: action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_charts", log_to_statsd=False, ) - def get_charts(self, pk: int) -> Response: + def get_charts(self, id_or_slug: str) -> Response: """Gets the chart definitions for a given dashboard --- get: @@ -239,8 +239,8 @@ def get_charts(self, pk: int) -> Response: parameters: - in: path schema: - type: integer - name: pk + type: string + name: id_or_slug responses: 200: description: Dashboard chart definitions @@ -263,7 +263,7 @@ def get_charts(self, pk: int) -> Response: $ref: '#/components/responses/404' """ try: - charts = DashboardDAO.get_charts_for_dashboard(pk) + charts = DashboardDAO.get_charts_for_dashboard(id_or_slug) result = [self.chart_entity_response_schema.dump(chart) for chart in charts] return self.response(200, result=result) except DashboardNotFoundError: diff --git a/superset/dashboards/dao.py b/superset/dashboards/dao.py index 7a6d1b79b6ebf..17e1ce87a463c 100644 --- a/superset/dashboards/dao.py +++ b/superset/dashboards/dao.py @@ -18,6 +18,7 @@ import logging from typing import Any, Dict, List, Optional +from sqlalchemy import or_ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import contains_eager @@ -38,12 +39,17 @@ class DashboardDAO(BaseDAO): base_filter = DashboardFilter @staticmethod - def get_charts_for_dashboard(dashboard_id: int) -> List[Slice]: + def get_charts_for_dashboard(dashboard_id_or_slug: str) -> List[Slice]: query = ( db.session.query(Dashboard) .outerjoin(Slice, Dashboard.slices) .outerjoin(Slice.table) - .filter(Dashboard.id == dashboard_id) + .filter( + or_( + Dashboard.id == dashboard_id_or_slug, + Dashboard.slug == dashboard_id_or_slug, + ) + ) .options(contains_eager(Dashboard.slices)) ) dashboard = query.one_or_none() diff --git a/tests/dashboards/api_tests.py b/tests/dashboards/api_tests.py index 5f5504e318119..a8b90a874a731 100644 --- a/tests/dashboards/api_tests.py +++ b/tests/dashboards/api_tests.py @@ -184,6 +184,22 @@ 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): """ From 2450a8534274aa8f6d156746b3f62af0a6026916 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Tue, 23 Feb 2021 16:10:17 -0800 Subject: [PATCH 10/59] add permissions function --- .../src/dashboard/reducers/getInitialState.js | 28 +++++++++++-------- .../src/dashboard/util/getPermissions.tsx | 27 ++++++++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 superset-frontend/src/dashboard/util/getPermissions.tsx diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index 4c1e648e928ac..84fb0aa0f2fa5 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -26,6 +26,7 @@ import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/re 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, @@ -53,12 +54,15 @@ export default function getInitialState( chartData, dashboardData, ) { - // user_id and datasources need exploration for spa - console.log('dashboarddata', dashboardData) - const { user_id, datasources, common, editMode, urlParams } = bootstrapData; + const { + user_id, + datasources, + common, + editMode, + urlParams, + user, + } = bootstrapData; const dashboard = { ...bootstrapData.dashboard_data }; - console.log('chartData', chartData) - console.log('dashboard', dashboard, { common, editMode, datasources, urlParams }) const metadata = JSON.parse(dashboardData.json_metadata); let preselectFilters = {}; @@ -276,6 +280,8 @@ export default function getInitialState( dashboard.metadata.filter_configuration || [], ); + const roles = user.roles.Admin; + return { datasources, sliceEntities: { ...initSliceEntities, slices, isLoading: false }, @@ -286,11 +292,11 @@ export default function getInitialState( slug: dashboardData.slug, 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, + dash_edit_perm: getPermissions('can_write', 'Dashboard', roles), + dash_save_perm: getPermissions('can_save_dash', 'Superset', roles), + superset_can_explore: getPermissions('can_explore', 'Superset', roles), + superset_can_csv: getPermissions('can_csv', 'Superset', roles), + slice_can_edit: getPermissions('can_slice', 'Superset', roles), common: { flash_messages: common.flash_messages, conf: common.conf, @@ -312,7 +318,7 @@ export default function getInitialState( css: dashboardData.css || '', colorNamespace: metadata.color_namespace, colorScheme: metadata.color_scheme, - editMode: dashboard.dash_edit_perm && editMode, + editMode: getPermissions('can_write', 'Dashboard', roles) && editMode, isPublished: dashboardData.published, hasUnsavedChanges: false, maxUndoHistoryExceeded: false, diff --git a/superset-frontend/src/dashboard/util/getPermissions.tsx b/superset-frontend/src/dashboard/util/getPermissions.tsx new file mode 100644 index 0000000000000..32faffc4670b8 --- /dev/null +++ b/superset-frontend/src/dashboard/util/getPermissions.tsx @@ -0,0 +1,27 @@ +/** + * 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. + */ +export default function getPermissions( + perm: string, + view: string, + roles: Array, +) { + if (!roles.length) return false; + + return Boolean(roles.find(role => role[0] === perm && role[1] === view)); +} From 168bd0e4def27f528d1b4248d27f7c8f30a01911 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Tue, 23 Feb 2021 17:07:56 -0800 Subject: [PATCH 11/59] fix merge --- superset-frontend/src/dashboard/actions/nativeFilters.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts index a020a8f69c136..335ad900f97ea 100644 --- a/superset-frontend/src/dashboard/actions/nativeFilters.ts +++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts @@ -227,9 +227,5 @@ export type AnyFilterAction = | SetFilterSetsConfigFail | SetFiltersState | SetExtraFormData -<<<<<<< HEAD - | SetFilterState - | SetBooststapData; -======= + | SetBooststapData | SaveFilterSets; ->>>>>>> master From e9c0a74c8e1f6075539e93e5d7ca81f81c14f8b0 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Wed, 10 Mar 2021 13:29:06 -0800 Subject: [PATCH 12/59] update state --- .../src/dashboard/actions/nativeFilters.ts | 10 +++++++--- .../src/dashboard/reducers/nativeFilters.ts | 14 -------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts index 2d18ae5cc318b..5749dd0c9a7f5 100644 --- a/superset-frontend/src/dashboard/actions/nativeFilters.ts +++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts @@ -19,13 +19,16 @@ import { makeApi } from '@superset-ui/core'; import { Dispatch } from 'redux'; -import { LOAD_DASHBOARD_BOOTSTRAPDATA } from './bootstrapData'; -import { FilterConfiguration } from 'src/dashboard/components/nativeFilters/types'; +import { + Filter, + 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 { LOAD_DASHBOARD_BOOTSTRAPDATA } from './bootstrapData'; import { dashboardInfoChanged } from './dashboardInfo'; import { FiltersSet } from '../reducers/types'; @@ -189,4 +192,5 @@ export type AnyFilterAction = | SetFilterSetsConfigBegin | SetFilterSetsConfigComplete | SetFilterSetsConfigFail - | SaveFilterSets; + | SaveFilterSets + | SetBooststapData; diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts index 51f5dd5a8fe15..2f8af37f5c968 100644 --- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts +++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts @@ -73,20 +73,6 @@ export default function nativeFilterReducer( case LOAD_DASHBOARD_BOOTSTRAPDATA: return { filters: action.data.nativeFilters.filters, - filtersState: action.data.nativeFilters.filtersState, - }; - case SET_EXTRA_FORM_DATA: - return { - ...state, - filters, - filtersState: { - ...filtersState, - [action.filterId]: { - ...filtersState[action.filterId], - extraFormData: action.extraFormData, - currentState: action.currentState, - }, - }, }; case SAVE_FILTER_SETS: return { From 96d5a826fb275901af43a3c5684eb3e9ce57d0b1 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 10 Mar 2021 13:38:20 -0800 Subject: [PATCH 13/59] get dashboard charts by id or slug --- superset/dashboards/api.py | 2 +- superset/dashboards/dao.py | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index 99f67a80954a0..678c09e37d7aa 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -252,7 +252,7 @@ def get(self, id_or_slug: str) -> Response: except DashboardNotFoundError: return self.response_404() - @expose("//charts", methods=["GET"]) + @expose("//charts", methods=["GET"]) @protect() @safe @statsd_metrics diff --git a/superset/dashboards/dao.py b/superset/dashboards/dao.py index 9d4fead1c088b..0b57fc7831861 100644 --- a/superset/dashboards/dao.py +++ b/superset/dashboards/dao.py @@ -59,17 +59,12 @@ def get_by_id_or_slug(id_or_slug: str) -> Dashboard: return dashboard @staticmethod - def get_charts_for_dashboard(dashboard_id: int) -> List[Slice]: + def get_charts_for_dashboard(id_or_slug: str) -> List[Slice]: query = ( db.session.query(Dashboard) .outerjoin(Slice, Dashboard.slices) .outerjoin(Slice.table) - .filter( - or_( - Dashboard.id == dashboard_id_or_slug, - Dashboard.slug == dashboard_id_or_slug, - ) - ) + .filter(id_or_slug_filter(id_or_slug)) .options(contains_eager(Dashboard.slices)) ) # Apply dashboard base filters From 46ffec7d63bf42a657d4ee4f105301b8f10803ac Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Wed, 10 Mar 2021 14:34:30 -0800 Subject: [PATCH 14/59] fix undefined states --- .../src/dashboard/reducers/getInitialState.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index 3fc5d09d36744..47b887852115e 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -117,7 +117,7 @@ export default function getInitialState( let newSlicesContainer; let newSlicesContainerWidth = 0; - const filterScopes = metadata.filter_scopes || {}; + const filterScopes = (metadata && metadata.filter_scopes) || {}; const chartQueries = {}; const dashboardFilters = {}; @@ -310,14 +310,14 @@ export default function getInitialState( directPathToChild, directPathLastUpdated: Date.now(), focusedFilterField: null, - expandedSlices: metadata.expanded_slices || {}, - refreshFrequency: metadata.refresh_frequency || 0, + expandedSlices: (metadata && metadata.expanded_slices) || {}, + refreshFrequency: (metadata && 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, - colorScheme: metadata.color_scheme, + colorNamespace: metadata?.color_namespace || null, + colorScheme: metadata?.color_scheme || null, editMode: getPermissions('can_write', 'Dashboard', roles) && editMode, isPublished: dashboardData.published, hasUnsavedChanges: false, From d0d31178a0e604cf310310c06c97fa8f94d7233f Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 10 Mar 2021 16:11:20 -0800 Subject: [PATCH 15/59] variable names --- superset-frontend/src/dashboard/App.jsx | 4 ++-- .../src/dashboard/components/DashboardRoute.tsx | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/superset-frontend/src/dashboard/App.jsx b/superset-frontend/src/dashboard/App.jsx index 9050f275f0462..0c577a99114b2 100644 --- a/superset-frontend/src/dashboard/App.jsx +++ b/superset-frontend/src/dashboard/App.jsx @@ -33,13 +33,13 @@ setupApp(); setupPlugins(); const App = ({ store }) => { - const dashboardId = window.location.pathname.split('/')[3]; + const dashboardIdOrSlug = window.location.pathname.split('/')[3]; return ( - + diff --git a/superset-frontend/src/dashboard/components/DashboardRoute.tsx b/superset-frontend/src/dashboard/components/DashboardRoute.tsx index c317f29cc3864..faea5c6680539 100644 --- a/superset-frontend/src/dashboard/components/DashboardRoute.tsx +++ b/superset-frontend/src/dashboard/components/DashboardRoute.tsx @@ -28,12 +28,12 @@ interface DashboardRouteProps { actions: { setInitialState: (arg0: object) => void; }; - dashboardId: string; + dashboardIdOrSlug: string; } -const getData = (id: string) => { +const getData = (idOrSlug: string) => { const batch = [ - SupersetClient.get({ endpoint: `/api/v1/dashboard/${id}/charts` }), - SupersetClient.get({ endpoint: `/api/v1/dashboard/${id}` }), + SupersetClient.get({ endpoint: `/api/v1/dashboard/${idOrSlug}/charts` }), + SupersetClient.get({ endpoint: `/api/v1/dashboard/${idOrSlug}` }), ]; return Promise.all(batch) .then(([chartRes, dashboardRes]) => ({ @@ -48,7 +48,7 @@ const getData = (id: string) => { const DashboardRoute: FC = ({ children, actions, - dashboardId, // eventually get from react router + dashboardIdOrSlug, // eventually get from react router }) => { const appContainer = document.getElementById('app'); const bootstrapData = appContainer?.getAttribute('data-bootstrap'); @@ -56,7 +56,7 @@ const DashboardRoute: FC = ({ const [loaded, setLoaded] = useState(false); useEffect(() => { - getData(dashboardId).then(data => { + getData(dashboardIdOrSlug).then(data => { if (data) { const initState = getInitialState( bootstrapDataJson, @@ -67,7 +67,7 @@ const DashboardRoute: FC = ({ setLoaded(true); } }); - }, []); + }, [dashboardIdOrSlug]); if (!loaded) return ; return <>{children} ; From d170b794b478c8a1f6e2507131bc8b9c63f371d9 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 10 Mar 2021 16:12:16 -0800 Subject: [PATCH 16/59] stop using some more bootstrap data --- .../src/dashboard/reducers/getInitialState.js | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index 47b887852115e..541b558e6922f 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -20,6 +20,7 @@ import { isString } from 'lodash'; import shortid from 'shortid'; import { CategoricalColorNamespace } from '@superset-ui/core'; +import querystring from 'query-string'; import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities'; import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters'; @@ -49,21 +50,34 @@ 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 +const extractUrlParams = queryParams => + Object.entries(queryParams).reduce((acc, [key, value]) => { + if (reservedQueryParams.has(key)) return acc; + if (Array.isArray(value)) { + return { + ...acc, + [key]: value[0], + }; + } + return { ...acc, [key]: value }; + }, {}); + export default function getInitialState( bootstrapData, chartData, dashboardData, ) { - const { - user_id, - datasources, - common, - editMode, - urlParams, - user, - } = bootstrapData; + const { datasources, common, user } = bootstrapData; const dashboard = { ...bootstrapData.dashboard_data }; const metadata = JSON.parse(dashboardData.json_metadata); + const queryParams = querystring.parse(window.location.search); + const urlParams = extractUrlParams(queryParams); + const editMode = queryParams.edit === 'true'; + let preselectFilters = {}; chartData.forEach(chart => { @@ -291,7 +305,7 @@ export default function getInitialState( id: dashboardData.id, slug: dashboardData.slug, metadata, - userId: user_id, + userId: user.id, dash_edit_perm: getPermissions('can_write', 'Dashboard', roles), dash_save_perm: getPermissions('can_save_dash', 'Superset', roles), superset_can_explore: getPermissions('can_explore', 'Superset', roles), From ff023bcf32811ceb39e37c1dd3b81c1eed45b966 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 10 Mar 2021 16:15:06 -0800 Subject: [PATCH 17/59] fix metadata reference --- superset-frontend/src/dashboard/reducers/getInitialState.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index 541b558e6922f..9c55e1cc898c6 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -290,8 +290,8 @@ export default function getInitialState( } const nativeFilters = getInitialNativeFilterState({ - filterConfig: dashboard.metadata.filter_configuration || [], - filterSetsConfig: dashboard.metadata.filter_sets_configuration || [], + filterConfig: metadata.filter_configuration || [], + filterSetsConfig: metadata.filter_sets_configuration || [], }); const roles = user.roles.Admin; From 8034e91d78f3144ade3b1b25a5c9dd7e6d26cd70 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 10 Mar 2021 16:22:30 -0800 Subject: [PATCH 18/59] remove unused bootstrap from the template --- superset/views/core.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/superset/views/core.py b/superset/views/core.py index 8c24cbe628d73..f10102c0fe5d8 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1868,20 +1868,8 @@ def dashboard( # pylint: disable=too-many-locals } bootstrap_data = { - "user_id": g.user.get_id(), "user": bootstrap_user_data(g.user, include_perms=True), "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"], } From 23fe65ba8418d5da71f335809de08be8a0ceb951 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Wed, 10 Mar 2021 16:38:51 -0800 Subject: [PATCH 19/59] add errorboundry to dashboard --- .../dashboard/components/DashboardRoute.tsx | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/superset-frontend/src/dashboard/components/DashboardRoute.tsx b/superset-frontend/src/dashboard/components/DashboardRoute.tsx index c317f29cc3864..f6f7a5d28c242 100644 --- a/superset-frontend/src/dashboard/components/DashboardRoute.tsx +++ b/superset-frontend/src/dashboard/components/DashboardRoute.tsx @@ -22,6 +22,7 @@ import { AnyAction, bindActionCreators, Dispatch } from 'redux'; import { SupersetClient } from '@superset-ui/core'; import setInitialState from 'src/dashboard/actions/bootstrapData'; import Loading from 'src/components/Loading'; +import ErrorBoundary from 'src/components/ErrorBoundary'; import getInitialState from '../reducers/getInitialState'; interface DashboardRouteProps { @@ -35,14 +36,10 @@ const getData = (id: string) => { SupersetClient.get({ endpoint: `/api/v1/dashboard/${id}/charts` }), SupersetClient.get({ endpoint: `/api/v1/dashboard/${id}` }), ]; - return Promise.all(batch) - .then(([chartRes, dashboardRes]) => ({ - chartRes: chartRes.json.result, - dashboardRes: dashboardRes.json.result, - })) - .catch(err => { - console.log('err', err); - }); + return Promise.all(batch).then(([chartRes, dashboardRes]) => ({ + chartRes: chartRes.json.result, + dashboardRes: dashboardRes.json.result, + })); }; const DashboardRoute: FC = ({ @@ -55,22 +52,29 @@ const DashboardRoute: FC = ({ const bootstrapDataJson = JSON.parse(bootstrapData || ''); const [loaded, setLoaded] = useState(false); + const handleError = (error: unknown) => ({ error, info: null }); + useEffect(() => { - getData(dashboardId).then(data => { - if (data) { - const initState = getInitialState( - bootstrapDataJson, - data.chartRes, - data.dashboardRes, - ); - actions.setInitialState(initState); + getData(dashboardId) + .then(data => { + if (data) { + const initState = getInitialState( + bootstrapDataJson, + data.chartRes, + data.dashboardRes, + ); + actions.setInitialState(initState); + setLoaded(true); + } + }) + .catch(err => { setLoaded(true); - } - }); + handleError(err); + }); }, []); if (!loaded) return ; - return <>{children} ; + return {children} ; }; function mapDispatchToProps(dispatch: Dispatch) { From 64137af51af0ea80196d1167a72a3f72e7645e3e Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Thu, 11 Mar 2021 12:51:47 -0800 Subject: [PATCH 20/59] refactoring, fixing --- .../src/dashboard/actions/bootstrapData.js | 333 ++++++++++++++++- .../dashboard/components/DashboardRoute.tsx | 15 +- superset-frontend/src/dashboard/index.jsx | 7 + .../src/dashboard/reducers/getInitialState.js | 345 ------------------ .../src/dashboard/reducers/index.js | 2 + superset/charts/schemas.py | 1 + 6 files changed, 348 insertions(+), 355 deletions(-) delete mode 100644 superset-frontend/src/dashboard/reducers/getInitialState.js diff --git a/superset-frontend/src/dashboard/actions/bootstrapData.js b/superset-frontend/src/dashboard/actions/bootstrapData.js index db3e6720f88a1..debdbdacf1810 100644 --- a/superset-frontend/src/dashboard/actions/bootstrapData.js +++ b/superset-frontend/src/dashboard/actions/bootstrapData.js @@ -16,11 +16,340 @@ * specific language governing permissions and limitations * under the License. */ -export const LOAD_DASHBOARD_BOOTSTRAPDATA = 'LOAD_DASHBOARD_BOOTSTRAPDATA'; +/* eslint-disable camelcase */ +import { isString } from 'lodash'; +import shortid from 'shortid'; +import { CategoricalColorNamespace } from '@superset-ui/core'; +import querystring from 'query-string'; + +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 '../reducers/dashboardFilters'; +import { chart } from '../../chart/chartReducer'; +import { + DASHBOARD_HEADER_ID, + GRID_DEFAULT_CHART_WIDTH, + GRID_COLUMN_COUNT, +} from '../util/constants'; +import { + DASHBOARD_HEADER_TYPE, + CHART_TYPE, + ROW_TYPE, +} 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 +const extractUrlParams = queryParams => + Object.entries(queryParams).reduce((acc, [key, value]) => { + if (reservedQueryParams.has(key)) return acc; + if (Array.isArray(value)) { + return { + ...acc, + [key]: value[0], + }; + } + return { ...acc, [key]: value }; + }, {}); -export default function setDashboardData(data) { +export const LOAD_DASHBOARD_BOOTSTRAPDATA = 'LOAD_DASHBOARD_BOOTSTRAPDATA'; +export function setDashboardBootstrapState(data) { return { type: LOAD_DASHBOARD_BOOTSTRAPDATA, data, }; } + +export const bootstrapDashboardState = ( + datasourcesData, + chartData, + dashboardData, +) => (dispatch, getState) => { + const { user, common } = getState(); + const metadata = JSON.parse(dashboardData.json_metadata); + const queryParams = querystring.parse(window.location.search); + const urlParams = extractUrlParams(queryParams); + const editMode = queryParams.edit === 'true'; + + 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, + ); + } catch (e) { + // + } + + // Priming the color palette with user's label-color mapping provided in + // the dashboard's JSON metadata + if (metadata && 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; + Object.keys(colorMap).forEach(label => { + CategoricalColorNamespace.getScale(scheme, namespace).setColor( + label, + colorMap[label], + ); + }); + } + + // dashboard layout + const positionJson = JSON.parse(dashboardData.position_json); + // new dash: positionJson could be {} or null + const layout = + positionJson && Object.keys(positionJson).length > 0 + ? positionJson + : getEmptyLayout(); + + // create a lookup to sync layout names with slice names + const chartIdToLayoutId = {}; + Object.values(layout).forEach(layoutComponent => { + if (layoutComponent.type === CHART_TYPE) { + chartIdToLayoutId[layoutComponent.meta.chartId] = layoutComponent.id; + } + }); + + // find root level chart container node for newly-added slices + const parentId = findFirstParentContainerId(layout); + const parent = layout[parentId]; + let newSlicesContainer; + let newSlicesContainerWidth = 0; + + const filterScopes = (metadata && metadata.filter_scopes) || {}; + + const chartQueries = {}; + const dashboardFilters = {}; + const slices = {}; + const sliceIds = new Set(); + chartData.forEach(slice => { + const key = slice.slice_id; + const form_data = { + ...slice.form_data, + url_params: { + ...slice.form_data.url_params, + ...urlParams, + }, + }; + chartQueries[key] = { + ...chart, + id: key, + form_data, + formData: applyDefaultFormData(form_data), + }; + + slices[key] = { + slice_id: key, + slice_url: slice.slice_url, + slice_name: slice.slice_name, + form_data: slice.form_data, + viz_type: slice.form_data.viz_type, + datasource: slice.form_data.datasource, + description: slice.description, + description_markeddown: slice.description_markeddown, + owners: slice.owners, + modified: slice.modified, + changed_on: new Date(slice.changed_on).getTime(), + }; + + sliceIds.add(key); + + // if there are newly added slices from explore view, fill slices into 1 or more rows + if (!chartIdToLayoutId[key] && layout[parentId]) { + if ( + newSlicesContainerWidth === 0 || + newSlicesContainerWidth + GRID_DEFAULT_CHART_WIDTH > GRID_COLUMN_COUNT + ) { + newSlicesContainer = newComponentFactory( + ROW_TYPE, + (parent.parents || []).slice(), + ); + layout[newSlicesContainer.id] = newSlicesContainer; + parent.children.push(newSlicesContainer.id); + newSlicesContainerWidth = 0; + } + + const chartHolder = newComponentFactory( + CHART_TYPE, + { + chartId: slice.slice_id, + }, + (newSlicesContainer.parents || []).slice(), + ); + + layout[chartHolder.id] = chartHolder; + newSlicesContainer.children.push(chartHolder.id); + chartIdToLayoutId[chartHolder.meta.chartId] = chartHolder.id; + newSlicesContainerWidth += GRID_DEFAULT_CHART_WIDTH; + } + + // build DashboardFilters for interactive filter features + if ( + slice.form_data.viz_type === 'filter_box' || + slice.form_data.viz_type === 'filter_select' + ) { + const configs = getFilterConfigsFromFormdata(slice.form_data); + let { columns } = configs; + const { labels } = configs; + if (preselectFilters[key]) { + Object.keys(columns).forEach(col => { + if (preselectFilters[key][col]) { + columns = { + ...columns, + [col]: preselectFilters[key][col], + }; + } + }); + } + + const scopesByChartId = Object.keys(columns).reduce((map, column) => { + const scopeSettings = { + ...filterScopes[key], + }; + const { scope, immune } = { + ...DASHBOARD_FILTER_SCOPE_GLOBAL, + ...scopeSettings[column], + }; + + return { + ...map, + [column]: { + scope, + immune, + }, + }; + }, {}); + + const componentId = chartIdToLayoutId[key]; + const directPathToFilter = (layout[componentId].parents || []).slice(); + directPathToFilter.push(componentId); + dashboardFilters[key] = { + ...dashboardFilter, + chartId: key, + componentId, + datasourceId: slice.form_data.datasource, + filterName: slice.slice_name, + directPathToFilter, + columns, + labels, + scopes: scopesByChartId, + isInstantFilter: !!slice.form_data.instant_filtering, + isDateFilter: Object.keys(columns).includes(TIME_RANGE), + }; + } + + // sync layout names with current slice names in case a slice was edited + // in explore since the layout was updated. name updates go through layout for undo/redo + // functionality and python updates slice names based on layout upon dashboard save + const layoutId = chartIdToLayoutId[key]; + if (layoutId && layout[layoutId]) { + layout[layoutId].meta.sliceName = slice.slice_name; + } + }); + buildActiveFilters({ + dashboardFilters, + components: layout, + }); + + // store the header as a layout component so we can undo/redo changes + layout[DASHBOARD_HEADER_ID] = { + id: DASHBOARD_HEADER_ID, + type: DASHBOARD_HEADER_TYPE, + meta: { + text: dashboardData.dashboard_title, + }, + }; + + const dashboardLayout = { + past: [], + present: layout, + future: [], + }; + + // find direct link component and path from root + const directLinkComponentId = getLocationHash(); + let directPathToChild = []; + if (layout[directLinkComponentId]) { + directPathToChild = (layout[directLinkComponentId].parents || []).slice(); + directPathToChild.push(directLinkComponentId); + } + + const nativeFilters = getInitialNativeFilterState({ + filterConfig: metadata.filter_configuration || [], + filterSetsConfig: metadata.filter_sets_configuration || [], + }); + + const roles = getState().user.roles.Admin; + + dispatch( + setDashboardBootstrapState({ + datasources: datasourcesData, + sliceEntities: { ...initSliceEntities, slices, isLoading: false }, + charts: chartQueries, + // read-only data + dashboardInfo: { + id: dashboardData.id, + slug: dashboardData.slug, + metadata, + userId: 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), + superset_can_explore: getPermissions('can_explore', '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, + }, + lastModifiedTime: dashboardData.last_modified_time, + }, + dashboardFilters, + nativeFilters, + dashboardState: { + sliceIds: Array.from(sliceIds), + directPathToChild, + directPathLastUpdated: Date.now(), + focusedFilterField: null, + expandedSlices: (metadata && metadata.expanded_slices) || {}, + refreshFrequency: (metadata && 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, + }, + dashboardLayout, + messageToasts: [], + impressionId: shortid.generate(), + }), + ); +}; diff --git a/superset-frontend/src/dashboard/components/DashboardRoute.tsx b/superset-frontend/src/dashboard/components/DashboardRoute.tsx index ca1e4decba8e4..b49ec7c4f7a39 100644 --- a/superset-frontend/src/dashboard/components/DashboardRoute.tsx +++ b/superset-frontend/src/dashboard/components/DashboardRoute.tsx @@ -20,14 +20,13 @@ import React, { useEffect, useState, FC } from 'react'; import { connect } from 'react-redux'; import { AnyAction, bindActionCreators, Dispatch } from 'redux'; import { SupersetClient } from '@superset-ui/core'; -import setInitialState from 'src/dashboard/actions/bootstrapData'; import Loading from 'src/components/Loading'; import ErrorBoundary from 'src/components/ErrorBoundary'; -import getInitialState from '../reducers/getInitialState'; +import { bootstrapDashboardState } from '../actions/bootstrapData'; interface DashboardRouteProps { actions: { - setInitialState: (arg0: object) => void; + bootstrapDashboardState: typeof bootstrapDashboardState; }; dashboardIdOrSlug: string; } @@ -55,15 +54,15 @@ const DashboardRoute: FC = ({ const handleError = (error: unknown) => ({ error, info: null }); useEffect(() => { + setLoaded(false); getData(dashboardIdOrSlug) .then(data => { if (data) { - const initState = getInitialState( - bootstrapDataJson, + actions.bootstrapDashboardState( + bootstrapDataJson.datasources, data.chartRes, data.dashboardRes, ); - actions.setInitialState(initState); setLoaded(true); } }) @@ -71,7 +70,7 @@ const DashboardRoute: FC = ({ setLoaded(true); handleError(err); }); - }, [dashboardIdOrSlug]); + }, [dashboardIdOrSlug, actions]); if (!loaded) return ; return {children} ; @@ -81,7 +80,7 @@ function mapDispatchToProps(dispatch: Dispatch) { return { actions: bindActionCreators( { - setInitialState, + bootstrapDashboardState, }, dispatch, ), diff --git a/superset-frontend/src/dashboard/index.jsx b/superset-frontend/src/dashboard/index.jsx index 4eac381c0c738..e0cd889cee6a0 100644 --- a/superset-frontend/src/dashboard/index.jsx +++ b/superset-frontend/src/dashboard/index.jsx @@ -32,6 +32,12 @@ 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 asyncEventMiddleware = initAsyncEvents({ config: bootstrapData.common.conf, getPendingComponents: ({ charts }) => @@ -46,6 +52,7 @@ const asyncEventMiddleware = initAsyncEvents({ const store = createStore( rootReducer, + initialState, compose( applyMiddleware(thunk, logger, asyncEventMiddleware), initEnhancer(false), diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js deleted file mode 100644 index 9c55e1cc898c6..0000000000000 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ /dev/null @@ -1,345 +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. - */ -/* eslint-disable camelcase */ -import { isString } from 'lodash'; -import shortid from 'shortid'; -import { CategoricalColorNamespace } from '@superset-ui/core'; -import querystring from 'query-string'; - -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 './dashboardFilters'; -import { chart } from '../../chart/chartReducer'; -import { - DASHBOARD_HEADER_ID, - GRID_DEFAULT_CHART_WIDTH, - GRID_COLUMN_COUNT, -} from '../util/constants'; -import { - DASHBOARD_HEADER_TYPE, - CHART_TYPE, - ROW_TYPE, -} 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 -const extractUrlParams = queryParams => - Object.entries(queryParams).reduce((acc, [key, value]) => { - if (reservedQueryParams.has(key)) return acc; - if (Array.isArray(value)) { - return { - ...acc, - [key]: value[0], - }; - } - return { ...acc, [key]: value }; - }, {}); - -export default function getInitialState( - bootstrapData, - chartData, - dashboardData, -) { - const { datasources, common, user } = bootstrapData; - const dashboard = { ...bootstrapData.dashboard_data }; - const metadata = JSON.parse(dashboardData.json_metadata); - const queryParams = querystring.parse(window.location.search); - const urlParams = extractUrlParams(queryParams); - const editMode = queryParams.edit === 'true'; - - 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, - ); - } catch (e) { - // - } - - // Priming the color palette with user's label-color mapping provided in - // the dashboard's JSON metadata - if (metadata && 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; - Object.keys(colorMap).forEach(label => { - CategoricalColorNamespace.getScale(scheme, namespace).setColor( - label, - colorMap[label], - ); - }); - } - - // dashboard layout - const positionJson = JSON.parse(dashboardData.position_json); - // new dash: positionJson could be {} or null - const layout = - positionJson && Object.keys(positionJson).length > 0 - ? positionJson - : getEmptyLayout(); - - // create a lookup to sync layout names with slice names - const chartIdToLayoutId = {}; - Object.values(layout).forEach(layoutComponent => { - if (layoutComponent.type === CHART_TYPE) { - chartIdToLayoutId[layoutComponent.meta.chartId] = layoutComponent.id; - } - }); - - // find root level chart container node for newly-added slices - const parentId = findFirstParentContainerId(layout); - const parent = layout[parentId]; - let newSlicesContainer; - let newSlicesContainerWidth = 0; - - const filterScopes = (metadata && metadata.filter_scopes) || {}; - - const chartQueries = {}; - const dashboardFilters = {}; - const slices = {}; - const sliceIds = new Set(); - chartData.forEach(slice => { - const key = slice.slice_id; - const form_data = { - ...slice.form_data, - url_params: { - ...slice.form_data.url_params, - ...urlParams, - }, - }; - chartQueries[key] = { - ...chart, - id: key, - form_data, - formData: applyDefaultFormData(form_data), - }; - - slices[key] = { - slice_id: key, - slice_url: slice.slice_url, - slice_name: slice.slice_name, - form_data: slice.form_data, - viz_type: slice.form_data.viz_type, - datasource: slice.form_data.datasource, - description: slice.description, - description_markeddown: slice.description_markeddown, - owners: slice.owners, - modified: slice.modified, - changed_on: new Date(slice.changed_on).getTime(), - }; - - sliceIds.add(key); - - // if there are newly added slices from explore view, fill slices into 1 or more rows - if (!chartIdToLayoutId[key] && layout[parentId]) { - if ( - newSlicesContainerWidth === 0 || - newSlicesContainerWidth + GRID_DEFAULT_CHART_WIDTH > GRID_COLUMN_COUNT - ) { - newSlicesContainer = newComponentFactory( - ROW_TYPE, - (parent.parents || []).slice(), - ); - layout[newSlicesContainer.id] = newSlicesContainer; - parent.children.push(newSlicesContainer.id); - newSlicesContainerWidth = 0; - } - - const chartHolder = newComponentFactory( - CHART_TYPE, - { - chartId: slice.slice_id, - }, - (newSlicesContainer.parents || []).slice(), - ); - - layout[chartHolder.id] = chartHolder; - newSlicesContainer.children.push(chartHolder.id); - chartIdToLayoutId[chartHolder.meta.chartId] = chartHolder.id; - newSlicesContainerWidth += GRID_DEFAULT_CHART_WIDTH; - } - - // build DashboardFilters for interactive filter features - if ( - slice.form_data.viz_type === 'filter_box' || - slice.form_data.viz_type === 'filter_select' - ) { - const configs = getFilterConfigsFromFormdata(slice.form_data); - let { columns } = configs; - const { labels } = configs; - if (preselectFilters[key]) { - Object.keys(columns).forEach(col => { - if (preselectFilters[key][col]) { - columns = { - ...columns, - [col]: preselectFilters[key][col], - }; - } - }); - } - - const scopesByChartId = Object.keys(columns).reduce((map, column) => { - const scopeSettings = { - ...filterScopes[key], - }; - const { scope, immune } = { - ...DASHBOARD_FILTER_SCOPE_GLOBAL, - ...scopeSettings[column], - }; - - return { - ...map, - [column]: { - scope, - immune, - }, - }; - }, {}); - - const componentId = chartIdToLayoutId[key]; - const directPathToFilter = (layout[componentId].parents || []).slice(); - directPathToFilter.push(componentId); - dashboardFilters[key] = { - ...dashboardFilter, - chartId: key, - componentId, - datasourceId: slice.form_data.datasource, - filterName: slice.slice_name, - directPathToFilter, - columns, - labels, - scopes: scopesByChartId, - isInstantFilter: !!slice.form_data.instant_filtering, - isDateFilter: Object.keys(columns).includes(TIME_RANGE), - }; - } - - // sync layout names with current slice names in case a slice was edited - // in explore since the layout was updated. name updates go through layout for undo/redo - // functionality and python updates slice names based on layout upon dashboard save - const layoutId = chartIdToLayoutId[key]; - if (layoutId && layout[layoutId]) { - layout[layoutId].meta.sliceName = slice.slice_name; - } - }); - buildActiveFilters({ - dashboardFilters, - components: layout, - }); - - // store the header as a layout component so we can undo/redo changes - layout[DASHBOARD_HEADER_ID] = { - id: DASHBOARD_HEADER_ID, - type: DASHBOARD_HEADER_TYPE, - meta: { - text: dashboard.dashboard_title, - }, - }; - - const dashboardLayout = { - past: [], - present: layout, - future: [], - }; - - // find direct link component and path from root - const directLinkComponentId = getLocationHash(); - let directPathToChild = []; - if (layout[directLinkComponentId]) { - directPathToChild = (layout[directLinkComponentId].parents || []).slice(); - directPathToChild.push(directLinkComponentId); - } - - const nativeFilters = getInitialNativeFilterState({ - filterConfig: metadata.filter_configuration || [], - filterSetsConfig: metadata.filter_sets_configuration || [], - }); - - const roles = user.roles.Admin; - - return { - datasources, - sliceEntities: { ...initSliceEntities, slices, isLoading: false }, - charts: chartQueries, - // read-only data - dashboardInfo: { - id: dashboardData.id, - slug: dashboardData.slug, - metadata, - userId: user.id, - dash_edit_perm: getPermissions('can_write', 'Dashboard', roles), - dash_save_perm: getPermissions('can_save_dash', 'Superset', roles), - superset_can_explore: getPermissions('can_explore', 'Superset', roles), - superset_can_csv: getPermissions('can_csv', 'Superset', roles), - slice_can_edit: getPermissions('can_slice', 'Superset', roles), - common: { - flash_messages: common.flash_messages, - conf: common.conf, - }, - lastModifiedTime: dashboard.last_modified_time, - }, - dashboardFilters, - nativeFilters, - dashboardState: { - sliceIds: Array.from(sliceIds), - directPathToChild, - directPathLastUpdated: Date.now(), - focusedFilterField: null, - expandedSlices: (metadata && metadata.expanded_slices) || {}, - refreshFrequency: (metadata && 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, - }, - dashboardLayout, - messageToasts: [], - impressionId: shortid.generate(), - }; -} diff --git a/superset-frontend/src/dashboard/reducers/index.js b/superset-frontend/src/dashboard/reducers/index.js index 481f1675ff4aa..28804a7209069 100644 --- a/superset-frontend/src/dashboard/reducers/index.js +++ b/superset-frontend/src/dashboard/reducers/index.js @@ -32,6 +32,8 @@ 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/charts/schemas.py b/superset/charts/schemas.py index 7703df7fcd433..ebe958710b2a7 100644 --- a/superset/charts/schemas.py +++ b/superset/charts/schemas.py @@ -155,6 +155,7 @@ 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( From 17465ee13e836c8db2df46c9ef7e17b13efab9ab Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Thu, 11 Mar 2021 14:08:27 -0800 Subject: [PATCH 21/59] update permissions --- .../src/dashboard/reducers/getInitialState.js | 3 +-- .../src/dashboard/util/getPermissions.tsx | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index 9c55e1cc898c6..558625b1023b3 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -294,8 +294,7 @@ export default function getInitialState( filterSetsConfig: metadata.filter_sets_configuration || [], }); - const roles = user.roles.Admin; - + const { roles } = user; return { datasources, sliceEntities: { ...initSliceEntities, slices, isLoading: false }, diff --git a/superset-frontend/src/dashboard/util/getPermissions.tsx b/superset-frontend/src/dashboard/util/getPermissions.tsx index 32faffc4670b8..bd3d2b4dd8c29 100644 --- a/superset-frontend/src/dashboard/util/getPermissions.tsx +++ b/superset-frontend/src/dashboard/util/getPermissions.tsx @@ -19,9 +19,19 @@ export default function getPermissions( perm: string, view: string, - roles: Array, + roles: object, ) { - if (!roles.length) return false; + const roleList = Object.entries(roles); + if (roleList.length === 0) return false; + let bool; - return Boolean(roles.find(role => role[0] === perm && role[1] === view)); + roleList.forEach(([role, permissions]) => { + bool = Boolean( + permissions.find( + (permission: Array) => + permission[0] === perm && permission[1] === view, + ), + ); + }); + return bool; } From af5d24a02d1b363838ac89238302b0347558fa0e Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Thu, 11 Mar 2021 14:32:39 -0800 Subject: [PATCH 22/59] add just roles --- superset-frontend/src/dashboard/actions/bootstrapData.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/actions/bootstrapData.js b/superset-frontend/src/dashboard/actions/bootstrapData.js index debdbdacf1810..45d449cfe3d2b 100644 --- a/superset-frontend/src/dashboard/actions/bootstrapData.js +++ b/superset-frontend/src/dashboard/actions/bootstrapData.js @@ -301,7 +301,7 @@ export const bootstrapDashboardState = ( filterSetsConfig: metadata.filter_sets_configuration || [], }); - const roles = getState().user.roles.Admin; + const { roles } = getState().user; dispatch( setDashboardBootstrapState({ From 8691cac3f504e77bd6e09403a1ca51e275068efe Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Thu, 11 Mar 2021 16:19:48 -0800 Subject: [PATCH 23/59] id is supposed to be a string --- superset-frontend/src/dashboard/actions/bootstrapData.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/actions/bootstrapData.js b/superset-frontend/src/dashboard/actions/bootstrapData.js index 45d449cfe3d2b..c55a89351f7b8 100644 --- a/superset-frontend/src/dashboard/actions/bootstrapData.js +++ b/superset-frontend/src/dashboard/actions/bootstrapData.js @@ -313,7 +313,7 @@ export const bootstrapDashboardState = ( id: dashboardData.id, slug: dashboardData.slug, metadata, - userId: user.userId, // legacy, please use state.user instead + 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), superset_can_explore: getPermissions('can_explore', 'Superset', roles), From 601df9b060d576c924db0a70bfa723a9ef2cc4d2 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Thu, 11 Mar 2021 22:10:24 -0800 Subject: [PATCH 24/59] unused vars --- superset/views/core.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/superset/views/core.py b/superset/views/core.py index 663bb6a23ad2e..3e37f328c9873 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1838,10 +1838,6 @@ 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" From eb80636d2c8aed27ad80b08aabcc6404b2be9ff5 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Thu, 11 Mar 2021 22:25:01 -0800 Subject: [PATCH 25/59] get datasources from api --- .../src/dashboard/actions/bootstrapData.js | 4 ++-- .../dashboard/components/DashboardRoute.tsx | 19 +++++++++---------- superset/views/core.py | 1 - 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/superset-frontend/src/dashboard/actions/bootstrapData.js b/superset-frontend/src/dashboard/actions/bootstrapData.js index c55a89351f7b8..d4a2cc7054659 100644 --- a/superset-frontend/src/dashboard/actions/bootstrapData.js +++ b/superset-frontend/src/dashboard/actions/bootstrapData.js @@ -17,7 +17,7 @@ * under the License. */ /* eslint-disable camelcase */ -import { isString } from 'lodash'; +import { isString, keyBy } from 'lodash'; import shortid from 'shortid'; import { CategoricalColorNamespace } from '@superset-ui/core'; import querystring from 'query-string'; @@ -305,7 +305,7 @@ export const bootstrapDashboardState = ( dispatch( setDashboardBootstrapState({ - datasources: datasourcesData, + datasources: keyBy(datasourcesData, 'uid'), sliceEntities: { ...initSliceEntities, slices, isLoading: false }, charts: chartQueries, // read-only data diff --git a/superset-frontend/src/dashboard/components/DashboardRoute.tsx b/superset-frontend/src/dashboard/components/DashboardRoute.tsx index b49ec7c4f7a39..86da5acc2bddc 100644 --- a/superset-frontend/src/dashboard/components/DashboardRoute.tsx +++ b/superset-frontend/src/dashboard/components/DashboardRoute.tsx @@ -32,12 +32,14 @@ interface DashboardRouteProps { } const getData = (idOrSlug: string) => { const batch = [ - SupersetClient.get({ endpoint: `/api/v1/dashboard/${idOrSlug}/charts` }), SupersetClient.get({ endpoint: `/api/v1/dashboard/${idOrSlug}` }), + SupersetClient.get({ endpoint: `/api/v1/dashboard/${idOrSlug}/charts` }), + SupersetClient.get({ endpoint: `/api/v1/dashboard/${idOrSlug}/datasets` }), ]; - return Promise.all(batch).then(([chartRes, dashboardRes]) => ({ - chartRes: chartRes.json.result, - dashboardRes: dashboardRes.json.result, + return Promise.all(batch).then(([dashboardRes, chartRes, datasetRes]) => ({ + dashboard: dashboardRes.json.result, + charts: chartRes.json.result, + datasets: datasetRes.json.result, })); }; @@ -46,9 +48,6 @@ const DashboardRoute: FC = ({ actions, dashboardIdOrSlug, // eventually get from react router }) => { - const appContainer = document.getElementById('app'); - const bootstrapData = appContainer?.getAttribute('data-bootstrap'); - const bootstrapDataJson = JSON.parse(bootstrapData || ''); const [loaded, setLoaded] = useState(false); const handleError = (error: unknown) => ({ error, info: null }); @@ -59,9 +58,9 @@ const DashboardRoute: FC = ({ .then(data => { if (data) { actions.bootstrapDashboardState( - bootstrapDataJson.datasources, - data.chartRes, - data.dashboardRes, + data.datasets, + data.charts, + data.dashboard, ); setLoaded(true); } diff --git a/superset/views/core.py b/superset/views/core.py index 3e37f328c9873..424e700170cef 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1866,7 +1866,6 @@ def dashboard( # pylint: disable=too-many-locals bootstrap_data = { "user": bootstrap_user_data(g.user, include_perms=True), "common": common_bootstrap_payload(), - "datasources": data["datasources"], } if request.args.get("json") == "true": From 7ae7e549d2b2e6f0aaf634d57c16905710784750 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Fri, 12 Mar 2021 09:50:27 -0800 Subject: [PATCH 26/59] make onError optional --- superset-frontend/src/components/ErrorBoundary/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/components/ErrorBoundary/index.jsx b/superset-frontend/src/components/ErrorBoundary/index.jsx index 7bc00758afd98..0a1d0c7c46510 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) { - this.props.onError(error, info); + if (this.props.onError) this.props.onError(error, info); this.setState({ error, info }); } From 19c6d3e84bb96e545a1780f9cbb9202585cc2b3e Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Fri, 12 Mar 2021 10:00:32 -0800 Subject: [PATCH 27/59] use resource hooks, better error boundary --- .../common/hooks/apiResources/dashboards.ts | 33 +++++++ .../src/common/hooks/apiResources/index.ts | 3 +- superset-frontend/src/dashboard/App.jsx | 8 +- .../src/dashboard/actions/bootstrapData.js | 4 +- .../dashboard/components/DashboardRoute.tsx | 95 +++++++++++-------- superset-frontend/src/types/Dashboard.ts | 40 ++++++++ superset-frontend/src/types/Role.ts | 24 +++++ 7 files changed, 160 insertions(+), 47 deletions(-) create mode 100644 superset-frontend/src/common/hooks/apiResources/dashboards.ts create mode 100644 superset-frontend/src/types/Dashboard.ts create mode 100644 superset-frontend/src/types/Role.ts diff --git a/superset-frontend/src/common/hooks/apiResources/dashboards.ts b/superset-frontend/src/common/hooks/apiResources/dashboards.ts new file mode 100644 index 0000000000000..e7a18ae2c68d2 --- /dev/null +++ b/superset-frontend/src/common/hooks/apiResources/dashboards.ts @@ -0,0 +1,33 @@ +/** + * 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 { useApiV1Resource } from './apiResources'; + +export const useDashboard = (idOrSlug: string | number) => + useApiV1Resource(`/api/v1/dashboard/${idOrSlug}`); + +// 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 8befc73735770..5e63920731144 100644 --- a/superset-frontend/src/common/hooks/apiResources/index.ts +++ b/superset-frontend/src/common/hooks/apiResources/index.ts @@ -26,4 +26,5 @@ export { // A central catalog of API Resource hooks. // Add new API hooks here, organized under // different files for different resource types. -export { useChartOwnerNames } from './charts'; +export * from './charts'; +export * from './dashboards'; diff --git a/superset-frontend/src/dashboard/App.jsx b/superset-frontend/src/dashboard/App.jsx index 0c577a99114b2..8cf4513056961 100644 --- a/superset-frontend/src/dashboard/App.jsx +++ b/superset-frontend/src/dashboard/App.jsx @@ -26,7 +26,6 @@ import { DynamicPluginProvider } from 'src/components/DynamicPlugins'; import setupApp from '../setup/setupApp'; import setupPlugins from '../setup/setupPlugins'; import DashboardRoute from './components/DashboardRoute'; -import DashboardContainer from './containers/Dashboard'; import { theme } from '../preamble'; setupApp(); @@ -39,9 +38,10 @@ const App = ({ store }) => { - - - + diff --git a/superset-frontend/src/dashboard/actions/bootstrapData.js b/superset-frontend/src/dashboard/actions/bootstrapData.js index d4a2cc7054659..6b15691e98ffc 100644 --- a/superset-frontend/src/dashboard/actions/bootstrapData.js +++ b/superset-frontend/src/dashboard/actions/bootstrapData.js @@ -75,9 +75,9 @@ export function setDashboardBootstrapState(data) { } export const bootstrapDashboardState = ( - datasourcesData, - chartData, dashboardData, + chartData, + datasourcesData, ) => (dispatch, getState) => { const { user, common } = getState(); const metadata = JSON.parse(dashboardData.json_metadata); diff --git a/superset-frontend/src/dashboard/components/DashboardRoute.tsx b/superset-frontend/src/dashboard/components/DashboardRoute.tsx index 86da5acc2bddc..93ab48aa4486e 100644 --- a/superset-frontend/src/dashboard/components/DashboardRoute.tsx +++ b/superset-frontend/src/dashboard/components/DashboardRoute.tsx @@ -16,13 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useEffect, useState, FC } from 'react'; +import React, { useEffect, FC } from 'react'; import { connect } from 'react-redux'; import { AnyAction, bindActionCreators, Dispatch } from 'redux'; -import { SupersetClient } from '@superset-ui/core'; import Loading from 'src/components/Loading'; import ErrorBoundary from 'src/components/ErrorBoundary'; -import { bootstrapDashboardState } from '../actions/bootstrapData'; +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 { bootstrapDashboardState } from 'src/dashboard/actions/bootstrapData'; +import DashboardContainer from 'src/dashboard/containers/Dashboard'; interface DashboardRouteProps { actions: { @@ -30,49 +37,46 @@ interface DashboardRouteProps { }; dashboardIdOrSlug: string; } -const getData = (idOrSlug: string) => { - const batch = [ - SupersetClient.get({ endpoint: `/api/v1/dashboard/${idOrSlug}` }), - SupersetClient.get({ endpoint: `/api/v1/dashboard/${idOrSlug}/charts` }), - SupersetClient.get({ endpoint: `/api/v1/dashboard/${idOrSlug}/datasets` }), - ]; - return Promise.all(batch).then(([dashboardRes, chartRes, datasetRes]) => ({ - dashboard: dashboardRes.json.result, - charts: chartRes.json.result, - datasets: datasetRes.json.result, - })); -}; -const DashboardRoute: FC = ({ - children, +const DashboardRouteGuts: FC = ({ actions, dashboardIdOrSlug, // eventually get from react router }) => { - const [loaded, setLoaded] = useState(false); - - const handleError = (error: unknown) => ({ error, info: null }); + 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(() => { - setLoaded(false); - getData(dashboardIdOrSlug) - .then(data => { - if (data) { - actions.bootstrapDashboardState( - data.datasets, - data.charts, - data.dashboard, - ); - setLoaded(true); - } - }) - .catch(err => { - setLoaded(true); - handleError(err); - }); - }, [dashboardIdOrSlug, actions]); + if ( + wasLoading && + dashboardResource.status === ResourceStatus.COMPLETE && + chartsResource.status === ResourceStatus.COMPLETE && + datasetsResource.status === ResourceStatus.COMPLETE + ) { + actions.bootstrapDashboardState( + dashboardResource.result, + chartsResource.result, + datasetsResource.result, + ); + } + }, [ + actions, + wasLoading, + dashboardResource, + chartsResource, + datasetsResource, + ]); - if (!loaded) return ; - return {children} ; + if (error) throw error; // caught in error boundary + if (isLoading) return ; + return ; }; function mapDispatchToProps(dispatch: Dispatch) { @@ -86,4 +90,15 @@ function mapDispatchToProps(dispatch: Dispatch) { }; } -export default connect(null, mapDispatchToProps)(DashboardRoute); +const ConnectedDashboardRoute = connect( + null, + mapDispatchToProps, +)(DashboardRouteGuts); + +const DashboardRoute = ({ dashboardIdOrSlug }: DashboardRouteProps) => ( + + + +); + +export default DashboardRoute; diff --git a/superset-frontend/src/types/Dashboard.ts b/superset-frontend/src/types/Dashboard.ts new file mode 100644 index 0000000000000..9608cc1d08b18 --- /dev/null +++ b/superset-frontend/src/types/Dashboard.ts @@ -0,0 +1,40 @@ +/** + * 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 new file mode 100644 index 0000000000000..54f6876f493a6 --- /dev/null +++ b/superset-frontend/src/types/Role.ts @@ -0,0 +1,24 @@ +/** + * 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; From d4cc924a2ead76450052c6c7dfa6895e62cf6c34 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Fri, 12 Mar 2021 18:21:23 -0800 Subject: [PATCH 28/59] add loading state for dashboardroute --- .../src/dashboard/components/DashboardRoute.tsx | 8 +++++--- superset-frontend/src/dashboard/containers/Dashboard.jsx | 5 +++-- .../src/dashboard/reducers/getInitialState.js | 5 +++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/superset-frontend/src/dashboard/components/DashboardRoute.tsx b/superset-frontend/src/dashboard/components/DashboardRoute.tsx index 93ab48aa4486e..89e9d8719ea4d 100644 --- a/superset-frontend/src/dashboard/components/DashboardRoute.tsx +++ b/superset-frontend/src/dashboard/components/DashboardRoute.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useEffect, FC } from 'react'; +import React, { useEffect, useState, FC } from 'react'; import { connect } from 'react-redux'; import { AnyAction, bindActionCreators, Dispatch } from 'redux'; import Loading from 'src/components/Loading'; @@ -42,6 +42,7 @@ const DashboardRouteGuts: FC = ({ actions, dashboardIdOrSlug, // eventually get from react router }) => { + const [isLoaded, setLoaded] = useState(false); const dashboardResource = useDashboard(dashboardIdOrSlug); const chartsResource = useDashboardCharts(dashboardIdOrSlug); const datasetsResource = useDashboardDatasets(dashboardIdOrSlug); @@ -52,7 +53,6 @@ const DashboardRouteGuts: FC = ({ const error = [dashboardResource, chartsResource, datasetsResource].find( resource => resource.status === ResourceStatus.ERROR, )?.error; - useEffect(() => { if ( wasLoading && @@ -65,6 +65,7 @@ const DashboardRouteGuts: FC = ({ chartsResource.result, datasetsResource.result, ); + setLoaded(true); } }, [ actions, @@ -75,7 +76,8 @@ const DashboardRouteGuts: FC = ({ ]); if (error) throw error; // caught in error boundary - if (isLoading) return ; + + if (!isLoaded) return ; return ; }; diff --git a/superset-frontend/src/dashboard/containers/Dashboard.jsx b/superset-frontend/src/dashboard/containers/Dashboard.jsx index e2bf97b8029ee..a880a49a52775 100644 --- a/superset-frontend/src/dashboard/containers/Dashboard.jsx +++ b/superset-frontend/src/dashboard/containers/Dashboard.jsx @@ -44,8 +44,9 @@ function mapStateToProps(state) { } = state; return { - initMessages: dashboardInfo.common.flash_messages, - timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT, + // eslint-disable-next-line camelcase + initMessages: dashboardInfo?.common?.flash_messages, + timeout: dashboardInfo?.common.conf?.SUPERSET_WEBSERVER_TIMEOUT, userId: dashboardInfo.userId, dashboardInfo, dashboardState, diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index 558625b1023b3..d68acc1ded741 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -295,6 +295,7 @@ export default function getInitialState( }); const { roles } = user; + console.log('common', common) return { datasources, sliceEntities: { ...initSliceEntities, slices, isLoading: false }, @@ -311,8 +312,8 @@ export default function getInitialState( superset_can_csv: getPermissions('can_csv', 'Superset', roles), slice_can_edit: getPermissions('can_slice', 'Superset', roles), common: { - flash_messages: common.flash_messages, - conf: common.conf, + flash_messages: common?.flash_messages, + conf: common?.conf, }, lastModifiedTime: dashboard.last_modified_time, }, From 69fba2865c7bb10b13ae3118c4ba52aa999b276c Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Fri, 12 Mar 2021 18:35:20 -0800 Subject: [PATCH 29/59] remove console --- superset-frontend/src/dashboard/reducers/getInitialState.js | 1 - 1 file changed, 1 deletion(-) diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index d68acc1ded741..2cf379064efc8 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -295,7 +295,6 @@ export default function getInitialState( }); const { roles } = user; - console.log('common', common) return { datasources, sliceEntities: { ...initSliceEntities, slices, isLoading: false }, From a5f0337cf98c89137dd2d671b127aab72aef11e3 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Tue, 16 Mar 2021 09:49:06 -0700 Subject: [PATCH 30/59] add conditional --- superset-frontend/src/dashboard/actions/bootstrapData.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/dashboard/actions/bootstrapData.js b/superset-frontend/src/dashboard/actions/bootstrapData.js index 6b15691e98ffc..cfe658327725d 100644 --- a/superset-frontend/src/dashboard/actions/bootstrapData.js +++ b/superset-frontend/src/dashboard/actions/bootstrapData.js @@ -297,8 +297,8 @@ export const bootstrapDashboardState = ( } const nativeFilters = getInitialNativeFilterState({ - filterConfig: metadata.filter_configuration || [], - filterSetsConfig: metadata.filter_sets_configuration || [], + filterConfig: metadata?.filter_configuration || [], + filterSetsConfig: metadata?.filter_sets_configuration || [], }); const { roles } = getState().user; From 25c8ed61b477ce30f87746af4421ce1bf4d5ea99 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Tue, 16 Mar 2021 13:56:01 -0700 Subject: [PATCH 31/59] more conditionals --- superset-frontend/src/dashboard/components/SaveModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/components/SaveModal.tsx b/superset-frontend/src/dashboard/components/SaveModal.tsx index 0bbc327767558..257e7a94d1664 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, From 8528ce34d3a7b4f82c9edd39063262f03a95cced Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 17 Mar 2021 15:14:55 -0700 Subject: [PATCH 32/59] testing out a possible fix for cypress --- .../integration/dashboard/tabs.test.ts | 77 ++++++++++++------- .../components/gridComponents/Chart.jsx | 6 +- 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts index 2ceed512f6e84..482772a129d2f 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts @@ -39,30 +39,30 @@ describe('Dashboard tabs', () => { cy.visit(TABBED_DASHBOARD); - cy.get('#app').then(data => { - const bootstrapData = JSON.parse(data[0].dataset.bootstrap || ''); - const dashboard = bootstrapData.dashboard_data as { slices: Slice[] }; - 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; - interceptChart({ sliceId: filterId, legacy: true }).as('filterRequest'); - interceptChart({ sliceId: treemapId, legacy: true }).as('treemapRequest'); - interceptChart({ sliceId: linechartId, legacy: true }).as( - 'linechartRequest', - ); - interceptChart({ sliceId: boxplotId, legacy: false }).as( - 'boxplotRequest', - ); - }); + // cy.get('#app').then(data => { + // const bootstrapData = JSON.parse(data[0].dataset.bootstrap || ''); + // const dashboard = bootstrapData.dashboard_data as { slices: Slice[] }; + // 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; + // interceptChart({ sliceId: filterId, legacy: true }).as('filterRequest'); + // interceptChart({ sliceId: treemapId, legacy: true }).as('treemapRequest'); + // interceptChart({ sliceId: linechartId, legacy: true }).as( + // 'linechartRequest', + // ); + // interceptChart({ sliceId: boxplotId, legacy: false }).as( + // 'boxplotRequest', + // ); + // }); }); it('should switch active tab on click', () => { @@ -91,13 +91,25 @@ describe('Dashboard tabs', () => { .should('not.have.class', 'ant-tabs-tab-active'); }); - it('should load charts when tab is visible', () => { + it.only('should load charts when tab is visible', () => { // landing in first tab, should see 2 charts - cy.wait('@filterRequest'); + // cy.wait('@filterRequest'); cy.get('[data-test="grid-container"]') - .find('.filter_box') + .find('.chart-slice[data-test-viz-type="filter_box"]') + .then(element => { + const sliceId = parseInt(element.attr('data-test-chart-id') || '', 10); + // interceptChart({ sliceId, legacy: true }).as('filterRequest'); + return cy.wait(interceptChart({ sliceId, legacy: true })); + }) + .should('be.visible'); + cy.get('[data-test="grid-container"]') + .find('.chart-slice[data-test-viz-type="treemap"]') + .then(element => { + const sliceId = parseInt(element.attr('data-test-chart-id') || '', 10); + // interceptChart({ sliceId, legacy: true }).as('filterRequest'); + return cy.wait(interceptChart({ sliceId, legacy: true })); + }) .should('be.visible'); - cy.wait('@treemapRequest'); cy.get('[data-test="grid-container"]') .find('.treemap') .should('be.visible'); @@ -114,7 +126,14 @@ describe('Dashboard tabs', () => { cy.get('@row-level-tabs').last().click(); - cy.wait('@linechartRequest'); + cy.get('[data-test="grid-container"]') + .find('.chart-slice[data-test-viz-type="line"]') + .then(element => { + const sliceId = parseInt(element.attr('data-test-chart-id') || '', 10); + // interceptChart({ sliceId, legacy: true }).as('filterRequest'); + return cy.wait(interceptChart({ sliceId, legacy: true })); + }) + .should('be.visible'); cy.get('[data-test="grid-container"]').find('.line').should('be.visible'); // click top level tab, see 1 more chart diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index cdc8ba9535f35..6f29eb692e2c0 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -288,7 +288,11 @@ export default class Chart extends React.Component { }) : {}; return ( -
+
Date: Fri, 26 Mar 2021 14:44:48 -0700 Subject: [PATCH 33/59] convert edit/standalone test to cypress --- .../cypress/integration/dashboard/load.test.ts | 10 ++++++++-- tests/dashboard_tests.py | 14 -------------- 2 files changed, 8 insertions(+), 16 deletions(-) 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 b03cdd27965f2..3627b08c17b9a 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.ts @@ -23,12 +23,18 @@ import { } from './dashboard.helper'; describe('Dashboard load', () => { - before(() => { + beforeEach(() => { 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/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/tests/dashboard_tests.py b/tests/dashboard_tests.py index f24340095b1a0..aa708752b1b89 100644 --- a/tests/dashboard_tests.py +++ b/tests/dashboard_tests.py @@ -132,20 +132,6 @@ def test_new_dashboard(self): 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) From 291fcd3c9ba17314f5d66b1784b0a083ab343944 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Fri, 26 Mar 2021 15:50:19 -0700 Subject: [PATCH 34/59] remove bootstrappy assertions --- tests/dashboard_tests.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/dashboard_tests.py b/tests/dashboard_tests.py index aa708752b1b89..97d9e7b5440aa 100644 --- a/tests/dashboard_tests.py +++ b/tests/dashboard_tests.py @@ -128,7 +128,6 @@ 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) @@ -176,9 +175,6 @@ 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) @@ -394,8 +390,6 @@ 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) From 9dcfc00964acc75d5693f3e4e3e4e4a03903cb4e Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Fri, 26 Mar 2021 16:51:40 -0700 Subject: [PATCH 35/59] lint --- superset/dashboards/dao.py | 1 - superset/views/core.py | 6 ------ 2 files changed, 7 deletions(-) diff --git a/superset/dashboards/dao.py b/superset/dashboards/dao.py index 4c091725956d3..800ee66c9070b 100644 --- a/superset/dashboards/dao.py +++ b/superset/dashboards/dao.py @@ -19,7 +19,6 @@ from typing import Any, Dict, List, Optional from flask_appbuilder.models.sqla.interface import SQLAInterface -from sqlalchemy import or_ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import contains_eager diff --git a/superset/views/core.py b/superset/views/core.py index 6224422b44449..0080bd749d9e4 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1863,12 +1863,6 @@ def dashboard( # pylint: disable=too-many-locals 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), "common": common_bootstrap_payload(), From 1fdfe126491f2bfc2cc7c3f8a20f3856977f4c9f Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Mon, 29 Mar 2021 16:07:57 -0700 Subject: [PATCH 36/59] fix dashboard edit history issue --- .../src/dashboard/reducers/undoableDashboardLayout.js | 1 + 1 file changed, 1 insertion(+) diff --git a/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js b/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js index 30e39735f63d1..d39f848ca724a 100644 --- a/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js +++ b/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js @@ -37,6 +37,7 @@ 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([ LOAD_DASHBOARD_BOOTSTRAPDATA, UPDATE_COMPONENTS, From 21e7848ff46a59851f1c6e3b4e5244a1d1216cb3 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Mon, 29 Mar 2021 17:43:15 -0700 Subject: [PATCH 37/59] rename stuff --- superset-frontend/src/chart/chartReducer.ts | 4 +-- superset-frontend/src/dashboard/App.jsx | 2 +- .../actions/{bootstrapData.js => hydrate.js} | 33 +++++++++---------- .../src/dashboard/actions/nativeFilters.ts | 4 +-- .../{DashboardRoute.tsx => DashboardPage.tsx} | 23 +++++++------ .../dashboard/reducers/dashboardFilters.js | 4 +-- .../src/dashboard/reducers/dashboardInfo.js | 4 +-- .../src/dashboard/reducers/dashboardLayout.js | 4 +-- .../src/dashboard/reducers/dashboardState.js | 4 +-- .../src/dashboard/reducers/datasources.js | 4 +-- .../src/dashboard/reducers/nativeFilters.ts | 4 +-- .../src/dashboard/reducers/sliceEntities.js | 4 +-- .../reducers/undoableDashboardLayout.js | 4 +-- 13 files changed, 47 insertions(+), 51 deletions(-) rename superset-frontend/src/dashboard/actions/{bootstrapData.js => hydrate.js} (96%) rename superset-frontend/src/dashboard/components/{DashboardRoute.tsx => DashboardPage.tsx} (84%) diff --git a/superset-frontend/src/chart/chartReducer.ts b/superset-frontend/src/chart/chartReducer.ts index 8e93488dfd895..4acd1a47667d7 100644 --- a/superset-frontend/src/chart/chartReducer.ts +++ b/superset-frontend/src/chart/chartReducer.ts @@ -22,7 +22,7 @@ import { ChartState } from 'src/explore/types'; import { getFormDataFromControls } from 'src/explore/controlUtils'; import { now } from '../modules/dates'; import * as actions from './chartAction'; -import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../dashboard/actions/bootstrapData'; +import { HYDRATE_DASHBOARD } from '../dashboard/actions/hydrate'; export const chart: ChartState = { id: 0, @@ -193,7 +193,7 @@ export default function chartReducer( delete charts[key]; return charts; } - if (action.type === LOAD_DASHBOARD_BOOTSTRAPDATA) { + if (action.type === HYDRATE_DASHBOARD) { return { ...action.data.charts }; } if (action.type in actionHandlers) { diff --git a/superset-frontend/src/dashboard/App.jsx b/superset-frontend/src/dashboard/App.jsx index 8cf4513056961..8df94d3ff6588 100644 --- a/superset-frontend/src/dashboard/App.jsx +++ b/superset-frontend/src/dashboard/App.jsx @@ -25,7 +25,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import { DynamicPluginProvider } from 'src/components/DynamicPlugins'; import setupApp from '../setup/setupApp'; import setupPlugins from '../setup/setupPlugins'; -import DashboardRoute from './components/DashboardRoute'; +import DashboardRoute from './components/DashboardPage'; import { theme } from '../preamble'; setupApp(); diff --git a/superset-frontend/src/dashboard/actions/bootstrapData.js b/superset-frontend/src/dashboard/actions/hydrate.js similarity index 96% rename from superset-frontend/src/dashboard/actions/bootstrapData.js rename to superset-frontend/src/dashboard/actions/hydrate.js index cfe658327725d..b0abe40a55181 100644 --- a/superset-frontend/src/dashboard/actions/bootstrapData.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -52,8 +52,11 @@ 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 +/** + * 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; @@ -66,19 +69,12 @@ const extractUrlParams = queryParams => return { ...acc, [key]: value }; }, {}); -export const LOAD_DASHBOARD_BOOTSTRAPDATA = 'LOAD_DASHBOARD_BOOTSTRAPDATA'; -export function setDashboardBootstrapState(data) { - return { - type: LOAD_DASHBOARD_BOOTSTRAPDATA, - data, - }; -} +export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD'; -export const bootstrapDashboardState = ( - dashboardData, - chartData, - datasourcesData, -) => (dispatch, getState) => { +export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( + dispatch, + getState, +) => { const { user, common } = getState(); const metadata = JSON.parse(dashboardData.json_metadata); const queryParams = querystring.parse(window.location.search); @@ -303,8 +299,9 @@ export const bootstrapDashboardState = ( const { roles } = getState().user; - dispatch( - setDashboardBootstrapState({ + return dispatch({ + type: HYDRATE_DASHBOARD, + data: { datasources: keyBy(datasourcesData, 'uid'), sliceEntities: { ...initSliceEntities, slices, isLoading: false }, charts: chartQueries, @@ -350,6 +347,6 @@ export const bootstrapDashboardState = ( dashboardLayout, messageToasts: [], impressionId: shortid.generate(), - }), - ); + }, + }); }; diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts index 8b98c37481e16..2fc5541164da0 100644 --- a/superset-frontend/src/dashboard/actions/nativeFilters.ts +++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts @@ -28,7 +28,7 @@ import { SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE, SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL, } from 'src/dataMask/actions'; -import { LOAD_DASHBOARD_BOOTSTRAPDATA } from './bootstrapData'; +import { HYDRATE_DASHBOARD } from './hydrate'; import { dashboardInfoChanged } from './dashboardInfo'; import { DashboardInfo, FilterSet } from '../reducers/types'; @@ -117,7 +117,7 @@ type BootstrapData = { }; export interface SetBooststapData { - type: typeof LOAD_DASHBOARD_BOOTSTRAPDATA; + type: typeof HYDRATE_DASHBOARD; data: BootstrapData; } diff --git a/superset-frontend/src/dashboard/components/DashboardRoute.tsx b/superset-frontend/src/dashboard/components/DashboardPage.tsx similarity index 84% rename from superset-frontend/src/dashboard/components/DashboardRoute.tsx rename to superset-frontend/src/dashboard/components/DashboardPage.tsx index 89e9d8719ea4d..eeefaf9aa4371 100644 --- a/superset-frontend/src/dashboard/components/DashboardRoute.tsx +++ b/superset-frontend/src/dashboard/components/DashboardPage.tsx @@ -28,17 +28,17 @@ import { } from 'src/common/hooks/apiResources'; import { ResourceStatus } from 'src/common/hooks/apiResources/apiResources'; import { usePrevious } from 'src/common/hooks/usePrevious'; -import { bootstrapDashboardState } from 'src/dashboard/actions/bootstrapData'; +import { hydrateDashboard } from 'src/dashboard/actions/hydrate'; import DashboardContainer from 'src/dashboard/containers/Dashboard'; interface DashboardRouteProps { actions: { - bootstrapDashboardState: typeof bootstrapDashboardState; + hydrateDashboard: typeof hydrateDashboard; }; dashboardIdOrSlug: string; } -const DashboardRouteGuts: FC = ({ +const DashboardPage: FC = ({ actions, dashboardIdOrSlug, // eventually get from react router }) => { @@ -60,7 +60,7 @@ const DashboardRouteGuts: FC = ({ chartsResource.status === ResourceStatus.COMPLETE && datasetsResource.status === ResourceStatus.COMPLETE ) { - actions.bootstrapDashboardState( + actions.hydrateDashboard( dashboardResource.result, chartsResource.result, datasetsResource.result, @@ -85,22 +85,21 @@ function mapDispatchToProps(dispatch: Dispatch) { return { actions: bindActionCreators( { - bootstrapDashboardState, + hydrateDashboard, }, dispatch, ), }; } -const ConnectedDashboardRoute = connect( - null, - mapDispatchToProps, -)(DashboardRouteGuts); +const ConnectedDashboardPage = connect(null, mapDispatchToProps)(DashboardPage); -const DashboardRoute = ({ dashboardIdOrSlug }: DashboardRouteProps) => ( +const DashboardPageWithErrorBoundary = ({ + dashboardIdOrSlug, +}: DashboardRouteProps) => ( - + ); -export default DashboardRoute; +export default DashboardPageWithErrorBoundary; diff --git a/superset-frontend/src/dashboard/reducers/dashboardFilters.js b/superset-frontend/src/dashboard/reducers/dashboardFilters.js index 01c0e27cbb289..d31af825717bd 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardFilters.js +++ b/superset-frontend/src/dashboard/reducers/dashboardFilters.js @@ -25,7 +25,7 @@ import { UPDATE_LAYOUT_COMPONENTS, UPDATE_DASHBOARD_FILTERS_SCOPE, } from '../actions/dashboardFilters'; -import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; +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,7 +162,7 @@ export default function dashboardFiltersReducer(dashboardFilters = {}, action) { return updatedFilters; } - if (action.type === LOAD_DASHBOARD_BOOTSTRAPDATA) { + if (action.type === HYDRATE_DASHBOARD) { return action.data.dashboardFilters; } diff --git a/superset-frontend/src/dashboard/reducers/dashboardInfo.js b/superset-frontend/src/dashboard/reducers/dashboardInfo.js index 87803eb1068a9..490875670f680 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardInfo.js +++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.js @@ -18,7 +18,7 @@ */ import { DASHBOARD_INFO_UPDATED } from '../actions/dashboardInfo'; -import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; +import { HYDRATE_DASHBOARD } from '../actions/hydrate'; export default function dashboardStateReducer(state = {}, action) { switch (action.type) { @@ -29,7 +29,7 @@ export default function dashboardStateReducer(state = {}, action) { // server-side compare last_modified_time in second level lastModifiedTime: Math.round(new Date().getTime() / 1000), }; - case LOAD_DASHBOARD_BOOTSTRAPDATA: + case HYDRATE_DASHBOARD: return { ...state, ...action.data.dashboardInfo, diff --git a/superset-frontend/src/dashboard/reducers/dashboardLayout.js b/superset-frontend/src/dashboard/reducers/dashboardLayout.js index 4972a45fe8814..30ad33c62b6c1 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardLayout.js +++ b/superset-frontend/src/dashboard/reducers/dashboardLayout.js @@ -43,10 +43,10 @@ import { DASHBOARD_TITLE_CHANGED, } from '../actions/dashboardLayout'; -import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; +import { HYDRATE_DASHBOARD } from '../actions/hydrate'; const actionHandlers = { - [LOAD_DASHBOARD_BOOTSTRAPDATA](state, action) { + [HYDRATE_DASHBOARD](state, action) { return { ...action.data.dashboardLayout.present, }; diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js index 482d832d61829..6f162084708f4 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.js @@ -36,11 +36,11 @@ import { SET_FOCUSED_FILTER_FIELD, UNSET_FOCUSED_FILTER_FIELD, } from '../actions/dashboardState'; -import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; +import { HYDRATE_DASHBOARD } from '../actions/hydrate'; export default function dashboardStateReducer(state = {}, action) { const actionHandlers = { - [LOAD_DASHBOARD_BOOTSTRAPDATA]() { + [HYDRATE_DASHBOARD]() { return { ...state, ...action.data.dashboardState }; }, [UPDATE_CSS]() { diff --git a/superset-frontend/src/dashboard/reducers/datasources.js b/superset-frontend/src/dashboard/reducers/datasources.js index adfde757e5d60..616c3c134ffdb 100644 --- a/superset-frontend/src/dashboard/reducers/datasources.js +++ b/superset-frontend/src/dashboard/reducers/datasources.js @@ -17,11 +17,11 @@ * under the License. */ import { SET_DATASOURCE } from '../actions/datasources'; -import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; +import { HYDRATE_DASHBOARD } from '../actions/hydrate'; export default function datasourceReducer(datasources = {}, action) { const actionHandlers = { - [LOAD_DASHBOARD_BOOTSTRAPDATA]() { + [HYDRATE_DASHBOARD]() { return action.data.datasources; }, [SET_DATASOURCE]() { diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts index 71a2fb1d75ff8..8b8dc4fd7b593 100644 --- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts +++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts @@ -24,7 +24,7 @@ import { } from 'src/dashboard/actions/nativeFilters'; import { FilterSet, NativeFiltersState } from './types'; import { FilterConfiguration } from '../components/nativeFilters/types'; -import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; +import { HYDRATE_DASHBOARD } from '../actions/hydrate'; export function getInitialState({ filterSetsConfig, @@ -70,7 +70,7 @@ export default function nativeFilterReducer( ) { const { filterSets } = state; switch (action.type) { - case LOAD_DASHBOARD_BOOTSTRAPDATA: + case HYDRATE_DASHBOARD: return { filters: action.data.nativeFilters.filters, }; diff --git a/superset-frontend/src/dashboard/reducers/sliceEntities.js b/superset-frontend/src/dashboard/reducers/sliceEntities.js index d4a0f2132c229..70b66db72475d 100644 --- a/superset-frontend/src/dashboard/reducers/sliceEntities.js +++ b/superset-frontend/src/dashboard/reducers/sliceEntities.js @@ -23,7 +23,7 @@ import { FETCH_ALL_SLICES_STARTED, SET_ALL_SLICES, } from '../actions/sliceEntities'; -import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; +import { HYDRATE_DASHBOARD } from '../actions/hydrate'; export const initSliceEntities = { slices: {}, @@ -37,7 +37,7 @@ export default function sliceEntitiesReducer( action, ) { const actionHandlers = { - [LOAD_DASHBOARD_BOOTSTRAPDATA]() { + [HYDRATE_DASHBOARD]() { return { ...action.data.sliceEntities, }; diff --git a/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js b/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js index d39f848ca724a..2edb51d00fae2 100644 --- a/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js +++ b/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js @@ -29,7 +29,7 @@ import { HANDLE_COMPONENT_DROP, } from '../actions/dashboardLayout'; -import { LOAD_DASHBOARD_BOOTSTRAPDATA } from '../actions/bootstrapData'; +import { HYDRATE_DASHBOARD } from '../actions/hydrate'; import dashboardLayout from './dashboardLayout'; @@ -39,7 +39,7 @@ export default undoable(dashboardLayout, { limit: UNDO_LIMIT + 2, ignoreInitialState: true, filter: includeAction([ - LOAD_DASHBOARD_BOOTSTRAPDATA, + HYDRATE_DASHBOARD, UPDATE_COMPONENTS, DELETE_COMPONENT, CREATE_COMPONENT, From 321ad7ff3aba45fa9d99888094b584fdacad54b2 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 31 Mar 2021 01:36:12 -0700 Subject: [PATCH 38/59] address recent native filters schema change --- .../dashboard/nativeFilters.test.ts | 19 +++++++++----- .../SupersetResourceSelect/index.tsx | 25 +++++++++++-------- .../src/dashboard/actions/hydrate.js | 10 ++++---- 3 files changed, 33 insertions(+), 21 deletions(-) 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 f279b8e0643d8..fbb1aea9d0614 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts @@ -61,9 +61,12 @@ describe('Nativefilters', () => { .click() .type('Country name'); - cy.get('.ant-modal').find('[data-test="datasource-input"]').click(); + cy.get('.ant-modal') + .find('[data-test="datasource-input"]') + .click() + .type('wb_health_population'); - cy.get('[data-test="datasource-input"]') + cy.get('.ant-modal [data-test="datasource-input"] .Select__menu') .contains('wb_health_population') .click(); @@ -155,9 +158,12 @@ describe('Nativefilters', () => { .click() .type('Country name'); - cy.get('.ant-modal').find('[data-test="datasource-input"]').click(); + cy.get('.ant-modal') + .find('[data-test="datasource-input"]') + .click() + .type('wb_health_population'); - cy.get('[data-test="datasource-input"]') + cy.get('.ant-modal [data-test="datasource-input"] .Select__menu') .contains('wb_health_population') .click(); @@ -187,9 +193,10 @@ describe('Nativefilters', () => { cy.get('.ant-modal') .find('[data-test="datasource-input"]') .last() - .click(); + .click() + .type('wb_health_population'); - cy.get('[data-test="datasource-input"]') + cy.get('.ant-modal [data-test="datasource-input"] .Select__menu') .last() .contains('wb_health_population') .click(); diff --git a/superset-frontend/src/components/SupersetResourceSelect/index.tsx b/superset-frontend/src/components/SupersetResourceSelect/index.tsx index 3f69885096454..27daab73ab39f 100644 --- a/superset-frontend/src/components/SupersetResourceSelect/index.tsx +++ b/superset-frontend/src/components/SupersetResourceSelect/index.tsx @@ -93,16 +93,21 @@ export default function SupersetResourceSelect({ : rison.encode({ filter: value }); return cachedSupersetGet({ endpoint: `/api/v1/${resource}/?q=${query}`, - }).then( - response => - response.json.result - .map(transformItem) - .sort((a: Value, b: Value) => a.label.localeCompare(b.label)), - async badResponse => { - onError(await getClientErrorObject(badResponse)); - return []; - }, - ); + }) + .then( + response => + response.json.result + .map(transformItem) + .sort((a: Value, b: Value) => a.label.localeCompare(b.label)), + async badResponse => { + onError(await getClientErrorObject(badResponse)); + return []; + }, + ) + .then(result => { + console.log('datasources', result); + return result; + }); } return ( diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index b0abe40a55181..13b7ca424b825 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -98,7 +98,7 @@ 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 && metadata.label_colors) { + if (metadata?.label_colors) { const scheme = metadata.color_scheme; const namespace = metadata.color_namespace; const colorMap = isString(metadata.label_colors) @@ -134,7 +134,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( let newSlicesContainer; let newSlicesContainerWidth = 0; - const filterScopes = (metadata && metadata.filter_scopes) || {}; + const filterScopes = metadata?.filter_scopes || {}; const chartQueries = {}; const dashboardFilters = {}; @@ -293,7 +293,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( } const nativeFilters = getInitialNativeFilterState({ - filterConfig: metadata?.filter_configuration || [], + filterConfig: metadata?.native_filter_configuration || [], filterSetsConfig: metadata?.filter_sets_configuration || [], }); @@ -330,8 +330,8 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( directPathToChild, directPathLastUpdated: Date.now(), focusedFilterField: null, - expandedSlices: (metadata && metadata.expanded_slices) || {}, - refreshFrequency: (metadata && metadata.refresh_frequency) || 0, + 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, From 5af2efa4790281c0d8e9aa78ed9d4f4eff3a75b3 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 31 Mar 2021 09:38:26 -0700 Subject: [PATCH 39/59] remove unused getInitialState --- .../src/dashboard/reducers/getInitialState.js | 344 ------------------ 1 file changed, 344 deletions(-) delete mode 100644 superset-frontend/src/dashboard/reducers/getInitialState.js diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js deleted file mode 100644 index 1278e49fdc904..0000000000000 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ /dev/null @@ -1,344 +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. - */ -/* eslint-disable camelcase */ -import { isString } from 'lodash'; -import shortid from 'shortid'; -import { CategoricalColorNamespace } from '@superset-ui/core'; -import querystring from 'query-string'; - -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 './dashboardFilters'; -import { chart } from '../../chart/chartReducer'; -import { - DASHBOARD_HEADER_ID, - GRID_DEFAULT_CHART_WIDTH, - GRID_COLUMN_COUNT, -} from '../util/constants'; -import { - DASHBOARD_HEADER_TYPE, - CHART_TYPE, - ROW_TYPE, -} 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 -const extractUrlParams = queryParams => - Object.entries(queryParams).reduce((acc, [key, value]) => { - if (reservedQueryParams.has(key)) return acc; - if (Array.isArray(value)) { - return { - ...acc, - [key]: value[0], - }; - } - return { ...acc, [key]: value }; - }, {}); - -export default function getInitialState( - bootstrapData, - chartData, - dashboardData, -) { - const { datasources, common, user } = bootstrapData; - const dashboard = { ...bootstrapData.dashboard_data }; - const metadata = JSON.parse(dashboardData.json_metadata); - const queryParams = querystring.parse(window.location.search); - const urlParams = extractUrlParams(queryParams); - const editMode = queryParams.edit === 'true'; - - 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, - ); - } catch (e) { - // - } - - // Priming the color palette with user's label-color mapping provided in - // the dashboard's JSON metadata - if (metadata && 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; - Object.keys(colorMap).forEach(label => { - CategoricalColorNamespace.getScale(scheme, namespace).setColor( - label, - colorMap[label], - ); - }); - } - - // dashboard layout - const positionJson = JSON.parse(dashboardData.position_json); - // new dash: positionJson could be {} or null - const layout = - positionJson && Object.keys(positionJson).length > 0 - ? positionJson - : getEmptyLayout(); - - // create a lookup to sync layout names with slice names - const chartIdToLayoutId = {}; - Object.values(layout).forEach(layoutComponent => { - if (layoutComponent.type === CHART_TYPE) { - chartIdToLayoutId[layoutComponent.meta.chartId] = layoutComponent.id; - } - }); - - // find root level chart container node for newly-added slices - const parentId = findFirstParentContainerId(layout); - const parent = layout[parentId]; - let newSlicesContainer; - let newSlicesContainerWidth = 0; - - const filterScopes = (metadata && metadata.filter_scopes) || {}; - - const chartQueries = {}; - const dashboardFilters = {}; - const slices = {}; - const sliceIds = new Set(); - chartData.forEach(slice => { - const key = slice.slice_id; - const form_data = { - ...slice.form_data, - url_params: { - ...slice.form_data.url_params, - ...urlParams, - }, - }; - chartQueries[key] = { - ...chart, - id: key, - form_data, - formData: applyDefaultFormData(form_data), - }; - - slices[key] = { - slice_id: key, - slice_url: slice.slice_url, - slice_name: slice.slice_name, - form_data: slice.form_data, - viz_type: slice.form_data.viz_type, - datasource: slice.form_data.datasource, - description: slice.description, - description_markeddown: slice.description_markeddown, - owners: slice.owners, - modified: slice.modified, - changed_on: new Date(slice.changed_on).getTime(), - }; - - sliceIds.add(key); - - // if there are newly added slices from explore view, fill slices into 1 or more rows - if (!chartIdToLayoutId[key] && layout[parentId]) { - if ( - newSlicesContainerWidth === 0 || - newSlicesContainerWidth + GRID_DEFAULT_CHART_WIDTH > GRID_COLUMN_COUNT - ) { - newSlicesContainer = newComponentFactory( - ROW_TYPE, - (parent.parents || []).slice(), - ); - layout[newSlicesContainer.id] = newSlicesContainer; - parent.children.push(newSlicesContainer.id); - newSlicesContainerWidth = 0; - } - - const chartHolder = newComponentFactory( - CHART_TYPE, - { - chartId: slice.slice_id, - }, - (newSlicesContainer.parents || []).slice(), - ); - - layout[chartHolder.id] = chartHolder; - newSlicesContainer.children.push(chartHolder.id); - chartIdToLayoutId[chartHolder.meta.chartId] = chartHolder.id; - newSlicesContainerWidth += GRID_DEFAULT_CHART_WIDTH; - } - - // build DashboardFilters for interactive filter features - if ( - slice.form_data.viz_type === 'filter_box' || - slice.form_data.viz_type === 'filter_select' - ) { - const configs = getFilterConfigsFromFormdata(slice.form_data); - let { columns } = configs; - const { labels } = configs; - if (preselectFilters[key]) { - Object.keys(columns).forEach(col => { - if (preselectFilters[key][col]) { - columns = { - ...columns, - [col]: preselectFilters[key][col], - }; - } - }); - } - - const scopesByChartId = Object.keys(columns).reduce((map, column) => { - const scopeSettings = { - ...filterScopes[key], - }; - const { scope, immune } = { - ...DASHBOARD_FILTER_SCOPE_GLOBAL, - ...scopeSettings[column], - }; - - return { - ...map, - [column]: { - scope, - immune, - }, - }; - }, {}); - - const componentId = chartIdToLayoutId[key]; - const directPathToFilter = (layout[componentId].parents || []).slice(); - directPathToFilter.push(componentId); - dashboardFilters[key] = { - ...dashboardFilter, - chartId: key, - componentId, - datasourceId: slice.form_data.datasource, - filterName: slice.slice_name, - directPathToFilter, - columns, - labels, - scopes: scopesByChartId, - isInstantFilter: !!slice.form_data.instant_filtering, - isDateFilter: Object.keys(columns).includes(TIME_RANGE), - }; - } - - // sync layout names with current slice names in case a slice was edited - // in explore since the layout was updated. name updates go through layout for undo/redo - // functionality and python updates slice names based on layout upon dashboard save - const layoutId = chartIdToLayoutId[key]; - if (layoutId && layout[layoutId]) { - layout[layoutId].meta.sliceName = slice.slice_name; - } - }); - buildActiveFilters({ - dashboardFilters, - components: layout, - }); - - // store the header as a layout component so we can undo/redo changes - layout[DASHBOARD_HEADER_ID] = { - id: DASHBOARD_HEADER_ID, - type: DASHBOARD_HEADER_TYPE, - meta: { - text: dashboard.dashboard_title, - }, - }; - - const dashboardLayout = { - past: [], - present: layout, - future: [], - }; - - // find direct link component and path from root - const directLinkComponentId = getLocationHash(); - let directPathToChild = []; - if (layout[directLinkComponentId]) { - directPathToChild = (layout[directLinkComponentId].parents || []).slice(); - directPathToChild.push(directLinkComponentId); - } - - const nativeFilters = getInitialNativeFilterState({ - filterConfig: metadata.native_filter_configuration || [], - filterSetsConfig: metadata.filter_sets_configuration || [], - }); - - const { roles } = user; - return { - datasources, - sliceEntities: { ...initSliceEntities, slices, isLoading: false }, - charts: chartQueries, - // read-only data - dashboardInfo: { - id: dashboardData.id, - slug: dashboardData.slug, - metadata, - userId: user.id, - dash_edit_perm: getPermissions('can_write', 'Dashboard', roles), - dash_save_perm: getPermissions('can_save_dash', 'Superset', roles), - superset_can_explore: getPermissions('can_explore', 'Superset', roles), - superset_can_csv: getPermissions('can_csv', 'Superset', roles), - slice_can_edit: getPermissions('can_slice', 'Superset', roles), - common: { - flash_messages: common?.flash_messages, - conf: common?.conf, - }, - lastModifiedTime: dashboard.last_modified_time, - }, - dashboardFilters, - nativeFilters, - dashboardState: { - sliceIds: Array.from(sliceIds), - directPathToChild, - directPathLastUpdated: Date.now(), - focusedFilterField: null, - expandedSlices: (metadata && metadata.expanded_slices) || {}, - refreshFrequency: (metadata && 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, - }, - dashboardLayout, - messageToasts: [], - impressionId: shortid.generate(), - }; -} From fdf9a5bc125542d4be1bc0d41e12a0f7438c73a2 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 31 Mar 2021 10:00:27 -0700 Subject: [PATCH 40/59] remove .only from test --- .../cypress-base/cypress/integration/dashboard/tabs.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts index 2cea30af1a790..d4ec42e0df3cf 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts @@ -70,7 +70,7 @@ describe('Dashboard tabs', () => { .should('not.have.class', 'ant-tabs-tab-active'); }); - it.only('should load charts when tab is visible', () => { + it('should load charts when tab is visible', () => { // landing in first tab, should see 2 charts waitForChartLoad(FILTER_BOX); waitForChartLoad(TREEMAP); From 8e1bf024066753f564f177ac29ef9d13427c986c Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 31 Mar 2021 10:06:01 -0700 Subject: [PATCH 41/59] hooksy redux usage --- .../dashboard/components/DashboardPage.tsx | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/superset-frontend/src/dashboard/components/DashboardPage.tsx b/superset-frontend/src/dashboard/components/DashboardPage.tsx index eeefaf9aa4371..6e391e0b00c31 100644 --- a/superset-frontend/src/dashboard/components/DashboardPage.tsx +++ b/superset-frontend/src/dashboard/components/DashboardPage.tsx @@ -17,8 +17,7 @@ * under the License. */ import React, { useEffect, useState, FC } from 'react'; -import { connect } from 'react-redux'; -import { AnyAction, bindActionCreators, Dispatch } from 'redux'; +import { useDispatch } from 'react-redux'; import Loading from 'src/components/Loading'; import ErrorBoundary from 'src/components/ErrorBoundary'; import { @@ -32,16 +31,13 @@ import { hydrateDashboard } from 'src/dashboard/actions/hydrate'; import DashboardContainer from 'src/dashboard/containers/Dashboard'; interface DashboardRouteProps { - actions: { - hydrateDashboard: typeof hydrateDashboard; - }; dashboardIdOrSlug: string; } const DashboardPage: FC = ({ - actions, dashboardIdOrSlug, // eventually get from react router }) => { + const dispatch = useDispatch(); const [isLoaded, setLoaded] = useState(false); const dashboardResource = useDashboard(dashboardIdOrSlug); const chartsResource = useDashboardCharts(dashboardIdOrSlug); @@ -60,15 +56,17 @@ const DashboardPage: FC = ({ chartsResource.status === ResourceStatus.COMPLETE && datasetsResource.status === ResourceStatus.COMPLETE ) { - actions.hydrateDashboard( - dashboardResource.result, - chartsResource.result, - datasetsResource.result, + dispatch( + hydrateDashboard( + dashboardResource.result, + chartsResource.result, + datasetsResource.result, + ), ); setLoaded(true); } }, [ - actions, + dispatch, wasLoading, dashboardResource, chartsResource, @@ -81,24 +79,11 @@ const DashboardPage: FC = ({ return ; }; -function mapDispatchToProps(dispatch: Dispatch) { - return { - actions: bindActionCreators( - { - hydrateDashboard, - }, - dispatch, - ), - }; -} - -const ConnectedDashboardPage = connect(null, mapDispatchToProps)(DashboardPage); - const DashboardPageWithErrorBoundary = ({ dashboardIdOrSlug, }: DashboardRouteProps) => ( - + ); From 9d250424fac99a92a9ac0aa38d550c6c3a721391 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 31 Mar 2021 10:25:07 -0700 Subject: [PATCH 42/59] Revert "more conditionals" This reverts commit 25c8ed61b477ce30f87746af4421ce1bf4d5ea99. --- superset-frontend/src/dashboard/components/SaveModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/components/SaveModal.tsx b/superset-frontend/src/dashboard/components/SaveModal.tsx index 257e7a94d1664..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, From cc638a3b3dfacbe713aab5426692bb4dc4b0d015 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 31 Mar 2021 10:28:33 -0700 Subject: [PATCH 43/59] cleanup --- .../src/dashboard/actions/hydrate.js | 20 +++++++++---------- .../components/FiltersBadge/selectors.ts | 1 + 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 13b7ca424b825..a3e7fbe336911 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -22,6 +22,7 @@ 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'; @@ -31,24 +32,23 @@ import getPermissions from 'src/dashboard/util/getPermissions'; import { DASHBOARD_FILTER_SCOPE_GLOBAL, dashboardFilter, -} from '../reducers/dashboardFilters'; -import { chart } from '../../chart/chartReducer'; +} from 'src/dashboard/reducers/dashboardFilters'; import { DASHBOARD_HEADER_ID, GRID_DEFAULT_CHART_WIDTH, GRID_COLUMN_COUNT, -} from '../util/constants'; +} from 'src/dashboard/util/constants'; import { DASHBOARD_HEADER_TYPE, CHART_TYPE, ROW_TYPE, -} 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'; +} 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'; const reservedQueryParams = new Set(['standalone', 'edit']); diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts index 9227e216625f6..2e5e7ac4b343a 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts +++ b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts @@ -156,6 +156,7 @@ export const selectIndicatorsForChart = ( // so grab the columns from the applied/rejected filters const appliedColumns = getAppliedColumns(chart); const rejectedColumns = getRejectedColumns(chart); + const indicators = Object.values(filters) .filter(filter => filter.chartId !== chartId) .reduce( From 09dd21beb9afbd821e2f65acda1e44f6b6453414 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 31 Mar 2021 10:42:17 -0700 Subject: [PATCH 44/59] undo unnecessary change --- superset-frontend/src/dashboard/containers/Dashboard.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/superset-frontend/src/dashboard/containers/Dashboard.jsx b/superset-frontend/src/dashboard/containers/Dashboard.jsx index 8d422b671d96d..53acf6ae9501e 100644 --- a/superset-frontend/src/dashboard/containers/Dashboard.jsx +++ b/superset-frontend/src/dashboard/containers/Dashboard.jsx @@ -44,9 +44,8 @@ function mapStateToProps(state) { } = state; return { - // eslint-disable-next-line camelcase - initMessages: dashboardInfo?.common?.flash_messages, - timeout: dashboardInfo?.common.conf?.SUPERSET_WEBSERVER_TIMEOUT, + initMessages: dashboardInfo.common.flash_messages, + timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT, userId: dashboardInfo.userId, dashboardInfo, dashboardState, From 9256762d441824189c9990b2cfa975f89655f2ad Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 31 Mar 2021 12:43:00 -0700 Subject: [PATCH 45/59] actually need conditions here --- superset-frontend/src/dashboard/components/SaveModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/components/SaveModal.tsx b/superset-frontend/src/dashboard/components/SaveModal.tsx index 0bbc327767558..1d3141df05f8e 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, From 77dea1915beee2761c9fc214f1196ef8d79832c8 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 31 Mar 2021 13:03:28 -0700 Subject: [PATCH 46/59] certainty --- .../src/dashboard/actions/hydrate.js | 18 +++++++++--------- .../src/dashboard/components/Header.jsx | 4 ++-- .../src/dashboard/components/SaveModal.tsx | 2 +- .../src/dashboard/containers/Chart.jsx | 2 +- .../src/dashboard/containers/Dashboard.jsx | 2 +- .../src/dashboard/containers/FiltersBadge.tsx | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index a3e7fbe336911..58081f2eccc1c 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -76,7 +76,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( getState, ) => { const { user, common } = getState(); - const metadata = JSON.parse(dashboardData.json_metadata); + const metadata = JSON.parse(dashboardData.json_metadata) || {}; const queryParams = querystring.parse(window.location.search); const urlParams = extractUrlParams(queryParams); const editMode = queryParams.edit === 'true'; @@ -98,7 +98,7 @@ 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) { + if (metadata.label_colors) { const scheme = metadata.color_scheme; const namespace = metadata.color_namespace; const colorMap = isString(metadata.label_colors) @@ -134,7 +134,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( let newSlicesContainer; let newSlicesContainerWidth = 0; - const filterScopes = metadata?.filter_scopes || {}; + const filterScopes = metadata.filter_scopes || {}; const chartQueries = {}; const dashboardFilters = {}; @@ -293,8 +293,8 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( } const nativeFilters = getInitialNativeFilterState({ - filterConfig: metadata?.native_filter_configuration || [], - filterSetsConfig: metadata?.filter_sets_configuration || [], + filterConfig: metadata.native_filter_configuration || [], + filterSetsConfig: metadata.filter_sets_configuration || [], }); const { roles } = getState().user; @@ -330,14 +330,14 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( directPathToChild, directPathLastUpdated: Date.now(), focusedFilterField: null, - expandedSlices: metadata?.expanded_slices || {}, - refreshFrequency: metadata?.refresh_frequency || 0, + 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, + colorNamespace: metadata.color_namespace || null, + colorScheme: metadata.color_scheme || null, editMode: getPermissions('can_write', 'Dashboard', roles) && editMode, isPublished: dashboardData.published, hasUnsavedChanges: false, diff --git a/superset-frontend/src/dashboard/components/Header.jsx b/superset-frontend/src/dashboard/components/Header.jsx index 8ad6c57e69ad1..5e2e494b90251 100644 --- a/superset-frontend/src/dashboard/components/Header.jsx +++ b/superset-frontend/src/dashboard/components/Header.jsx @@ -298,7 +298,7 @@ class Header extends React.PureComponent { let labelColors = colorScheme ? scale.getColorMap() : {}; // but allow metadata to overwrite if it exists // eslint-disable-next-line camelcase - const metadataLabelColors = dashboardInfo.metadata?.label_colors; + const metadataLabelColors = dashboardInfo.metadata.label_colors; if (metadataLabelColors) { labelColors = { ...labelColors, ...metadataLabelColors }; } @@ -306,7 +306,7 @@ class Header extends React.PureComponent { // check refresh frequency is for current session or persist const refreshFrequency = shouldPersistRefreshFrequency ? currentRefreshFrequency - : dashboardInfo.metadata?.refresh_frequency; // eslint-disable-line camelcase + : dashboardInfo.metadata.refresh_frequency; // eslint-disable-line camelcase const data = { positions, 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/Chart.jsx b/superset-frontend/src/dashboard/containers/Chart.jsx index 94f88d3a9ef7f..f87438066859d 100644 --- a/superset-frontend/src/dashboard/containers/Chart.jsx +++ b/superset-frontend/src/dashboard/containers/Chart.jsx @@ -62,7 +62,7 @@ function mapStateToProps( layout: dashboardLayout.present, chart, // eslint-disable-next-line camelcase - chartConfiguration: dashboardInfo.metadata?.chart_configuration, + chartConfiguration: dashboardInfo.metadata.chart_configuration, charts: chartQueries, filters: getAppliedFilterValues(id), colorScheme, diff --git a/superset-frontend/src/dashboard/containers/Dashboard.jsx b/superset-frontend/src/dashboard/containers/Dashboard.jsx index 53acf6ae9501e..0077f5bd6d591 100644 --- a/superset-frontend/src/dashboard/containers/Dashboard.jsx +++ b/superset-frontend/src/dashboard/containers/Dashboard.jsx @@ -60,7 +60,7 @@ function mapStateToProps(state) { ...getActiveFilters(), ...getAllActiveFilters({ // eslint-disable-next-line camelcase - chartConfiguration: dashboardInfo.metadata?.chart_configuration, + chartConfiguration: dashboardInfo.metadata.chart_configuration, nativeFilters: nativeFilters.filters, dataMask, layout: dashboardLayout.present, diff --git a/superset-frontend/src/dashboard/containers/FiltersBadge.tsx b/superset-frontend/src/dashboard/containers/FiltersBadge.tsx index 546b96303f8ac..9ee544ffed9ad 100644 --- a/superset-frontend/src/dashboard/containers/FiltersBadge.tsx +++ b/superset-frontend/src/dashboard/containers/FiltersBadge.tsx @@ -78,7 +78,7 @@ const mapStateToProps = ( chartId, charts, present, - dashboardInfo.metadata?.chart_configuration, + dashboardInfo.metadata.chart_configuration, ); const indicators = uniqWith( From 9fe05777e9f7b5f28f07d87d4f327389f42be831 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 31 Mar 2021 16:08:31 -0700 Subject: [PATCH 47/59] Revert "certainty" This reverts commit 77dea1915beee2761c9fc214f1196ef8d79832c8. --- .../src/dashboard/actions/hydrate.js | 18 +++++++++--------- .../src/dashboard/components/Header.jsx | 4 ++-- .../src/dashboard/components/SaveModal.tsx | 2 +- .../src/dashboard/containers/Chart.jsx | 2 +- .../src/dashboard/containers/Dashboard.jsx | 2 +- .../src/dashboard/containers/FiltersBadge.tsx | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 58081f2eccc1c..a3e7fbe336911 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -76,7 +76,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( getState, ) => { const { user, common } = getState(); - const metadata = JSON.parse(dashboardData.json_metadata) || {}; + const metadata = JSON.parse(dashboardData.json_metadata); const queryParams = querystring.parse(window.location.search); const urlParams = extractUrlParams(queryParams); const editMode = queryParams.edit === 'true'; @@ -98,7 +98,7 @@ 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) { + if (metadata?.label_colors) { const scheme = metadata.color_scheme; const namespace = metadata.color_namespace; const colorMap = isString(metadata.label_colors) @@ -134,7 +134,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( let newSlicesContainer; let newSlicesContainerWidth = 0; - const filterScopes = metadata.filter_scopes || {}; + const filterScopes = metadata?.filter_scopes || {}; const chartQueries = {}; const dashboardFilters = {}; @@ -293,8 +293,8 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( } const nativeFilters = getInitialNativeFilterState({ - filterConfig: metadata.native_filter_configuration || [], - filterSetsConfig: metadata.filter_sets_configuration || [], + filterConfig: metadata?.native_filter_configuration || [], + filterSetsConfig: metadata?.filter_sets_configuration || [], }); const { roles } = getState().user; @@ -330,14 +330,14 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( directPathToChild, directPathLastUpdated: Date.now(), focusedFilterField: null, - expandedSlices: metadata.expanded_slices || {}, - refreshFrequency: metadata.refresh_frequency || 0, + 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, + colorNamespace: metadata?.color_namespace || null, + colorScheme: metadata?.color_scheme || null, editMode: getPermissions('can_write', 'Dashboard', roles) && editMode, isPublished: dashboardData.published, hasUnsavedChanges: false, diff --git a/superset-frontend/src/dashboard/components/Header.jsx b/superset-frontend/src/dashboard/components/Header.jsx index 5e2e494b90251..8ad6c57e69ad1 100644 --- a/superset-frontend/src/dashboard/components/Header.jsx +++ b/superset-frontend/src/dashboard/components/Header.jsx @@ -298,7 +298,7 @@ class Header extends React.PureComponent { let labelColors = colorScheme ? scale.getColorMap() : {}; // but allow metadata to overwrite if it exists // eslint-disable-next-line camelcase - const metadataLabelColors = dashboardInfo.metadata.label_colors; + const metadataLabelColors = dashboardInfo.metadata?.label_colors; if (metadataLabelColors) { labelColors = { ...labelColors, ...metadataLabelColors }; } @@ -306,7 +306,7 @@ class Header extends React.PureComponent { // check refresh frequency is for current session or persist const refreshFrequency = shouldPersistRefreshFrequency ? currentRefreshFrequency - : dashboardInfo.metadata.refresh_frequency; // eslint-disable-line camelcase + : dashboardInfo.metadata?.refresh_frequency; // eslint-disable-line camelcase const data = { positions, diff --git a/superset-frontend/src/dashboard/components/SaveModal.tsx b/superset-frontend/src/dashboard/components/SaveModal.tsx index 0bbc327767558..1d3141df05f8e 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/Chart.jsx b/superset-frontend/src/dashboard/containers/Chart.jsx index f87438066859d..94f88d3a9ef7f 100644 --- a/superset-frontend/src/dashboard/containers/Chart.jsx +++ b/superset-frontend/src/dashboard/containers/Chart.jsx @@ -62,7 +62,7 @@ function mapStateToProps( layout: dashboardLayout.present, chart, // eslint-disable-next-line camelcase - chartConfiguration: dashboardInfo.metadata.chart_configuration, + chartConfiguration: dashboardInfo.metadata?.chart_configuration, charts: chartQueries, filters: getAppliedFilterValues(id), colorScheme, diff --git a/superset-frontend/src/dashboard/containers/Dashboard.jsx b/superset-frontend/src/dashboard/containers/Dashboard.jsx index 0077f5bd6d591..53acf6ae9501e 100644 --- a/superset-frontend/src/dashboard/containers/Dashboard.jsx +++ b/superset-frontend/src/dashboard/containers/Dashboard.jsx @@ -60,7 +60,7 @@ function mapStateToProps(state) { ...getActiveFilters(), ...getAllActiveFilters({ // eslint-disable-next-line camelcase - chartConfiguration: dashboardInfo.metadata.chart_configuration, + chartConfiguration: dashboardInfo.metadata?.chart_configuration, nativeFilters: nativeFilters.filters, dataMask, layout: dashboardLayout.present, diff --git a/superset-frontend/src/dashboard/containers/FiltersBadge.tsx b/superset-frontend/src/dashboard/containers/FiltersBadge.tsx index 9ee544ffed9ad..546b96303f8ac 100644 --- a/superset-frontend/src/dashboard/containers/FiltersBadge.tsx +++ b/superset-frontend/src/dashboard/containers/FiltersBadge.tsx @@ -78,7 +78,7 @@ const mapStateToProps = ( chartId, charts, present, - dashboardInfo.metadata.chart_configuration, + dashboardInfo.metadata?.chart_configuration, ); const indicators = uniqWith( From 52b31118b074d3e2bf9c33e1880788c9da985b31 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Thu, 1 Apr 2021 13:03:30 -0700 Subject: [PATCH 48/59] more permutations (untested yolo) --- .../cypress/integration/dashboard/load.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 3627b08c17b9a..99f5f729f99b2 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.ts @@ -32,6 +32,16 @@ describe('Dashboard load', () => { 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'); From 2e5b81788b6c305c634059ea3a0ef34f5826b29f Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian <1858430+suddjian@users.noreply.github.com> Date: Thu, 1 Apr 2021 13:11:18 -0700 Subject: [PATCH 49/59] Update superset-frontend/src/chart/chartReducer.ts Co-authored-by: Evan Rusackas --- superset-frontend/src/chart/chartReducer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/chart/chartReducer.ts b/superset-frontend/src/chart/chartReducer.ts index 4acd1a47667d7..c3ba0cb70a920 100644 --- a/superset-frontend/src/chart/chartReducer.ts +++ b/superset-frontend/src/chart/chartReducer.ts @@ -22,7 +22,7 @@ import { ChartState } from 'src/explore/types'; import { getFormDataFromControls } from 'src/explore/controlUtils'; import { now } from '../modules/dates'; import * as actions from './chartAction'; -import { HYDRATE_DASHBOARD } from '../dashboard/actions/hydrate'; +import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate'; export const chart: ChartState = { id: 0, From 874cf20bb19331b16512ae61282f68574054cc54 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Thu, 1 Apr 2021 13:12:36 -0700 Subject: [PATCH 50/59] import style --- superset-frontend/src/chart/chartReducer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/chart/chartReducer.ts b/superset-frontend/src/chart/chartReducer.ts index c3ba0cb70a920..d6e42dfa87f60 100644 --- a/superset-frontend/src/chart/chartReducer.ts +++ b/superset-frontend/src/chart/chartReducer.ts @@ -18,11 +18,11 @@ */ /* 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 '../modules/dates'; +import { now } from 'src/modules/dates'; import * as actions from './chartAction'; -import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate'; export const chart: ChartState = { id: 0, From 230bc970545953d60257726eedc7abf949f20391 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Thu, 1 Apr 2021 14:49:38 -0700 Subject: [PATCH 51/59] comment --- superset-frontend/src/dashboard/actions/hydrate.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index a3e7fbe336911..37870e0ddacf6 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -60,6 +60,8 @@ const reservedQueryParams = new Set(['standalone', 'edit']); 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, From 5bf69e6ab003e02e237d856c3ac3b463c8a98116 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Thu, 1 Apr 2021 19:23:15 -0700 Subject: [PATCH 52/59] cleaner dashboardInfo --- .../src/common/hooks/apiResources/dashboards.ts | 12 ++++++++++-- .../src/dashboard/actions/hydrate.js | 15 ++++++--------- .../src/dashboard/containers/DashboardHeader.jsx | 2 +- .../src/dashboard/reducers/dashboardInfo.js | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/superset-frontend/src/common/hooks/apiResources/dashboards.ts b/superset-frontend/src/common/hooks/apiResources/dashboards.ts index e7a18ae2c68d2..0bb21f16bfb60 100644 --- a/superset-frontend/src/common/hooks/apiResources/dashboards.ts +++ b/superset-frontend/src/common/hooks/apiResources/dashboards.ts @@ -17,10 +17,18 @@ * under the License. */ -import { useApiV1Resource } from './apiResources'; +import Dashboard from 'src/types/Dashboard'; +import { useApiV1Resource, useTransformedResource } from './apiResources'; export const useDashboard = (idOrSlug: string | number) => - useApiV1Resource(`/api/v1/dashboard/${idOrSlug}`); + 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) => diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 37870e0ddacf6..7cdaa2396d3ef 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -78,7 +78,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( getState, ) => { const { user, common } = getState(); - const metadata = JSON.parse(dashboardData.json_metadata); + const { metadata } = dashboardData; const queryParams = querystring.parse(window.location.search); const urlParams = extractUrlParams(queryParams); const editMode = queryParams.edit === 'true'; @@ -115,11 +115,11 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( } // dashboard layout - const positionJson = JSON.parse(dashboardData.position_json); - // new dash: positionJson could be {} or null + const { position_data } = dashboardData; + // new dash: position_json could be {} or null const layout = - positionJson && Object.keys(positionJson).length > 0 - ? positionJson + position_data && Object.keys(position_data).length > 0 + ? position_data : getEmptyLayout(); // create a lookup to sync layout names with slice names @@ -309,9 +309,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( charts: chartQueries, // read-only data dashboardInfo: { - id: dashboardData.id, - slug: dashboardData.slug, - metadata, + ...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), @@ -323,7 +321,6 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( flash_messages: common.flash_messages, conf: common.conf, }, - lastModifiedTime: dashboardData.last_modified_time, }, dashboardFilters, nativeFilters, diff --git a/superset-frontend/src/dashboard/containers/DashboardHeader.jsx b/superset-frontend/src/dashboard/containers/DashboardHeader.jsx index 3ffd51a6bbe93..6351561c71fc9 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.lastModifiedTime, + dashboardInfo.last_modified_time, ), editMode: !!dashboardState.editMode, slug: dashboardInfo.slug, diff --git a/superset-frontend/src/dashboard/reducers/dashboardInfo.js b/superset-frontend/src/dashboard/reducers/dashboardInfo.js index 490875670f680..fdd39fae12324 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardInfo.js +++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.js @@ -27,7 +27,7 @@ export default function dashboardStateReducer(state = {}, action) { ...state, ...action.newInfo, // server-side compare last_modified_time in second level - lastModifiedTime: Math.round(new Date().getTime() / 1000), + last_modified_time: Math.round(new Date().getTime() / 1000), }; case HYDRATE_DASHBOARD: return { From a56d3b3174bd27da73dc2c6d8faceb7cae0b91cb Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Fri, 2 Apr 2021 11:37:50 -0700 Subject: [PATCH 53/59] remove debug code --- .../SupersetResourceSelect/index.tsx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/superset-frontend/src/components/SupersetResourceSelect/index.tsx b/superset-frontend/src/components/SupersetResourceSelect/index.tsx index 27daab73ab39f..3f69885096454 100644 --- a/superset-frontend/src/components/SupersetResourceSelect/index.tsx +++ b/superset-frontend/src/components/SupersetResourceSelect/index.tsx @@ -93,21 +93,16 @@ export default function SupersetResourceSelect({ : rison.encode({ filter: value }); return cachedSupersetGet({ endpoint: `/api/v1/${resource}/?q=${query}`, - }) - .then( - response => - response.json.result - .map(transformItem) - .sort((a: Value, b: Value) => a.label.localeCompare(b.label)), - async badResponse => { - onError(await getClientErrorObject(badResponse)); - return []; - }, - ) - .then(result => { - console.log('datasources', result); - return result; - }); + }).then( + response => + response.json.result + .map(transformItem) + .sort((a: Value, b: Value) => a.label.localeCompare(b.label)), + async badResponse => { + onError(await getClientErrorObject(badResponse)); + return []; + }, + ); } return ( From efd189c41eb92b18a242b9b85131b7c71969ab5f Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Mon, 5 Apr 2021 12:40:43 -0700 Subject: [PATCH 54/59] use memo for getPermissions --- .../{getPermissions.tsx => getPermissions.ts} | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) rename superset-frontend/src/dashboard/util/{getPermissions.tsx => getPermissions.ts} (67%) diff --git a/superset-frontend/src/dashboard/util/getPermissions.tsx b/superset-frontend/src/dashboard/util/getPermissions.ts similarity index 67% rename from superset-frontend/src/dashboard/util/getPermissions.tsx rename to superset-frontend/src/dashboard/util/getPermissions.ts index bd3d2b4dd8c29..eb26cf53c1fc3 100644 --- a/superset-frontend/src/dashboard/util/getPermissions.tsx +++ b/superset-frontend/src/dashboard/util/getPermissions.ts @@ -16,22 +16,27 @@ * specific language governing permissions and limitations * under the License. */ +import memoizeOne from 'memoize-one'; + export default function getPermissions( perm: string, view: string, roles: object, ) { - const roleList = Object.entries(roles); - if (roleList.length === 0) return false; - let bool; + return memoizeOne( () => { + 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, - ), - ); + roleList.forEach(([role, permissions]) => { + bool = Boolean( + permissions.find( + (permission: Array) => + permission[0] === perm && permission[1] === view, + ), + ); + }); + console.log('bool', bool) + return bool; }); - return bool; } From 87b3928cb0c1f5b3172fca83248a1314d3f68ec0 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Mon, 5 Apr 2021 14:45:00 -0700 Subject: [PATCH 55/59] fix lint --- superset-frontend/src/dashboard/util/getPermissions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/dashboard/util/getPermissions.ts b/superset-frontend/src/dashboard/util/getPermissions.ts index eb26cf53c1fc3..3e7cb19765ddf 100644 --- a/superset-frontend/src/dashboard/util/getPermissions.ts +++ b/superset-frontend/src/dashboard/util/getPermissions.ts @@ -23,7 +23,7 @@ export default function getPermissions( view: string, roles: object, ) { - return memoizeOne( () => { + return memoizeOne(() => { const roleList = Object.entries(roles); if (roleList.length === 0) return false; let bool; @@ -36,7 +36,7 @@ export default function getPermissions( ), ); }); - console.log('bool', bool) + console.log('bool', bool); return bool; }); } From 2de8a797243c985e77e54baa1478543de3598428 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 7 Apr 2021 14:18:39 -0700 Subject: [PATCH 56/59] adjust name/location of DashboardPage --- superset-frontend/src/dashboard/App.jsx | 4 ++-- .../dashboard/{components => containers}/DashboardPage.tsx | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename superset-frontend/src/dashboard/{components => containers}/DashboardPage.tsx (100%) diff --git a/superset-frontend/src/dashboard/App.jsx b/superset-frontend/src/dashboard/App.jsx index 8df94d3ff6588..43d00f5a579c5 100644 --- a/superset-frontend/src/dashboard/App.jsx +++ b/superset-frontend/src/dashboard/App.jsx @@ -25,7 +25,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import { DynamicPluginProvider } from 'src/components/DynamicPlugins'; import setupApp from '../setup/setupApp'; import setupPlugins from '../setup/setupPlugins'; -import DashboardRoute from './components/DashboardPage'; +import DashboardPage from './containers/DashboardPage'; import { theme } from '../preamble'; setupApp(); @@ -38,7 +38,7 @@ const App = ({ store }) => { - diff --git a/superset-frontend/src/dashboard/components/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx similarity index 100% rename from superset-frontend/src/dashboard/components/DashboardPage.tsx rename to superset-frontend/src/dashboard/containers/DashboardPage.tsx From b6247ff999c1659bb50d91a1fe3982766e93a9a5 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 7 Apr 2021 14:22:29 -0700 Subject: [PATCH 57/59] move logic for REMOVE_SLICE_LEVEL_LABEL_COLORS to DAO --- superset/dashboards/api.py | 8 ++++++++ superset/views/core.py | 7 ------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index d5a79271417a6..458e59c47dc99 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -350,6 +350,14 @@ def get_charts(self, id_or_slug: str) -> Response: try: charts = DashboardDAO.get_charts_for_dashboard(id_or_slug) 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/views/core.py b/superset/views/core.py index 0080bd749d9e4..8fc4d9e76b9aa 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1856,13 +1856,6 @@ 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) - bootstrap_data = { "user": bootstrap_user_data(g.user, include_perms=True), "common": common_bootstrap_payload(), From 483389d05c72aff4d953213fc2ef2ef2e4f292a5 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 7 Apr 2021 14:25:31 -0700 Subject: [PATCH 58/59] stop using full_data() --- superset/views/core.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/superset/views/core.py b/superset/views/core.py index 8fc4d9e76b9aa..9c3de4af5b6a4 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1819,13 +1819,11 @@ def dashboard( # pylint: disable=too-many-locals if not dashboard: abort(404) - data = dashboard.full_data() - if config["ENABLE_ACCESS_REQUEST"]: - for datasource in data["datasources"].values(): + for datasource in dashboard.datasources: 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( From 8f4681625e96cd424343a4a8aa7b84fc7841d5ad Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian Date: Wed, 7 Apr 2021 14:30:49 -0700 Subject: [PATCH 59/59] remove unused (and now useless) json=true query param --- superset/views/core.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/superset/views/core.py b/superset/views/core.py index 9c3de4af5b6a4..c9b1fb3cadf1f 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1859,11 +1859,6 @@ def dashboard( # pylint: disable=too-many-locals "common": common_bootstrap_payload(), } - 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",