Revert "chore(spa refactor): refactoring dashboard to use api's instead of bootstrapdata (#13306)"
This reverts commit 4bb29b6f04c2c23585805bbac7349f2d2c9876bb.
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 99f5f72..b03cdd2 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard/load.test.ts
@@ -23,28 +23,12 @@
} from './dashboard.helper';
describe('Dashboard load', () => {
- beforeEach(() => {
+ before(() => {
cy.login();
+ cy.visit(WORLD_HEALTH_DASHBOARD);
});
it('should load dashboard', () => {
- cy.visit(WORLD_HEALTH_DASHBOARD);
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
});
-
- it('should load in edit mode', () => {
- cy.visit(`${WORLD_HEALTH_DASHBOARD}?edit=true&standalone=true`);
- cy.get('[data-test="discard-changes-button"]').should('be.visible');
- });
-
- it('should load in standalone mode', () => {
- cy.visit(`${WORLD_HEALTH_DASHBOARD}?edit=true&standalone=true`);
- cy.get('#app-menu').should('not.exist');
- });
-
- it('should load in edit/standalone mode', () => {
- cy.visit(`${WORLD_HEALTH_DASHBOARD}?edit=true&standalone=true`);
- cy.get('[data-test="discard-changes-button"]').should('be.visible');
- cy.get('#app-menu').should('not.exist');
- });
});
diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts
index fbb1aea..f279b8e 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts
@@ -61,12 +61,9 @@
.click()
.type('Country name');
- cy.get('.ant-modal')
- .find('[data-test="datasource-input"]')
- .click()
- .type('wb_health_population');
+ cy.get('.ant-modal').find('[data-test="datasource-input"]').click();
- cy.get('.ant-modal [data-test="datasource-input"] .Select__menu')
+ cy.get('[data-test="datasource-input"]')
.contains('wb_health_population')
.click();
@@ -158,12 +155,9 @@
.click()
.type('Country name');
- cy.get('.ant-modal')
- .find('[data-test="datasource-input"]')
- .click()
- .type('wb_health_population');
+ cy.get('.ant-modal').find('[data-test="datasource-input"]').click();
- cy.get('.ant-modal [data-test="datasource-input"] .Select__menu')
+ cy.get('[data-test="datasource-input"]')
.contains('wb_health_population')
.click();
@@ -193,10 +187,9 @@
cy.get('.ant-modal')
.find('[data-test="datasource-input"]')
.last()
- .click()
- .type('wb_health_population');
+ .click();
- cy.get('.ant-modal [data-test="datasource-input"] .Select__menu')
+ cy.get('[data-test="datasource-input"]')
.last()
.contains('wb_health_population')
.click();
diff --git a/superset-frontend/src/chart/chartReducer.ts b/superset-frontend/src/chart/chartReducer.ts
index d6e42df..3f68c04 100644
--- a/superset-frontend/src/chart/chartReducer.ts
+++ b/superset-frontend/src/chart/chartReducer.ts
@@ -18,10 +18,9 @@
*/
/* eslint camelcase: 0 */
import { t } from '@superset-ui/core';
-import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate';
import { ChartState } from 'src/explore/types';
import { getFormDataFromControls } from 'src/explore/controlUtils';
-import { now } from 'src/modules/dates';
+import { now } from '../modules/dates';
import * as actions from './chartAction';
export const chart: ChartState = {
@@ -193,9 +192,7 @@
delete charts[key];
return charts;
}
- if (action.type === HYDRATE_DASHBOARD) {
- return { ...action.data.charts };
- }
+
if (action.type in actionHandlers) {
return {
...charts,
diff --git a/superset-frontend/src/common/hooks/apiResources/dashboards.ts b/superset-frontend/src/common/hooks/apiResources/dashboards.ts
deleted file mode 100644
index 0bb21f1..0000000
--- a/superset-frontend/src/common/hooks/apiResources/dashboards.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import Dashboard from 'src/types/Dashboard';
-import { useApiV1Resource, useTransformedResource } from './apiResources';
-
-export const useDashboard = (idOrSlug: string | number) =>
- useTransformedResource(
- useApiV1Resource<Dashboard>(`/api/v1/dashboard/${idOrSlug}`),
- dashboard => ({
- ...dashboard,
- metadata: JSON.parse(dashboard.json_metadata),
- position_data: JSON.parse(dashboard.position_json),
- }),
- );
-
-// gets the chart definitions for a dashboard
-export const useDashboardCharts = (idOrSlug: string | number) =>
- useApiV1Resource(`/api/v1/dashboard/${idOrSlug}/charts`);
-
-// gets the datasets for a dashboard
-// important: this endpoint only returns the fields in the dataset
-// that are necessary for rendering the given dashboard
-export const useDashboardDatasets = (idOrSlug: string | number) =>
- useApiV1Resource(`/api/v1/dashboard/${idOrSlug}/datasets`);
diff --git a/superset-frontend/src/common/hooks/apiResources/index.ts b/superset-frontend/src/common/hooks/apiResources/index.ts
index 5e63920..8befc73 100644
--- a/superset-frontend/src/common/hooks/apiResources/index.ts
+++ b/superset-frontend/src/common/hooks/apiResources/index.ts
@@ -26,5 +26,4 @@
// A central catalog of API Resource hooks.
// Add new API hooks here, organized under
// different files for different resource types.
-export * from './charts';
-export * from './dashboards';
+export { useChartOwnerNames } from './charts';
diff --git a/superset-frontend/src/components/ErrorBoundary/index.jsx b/superset-frontend/src/components/ErrorBoundary/index.jsx
index 0a1d0c7..7bc0075 100644
--- a/superset-frontend/src/components/ErrorBoundary/index.jsx
+++ b/superset-frontend/src/components/ErrorBoundary/index.jsx
@@ -38,7 +38,7 @@
}
componentDidCatch(error, info) {
- if (this.props.onError) this.props.onError(error, info);
+ this.props.onError(error, info);
this.setState({ error, info });
}
diff --git a/superset-frontend/src/dashboard/App.jsx b/superset-frontend/src/dashboard/App.jsx
index 43d00f5..da06a01 100644
--- a/superset-frontend/src/dashboard/App.jsx
+++ b/superset-frontend/src/dashboard/App.jsx
@@ -25,28 +25,22 @@
import { DynamicPluginProvider } from 'src/components/DynamicPlugins';
import setupApp from '../setup/setupApp';
import setupPlugins from '../setup/setupPlugins';
-import DashboardPage from './containers/DashboardPage';
+import DashboardContainer from './containers/Dashboard';
import { theme } from '../preamble';
setupApp();
setupPlugins();
-const App = ({ store }) => {
- const dashboardIdOrSlug = window.location.pathname.split('/')[3];
- return (
- <Provider store={store}>
- <DndProvider backend={HTML5Backend}>
- <ThemeProvider theme={theme}>
- <DynamicPluginProvider>
- <DashboardPage
- store={store}
- dashboardIdOrSlug={dashboardIdOrSlug}
- />
- </DynamicPluginProvider>
- </ThemeProvider>
- </DndProvider>
- </Provider>
- );
-};
+const App = ({ store }) => (
+ <Provider store={store}>
+ <DndProvider backend={HTML5Backend}>
+ <ThemeProvider theme={theme}>
+ <DynamicPluginProvider>
+ <DashboardContainer />
+ </DynamicPluginProvider>
+ </ThemeProvider>
+ </DndProvider>
+ </Provider>
+);
export default hot(App);
diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js
deleted file mode 100644
index 8065bf6..0000000
--- a/superset-frontend/src/dashboard/actions/hydrate.js
+++ /dev/null
@@ -1,361 +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, keyBy } from 'lodash';
-import shortid from 'shortid';
-import { CategoricalColorNamespace } from '@superset-ui/core';
-import querystring from 'query-string';
-
-import { chart } from 'src/chart/chartReducer';
-import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities';
-import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters';
-import { getParam } from 'src/modules/utils';
-import { applyDefaultFormData } from 'src/explore/store';
-import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
-import getPermissions from 'src/dashboard/util/getPermissions';
-import {
- DASHBOARD_FILTER_SCOPE_GLOBAL,
- dashboardFilter,
-} from 'src/dashboard/reducers/dashboardFilters';
-import {
- DASHBOARD_HEADER_ID,
- GRID_DEFAULT_CHART_WIDTH,
- GRID_COLUMN_COUNT,
-} from 'src/dashboard/util/constants';
-import {
- DASHBOARD_HEADER_TYPE,
- CHART_TYPE,
- ROW_TYPE,
-} from 'src/dashboard/util/componentTypes';
-import findFirstParentContainerId from 'src/dashboard/util/findFirstParentContainer';
-import getEmptyLayout from 'src/dashboard/util/getEmptyLayout';
-import getFilterConfigsFromFormdata from 'src/dashboard/util/getFilterConfigsFromFormdata';
-import getLocationHash from 'src/dashboard/util/getLocationHash';
-import newComponentFactory from 'src/dashboard/util/newComponentFactory';
-import { TIME_RANGE } from 'src/visualizations/FilterBox/FilterBox';
-
-const reservedQueryParams = new Set(['standalone', 'edit']);
-
-/**
- * Returns the url params that are used to customize queries
- * in datasets built using sql lab.
- * We may want to extract this to some kind of util in the future.
- */
-const extractUrlParams = queryParams =>
- Object.entries(queryParams).reduce((acc, [key, value]) => {
- if (reservedQueryParams.has(key)) return acc;
- // if multiple url params share the same key (?foo=bar&foo=baz), they will appear as an array.
- // Only one value can be used for a given query param, so we just take the first one.
- if (Array.isArray(value)) {
- return {
- ...acc,
- [key]: value[0],
- };
- }
- return { ...acc, [key]: value };
- }, {});
-
-export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';
-
-export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
- dispatch,
- getState,
-) => {
- const { user, common } = getState();
- const { metadata } = dashboardData;
- const queryParams = querystring.parse(window.location.search);
- const urlParams = extractUrlParams(queryParams);
- const editMode = queryParams.edit === 'true';
-
- 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?.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 { position_data } = dashboardData;
- // new dash: position_json could be {} or null
- const layout =
- position_data && Object.keys(position_data).length > 0
- ? position_data
- : 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?.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?.native_filter_configuration || [],
- filterSetsConfig: metadata?.filter_sets_configuration || [],
- });
-
- const { roles } = getState().user;
-
- return dispatch({
- type: HYDRATE_DASHBOARD,
- data: {
- datasources: keyBy(datasourcesData, 'uid'),
- sliceEntities: { ...initSliceEntities, slices, isLoading: false },
- charts: chartQueries,
- // read-only data
- dashboardInfo: {
- ...dashboardData,
- userId: String(user.userId), // legacy, please use state.user instead
- dash_edit_perm: getPermissions('can_write', 'Dashboard', roles),
- dash_save_perm: getPermissions('can_save_dash', 'Superset', roles),
- dash_share_perm: getPermissions(
- 'can_share_dashboard',
- 'Superset',
- roles,
- ),
- superset_can_explore: getPermissions('can_explore', 'Superset', roles),
- superset_can_share: getPermissions(
- 'can_share_chart',
- 'Superset',
- roles,
- ),
- superset_can_csv: getPermissions('can_csv', 'Superset', roles),
- slice_can_edit: getPermissions('can_slice', 'Superset', roles),
- common: {
- // legacy, please use state.common instead
- flash_messages: common.flash_messages,
- conf: common.conf,
- },
- },
- dashboardFilters,
- nativeFilters,
- dashboardState: {
- sliceIds: Array.from(sliceIds),
- directPathToChild,
- directPathLastUpdated: Date.now(),
- focusedFilterField: null,
- expandedSlices: metadata?.expanded_slices || {},
- refreshFrequency: metadata?.refresh_frequency || 0,
- // dashboard viewers can set refresh frequency for the current visit,
- // only persistent refreshFrequency will be saved to backend
- shouldPersistRefreshFrequency: false,
- css: dashboardData.css || '',
- colorNamespace: metadata?.color_namespace || null,
- colorScheme: metadata?.color_scheme || null,
- editMode: getPermissions('can_write', 'Dashboard', roles) && editMode,
- isPublished: dashboardData.published,
- hasUnsavedChanges: false,
- maxUndoHistoryExceeded: false,
- lastModifiedTime: dashboardData.changed_on,
- },
- dashboardLayout,
- messageToasts: [],
- impressionId: shortid.generate(),
- },
- });
-};
diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts
index 2fc5541..fca59f2 100644
--- a/superset-frontend/src/dashboard/actions/nativeFilters.ts
+++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts
@@ -19,16 +19,12 @@
import { makeApi } from '@superset-ui/core';
import { Dispatch } from 'redux';
-import {
- Filter,
- FilterConfiguration,
-} from 'src/dashboard/components/nativeFilters/types';
+import { FilterConfiguration } from 'src/dashboard/components/nativeFilters/types';
import { DataMaskType, DataMaskStateWithId } from 'src/dataMask/types';
import {
SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE,
SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL,
} from 'src/dataMask/actions';
-import { HYDRATE_DASHBOARD } from './hydrate';
import { dashboardInfoChanged } from './dashboardInfo';
import { DashboardInfo, FilterSet } from '../reducers/types';
@@ -109,18 +105,6 @@
}
};
-type BootstrapData = {
- nativeFilters: {
- filters: Filter;
- filtersState: object;
- };
-};
-
-export interface SetBooststapData {
- type: typeof HYDRATE_DASHBOARD;
- data: BootstrapData;
-}
-
export const setFilterSetsConfiguration = (
filterSetsConfig: FilterSet[],
) => async (dispatch: Dispatch, getState: () => any) => {
@@ -189,5 +173,4 @@
| SetFilterSetsConfigBegin
| SetFilterSetsConfigComplete
| SetFilterSetsConfigFail
- | SaveFilterSets
- | SetBooststapData;
+ | SaveFilterSets;
diff --git a/superset-frontend/src/dashboard/components/DashboardGrid.jsx b/superset-frontend/src/dashboard/components/DashboardGrid.jsx
index 9fb0fb5..6889c91 100644
--- a/superset-frontend/src/dashboard/components/DashboardGrid.jsx
+++ b/superset-frontend/src/dashboard/components/DashboardGrid.jsx
@@ -123,6 +123,7 @@
width,
isComponentVisible,
} = this.props;
+
const columnPlusGutterWidth =
(width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
diff --git a/superset-frontend/src/dashboard/components/SaveModal.tsx b/superset-frontend/src/dashboard/components/SaveModal.tsx
index 1d3141d..0bbc327 100644
--- a/superset-frontend/src/dashboard/components/SaveModal.tsx
+++ b/superset-frontend/src/dashboard/components/SaveModal.tsx
@@ -140,7 +140,7 @@
// check refresh frequency is for current session or persist
const refreshFrequency = shouldPersistRefreshFrequency
? currentRefreshFrequency
- : dashboardInfo.metadata?.refresh_frequency; // eslint-disable camelcase
+ : dashboardInfo.metadata.refresh_frequency; // eslint-disable camelcase
const data = {
positions,
diff --git a/superset-frontend/src/dashboard/containers/DashboardHeader.jsx b/superset-frontend/src/dashboard/containers/DashboardHeader.jsx
index 6351561..3ffd51a 100644
--- a/superset-frontend/src/dashboard/containers/DashboardHeader.jsx
+++ b/superset-frontend/src/dashboard/containers/DashboardHeader.jsx
@@ -85,7 +85,7 @@
maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded,
lastModifiedTime: Math.max(
dashboardState.lastModifiedTime,
- dashboardInfo.last_modified_time,
+ dashboardInfo.lastModifiedTime,
),
editMode: !!dashboardState.editMode,
slug: dashboardInfo.slug,
diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx
deleted file mode 100644
index 6e391e0..0000000
--- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React, { useEffect, useState, FC } from 'react';
-import { useDispatch } from 'react-redux';
-import Loading from 'src/components/Loading';
-import ErrorBoundary from 'src/components/ErrorBoundary';
-import {
- useDashboard,
- useDashboardCharts,
- useDashboardDatasets,
-} from 'src/common/hooks/apiResources';
-import { ResourceStatus } from 'src/common/hooks/apiResources/apiResources';
-import { usePrevious } from 'src/common/hooks/usePrevious';
-import { hydrateDashboard } from 'src/dashboard/actions/hydrate';
-import DashboardContainer from 'src/dashboard/containers/Dashboard';
-
-interface DashboardRouteProps {
- dashboardIdOrSlug: string;
-}
-
-const DashboardPage: FC<DashboardRouteProps> = ({
- dashboardIdOrSlug, // eventually get from react router
-}) => {
- const dispatch = useDispatch();
- const [isLoaded, setLoaded] = useState(false);
- const dashboardResource = useDashboard(dashboardIdOrSlug);
- const chartsResource = useDashboardCharts(dashboardIdOrSlug);
- const datasetsResource = useDashboardDatasets(dashboardIdOrSlug);
- const isLoading = [dashboardResource, chartsResource, datasetsResource].some(
- resource => resource.status === ResourceStatus.LOADING,
- );
- const wasLoading = usePrevious(isLoading);
- const error = [dashboardResource, chartsResource, datasetsResource].find(
- resource => resource.status === ResourceStatus.ERROR,
- )?.error;
- useEffect(() => {
- if (
- wasLoading &&
- dashboardResource.status === ResourceStatus.COMPLETE &&
- chartsResource.status === ResourceStatus.COMPLETE &&
- datasetsResource.status === ResourceStatus.COMPLETE
- ) {
- dispatch(
- hydrateDashboard(
- dashboardResource.result,
- chartsResource.result,
- datasetsResource.result,
- ),
- );
- setLoaded(true);
- }
- }, [
- dispatch,
- wasLoading,
- dashboardResource,
- chartsResource,
- datasetsResource,
- ]);
-
- if (error) throw error; // caught in error boundary
-
- if (!isLoaded) return <Loading />;
- return <DashboardContainer />;
-};
-
-const DashboardPageWithErrorBoundary = ({
- dashboardIdOrSlug,
-}: DashboardRouteProps) => (
- <ErrorBoundary>
- <DashboardPage dashboardIdOrSlug={dashboardIdOrSlug} />
- </ErrorBoundary>
-);
-
-export default DashboardPageWithErrorBoundary;
diff --git a/superset-frontend/src/dashboard/index.jsx b/superset-frontend/src/dashboard/index.jsx
index 1a287c0..5d696bd 100644
--- a/superset-frontend/src/dashboard/index.jsx
+++ b/superset-frontend/src/dashboard/index.jsx
@@ -22,6 +22,7 @@
import { createStore, applyMiddleware, compose } from 'redux';
import { initFeatureFlags } from 'src/featureFlags';
import { initEnhancer } from '../reduxUtils';
+import getInitialState from './reducers/getInitialState';
import rootReducer from './reducers/index';
import logger from '../middleware/loggerMiddleware';
import App from './App';
@@ -29,16 +30,10 @@
const appContainer = document.getElementById('app');
const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
initFeatureFlags(bootstrapData.common.feature_flags);
-
-const initialState = {
- user: bootstrapData.user,
- common: bootstrapData.common,
- datasources: bootstrapData.datasources,
-};
-
+const initState = getInitialState(bootstrapData);
const store = createStore(
rootReducer,
- initialState,
+ initState,
compose(applyMiddleware(thunk, logger), initEnhancer(false)),
);
diff --git a/superset-frontend/src/dashboard/reducers/dashboardFilters.js b/superset-frontend/src/dashboard/reducers/dashboardFilters.js
index d31af82..f508c1b 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardFilters.js
+++ b/superset-frontend/src/dashboard/reducers/dashboardFilters.js
@@ -25,7 +25,6 @@
UPDATE_LAYOUT_COMPONENTS,
UPDATE_DASHBOARD_FILTERS_SCOPE,
} from '../actions/dashboardFilters';
-import { HYDRATE_DASHBOARD } from '../actions/hydrate';
import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox';
import { DASHBOARD_ROOT_ID } from '../util/constants';
import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata';
@@ -162,10 +161,6 @@
return updatedFilters;
}
- if (action.type === HYDRATE_DASHBOARD) {
- return action.data.dashboardFilters;
- }
-
if (action.type in actionHandlers) {
const updatedFilters = {
...dashboardFilters,
@@ -173,6 +168,7 @@
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 fdd39fa..01346d7 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardInfo.js
+++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.js
@@ -18,7 +18,6 @@
*/
import { DASHBOARD_INFO_UPDATED } from '../actions/dashboardInfo';
-import { HYDRATE_DASHBOARD } from '../actions/hydrate';
export default function dashboardStateReducer(state = {}, action) {
switch (action.type) {
@@ -27,13 +26,7 @@
...state,
...action.newInfo,
// server-side compare last_modified_time in second level
- last_modified_time: Math.round(new Date().getTime() / 1000),
- };
- case HYDRATE_DASHBOARD:
- return {
- ...state,
- ...action.data.dashboardInfo,
- // set async api call data
+ lastModifiedTime: Math.round(new Date().getTime() / 1000),
};
default:
return state;
diff --git a/superset-frontend/src/dashboard/reducers/dashboardLayout.js b/superset-frontend/src/dashboard/reducers/dashboardLayout.js
index 30ad33c..ffc5613 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardLayout.js
+++ b/superset-frontend/src/dashboard/reducers/dashboardLayout.js
@@ -43,15 +43,7 @@
DASHBOARD_TITLE_CHANGED,
} from '../actions/dashboardLayout';
-import { HYDRATE_DASHBOARD } from '../actions/hydrate';
-
const actionHandlers = {
- [HYDRATE_DASHBOARD](state, action) {
- return {
- ...action.data.dashboardLayout.present,
- };
- },
-
[UPDATE_COMPONENTS](state, action) {
const {
payload: { nextComponents },
diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js
index 6f16208..b948e2c 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardState.js
+++ b/superset-frontend/src/dashboard/reducers/dashboardState.js
@@ -36,13 +36,9 @@
SET_FOCUSED_FILTER_FIELD,
UNSET_FOCUSED_FILTER_FIELD,
} from '../actions/dashboardState';
-import { HYDRATE_DASHBOARD } from '../actions/hydrate';
export default function dashboardStateReducer(state = {}, action) {
const actionHandlers = {
- [HYDRATE_DASHBOARD]() {
- return { ...state, ...action.data.dashboardState };
- },
[UPDATE_CSS]() {
return { ...state, css: action.css };
},
diff --git a/superset-frontend/src/dashboard/reducers/datasources.js b/superset-frontend/src/dashboard/reducers/datasources.js
index 616c3c1..0cf7e1b 100644
--- a/superset-frontend/src/dashboard/reducers/datasources.js
+++ b/superset-frontend/src/dashboard/reducers/datasources.js
@@ -17,29 +17,22 @@
* under the License.
*/
import { SET_DATASOURCE } from '../actions/datasources';
-import { HYDRATE_DASHBOARD } from '../actions/hydrate';
export default function datasourceReducer(datasources = {}, action) {
const actionHandlers = {
- [HYDRATE_DASHBOARD]() {
- return action.data.datasources;
- },
[SET_DATASOURCE]() {
return action.datasource;
},
};
if (action.type in actionHandlers) {
- if (action.key) {
- return {
- ...datasources,
- [action.key]: actionHandlers[action.type](
- datasources[action.key],
- action,
- ),
- };
- }
- return actionHandlers[action.type]();
+ return {
+ ...datasources,
+ [action.key]: actionHandlers[action.type](
+ datasources[action.key],
+ action,
+ ),
+ };
}
return datasources;
}
diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js
new file mode 100644
index 0000000..19ea54e
--- /dev/null
+++ b/superset-frontend/src/dashboard/reducers/getInitialState.js
@@ -0,0 +1,312 @@
+/**
+ * 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 { 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 {
+ 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';
+
+export default function getInitialState(bootstrapData) {
+ const { user_id, datasources, common, editMode, urlParams } = bootstrapData;
+
+ const dashboard = { ...bootstrapData.dashboard_data };
+ let preselectFilters = {};
+ try {
+ // allow request parameter overwrite dashboard metadata
+ preselectFilters = JSON.parse(
+ getParam('preselect_filters') || dashboard.metadata.default_filters,
+ );
+ } catch (e) {
+ //
+ }
+
+ // 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;
+ Object.keys(colorMap).forEach(label => {
+ CategoricalColorNamespace.getScale(scheme, namespace).setColor(
+ label,
+ colorMap[label],
+ );
+ });
+ }
+
+ // dashboard layout
+ const { position_json: positionJson } = dashboard;
+ // 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 = dashboard.metadata.filter_scopes || {};
+
+ const chartQueries = {};
+ const dashboardFilters = {};
+ const slices = {};
+ const sliceIds = new Set();
+ dashboard.slices.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: dashboard.metadata.native_filter_configuration || [],
+ filterSetsConfig: dashboard.metadata.filter_sets_configuration || [],
+ });
+
+ return {
+ datasources,
+ sliceEntities: { ...initSliceEntities, slices, isLoading: false },
+ charts: chartQueries,
+ // read-only data
+ dashboardInfo: {
+ id: dashboard.id,
+ slug: dashboard.slug,
+ metadata: dashboard.metadata,
+ userId: user_id,
+ dash_edit_perm: dashboard.dash_edit_perm,
+ dash_save_perm: dashboard.dash_save_perm,
+ superset_can_explore: dashboard.superset_can_explore,
+ superset_can_csv: dashboard.superset_can_csv,
+ slice_can_edit: dashboard.slice_can_edit,
+ common: {
+ flash_messages: common.flash_messages,
+ conf: common.conf,
+ },
+ lastModifiedTime: dashboard.last_modified_time,
+ },
+ dashboardFilters,
+ nativeFilters,
+ dashboardState: {
+ sliceIds: Array.from(sliceIds),
+ directPathToChild,
+ directPathLastUpdated: Date.now(),
+ focusedFilterField: null,
+ expandedSlices: dashboard.metadata.expanded_slices || {},
+ refreshFrequency: dashboard.metadata.refresh_frequency || 0,
+ // dashboard viewers can set refresh frequency for the current visit,
+ // only persistent refreshFrequency will be saved to backend
+ shouldPersistRefreshFrequency: false,
+ css: dashboard.css || '',
+ colorNamespace: dashboard.metadata.color_namespace,
+ colorScheme: dashboard.metadata.color_scheme,
+ editMode: dashboard.dash_edit_perm && editMode,
+ isPublished: dashboard.published,
+ hasUnsavedChanges: false,
+ maxUndoHistoryExceeded: false,
+ lastModifiedTime: dashboard.last_modified_time,
+ },
+ dashboardLayout,
+ messageToasts: [],
+ impressionId: shortid.generate(),
+ };
+}
diff --git a/superset-frontend/src/dashboard/reducers/index.js b/superset-frontend/src/dashboard/reducers/index.js
index 28804a7..481f167 100644
--- a/superset-frontend/src/dashboard/reducers/index.js
+++ b/superset-frontend/src/dashboard/reducers/index.js
@@ -32,8 +32,6 @@
const impressionId = (state = '') => state;
export default combineReducers({
- user: (state = null) => state,
- common: (state = null) => state,
charts,
datasources,
dashboardInfo,
diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts
index 8b8dc4f..d860cbe 100644
--- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts
+++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts
@@ -24,7 +24,6 @@
} from 'src/dashboard/actions/nativeFilters';
import { FilterSet, NativeFiltersState } from './types';
import { FilterConfiguration } from '../components/nativeFilters/types';
-import { HYDRATE_DASHBOARD } from '../actions/hydrate';
export function getInitialState({
filterSetsConfig,
@@ -70,10 +69,6 @@
) {
const { filterSets } = state;
switch (action.type) {
- case HYDRATE_DASHBOARD:
- return {
- filters: action.data.nativeFilters.filters,
- };
case SAVE_FILTER_SETS:
return {
...state,
diff --git a/superset-frontend/src/dashboard/reducers/sliceEntities.js b/superset-frontend/src/dashboard/reducers/sliceEntities.js
index 70b66db..f34a0b6 100644
--- a/superset-frontend/src/dashboard/reducers/sliceEntities.js
+++ b/superset-frontend/src/dashboard/reducers/sliceEntities.js
@@ -23,7 +23,6 @@
FETCH_ALL_SLICES_STARTED,
SET_ALL_SLICES,
} from '../actions/sliceEntities';
-import { HYDRATE_DASHBOARD } from '../actions/hydrate';
export const initSliceEntities = {
slices: {},
@@ -37,11 +36,6 @@
action,
) {
const actionHandlers = {
- [HYDRATE_DASHBOARD]() {
- return {
- ...action.data.sliceEntities,
- };
- },
[FETCH_ALL_SLICES_STARTED]() {
return {
...state,
diff --git a/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js b/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js
index 2edb51d..49e0186 100644
--- a/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js
+++ b/superset-frontend/src/dashboard/reducers/undoableDashboardLayout.js
@@ -29,17 +29,13 @@
HANDLE_COMPONENT_DROP,
} from '../actions/dashboardLayout';
-import { HYDRATE_DASHBOARD } from '../actions/hydrate';
-
import dashboardLayout from './dashboardLayout';
export default undoable(dashboardLayout, {
// +1 because length of history seems max out at limit - 1
// +1 again so we can detect if we've exceeded the limit
limit: UNDO_LIMIT + 2,
- ignoreInitialState: true,
filter: includeAction([
- HYDRATE_DASHBOARD,
UPDATE_COMPONENTS,
DELETE_COMPONENT,
CREATE_COMPONENT,
diff --git a/superset-frontend/src/dashboard/util/getPermissions.ts b/superset-frontend/src/dashboard/util/getPermissions.ts
deleted file mode 100644
index 0208fd6..0000000
--- a/superset-frontend/src/dashboard/util/getPermissions.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import memoizeOne from 'memoize-one';
-
-const findPermissions = (perm: string, view: string, roles: object) => {
- const roleList = Object.entries(roles);
- if (roleList.length === 0) return false;
- let bool;
-
- roleList.forEach(([role, permissions]) => {
- bool = Boolean(
- permissions.find(
- (permission: Array<string>) =>
- permission[0] === perm && permission[1] === view,
- ),
- );
- });
- return bool;
-};
-
-const getPermissions = memoizeOne(findPermissions);
-
-export default getPermissions;
diff --git a/superset-frontend/src/types/Dashboard.ts b/superset-frontend/src/types/Dashboard.ts
deleted file mode 100644
index 9608cc1..0000000
--- a/superset-frontend/src/types/Dashboard.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import Owner from './Owner';
-import Role from './Role';
-
-type Dashboard = {
- id: number;
- slug: string;
- url: string;
- dashboard_title: string;
- thumbnail_url: string;
- published: boolean;
- css: string;
- json_metadata: string;
- position_json: string;
- changed_by_name: string;
- changed_by: Owner;
- changed_on: string;
- charts: string[]; // just chart names, unfortunately...
- owners: Owner[];
- roles: Role[];
-};
-
-export default Dashboard;
diff --git a/superset-frontend/src/types/Role.ts b/superset-frontend/src/types/Role.ts
deleted file mode 100644
index 54f6876..0000000
--- a/superset-frontend/src/types/Role.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-type Role = {
- id: number;
- name: string;
-};
-
-export default Role;
diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py
index ef11d84..3853a7c 100644
--- a/superset/charts/schemas.py
+++ b/superset/charts/schemas.py
@@ -139,7 +139,6 @@
slice_name = fields.String(description=slice_name_description)
cache_timeout = fields.Integer(description=cache_timeout_description)
changed_on = fields.String(description=changed_on_description)
- modified = fields.String()
datasource = fields.String(description=datasource_name_description)
description = fields.String(description=description_description)
description_markeddown = fields.String(
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index 458e59c..9d6258e 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -307,7 +307,7 @@
except DashboardNotFoundError:
return self.response_404()
- @expose("/<id_or_slug>/charts", methods=["GET"])
+ @expose("/<pk>/charts", methods=["GET"])
@protect()
@safe
@statsd_metrics
@@ -315,7 +315,7 @@
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_charts",
log_to_statsd=False,
)
- def get_charts(self, id_or_slug: str) -> Response:
+ def get_charts(self, pk: int) -> Response:
"""Gets the chart definitions for a given dashboard
---
get:
@@ -324,8 +324,8 @@
parameters:
- in: path
schema:
- type: string
- name: id_or_slug
+ type: integer
+ name: pk
responses:
200:
description: Dashboard chart definitions
@@ -348,16 +348,8 @@
$ref: '#/components/responses/404'
"""
try:
- charts = DashboardDAO.get_charts_for_dashboard(id_or_slug)
+ charts = DashboardDAO.get_charts_for_dashboard(pk)
result = [self.chart_entity_response_schema.dump(chart) for chart in charts]
-
- if is_feature_enabled("REMOVE_SLICE_LEVEL_LABEL_COLORS"):
- # dashboard metadata has dashboard-level label_colors,
- # so remove slice-level label_colors from its form_data
- for chart in result:
- form_data = chart.get("form_data")
- form_data.pop("label_colors", None)
-
return self.response(200, result=result)
except DashboardNotFoundError:
return self.response_404()
diff --git a/superset/dashboards/dao.py b/superset/dashboards/dao.py
index 800ee66..0951f47 100644
--- a/superset/dashboards/dao.py
+++ b/superset/dashboards/dao.py
@@ -82,12 +82,12 @@
return data
@staticmethod
- def get_charts_for_dashboard(id_or_slug: str) -> List[Slice]:
+ def get_charts_for_dashboard(dashboard_id: int) -> List[Slice]:
query = (
db.session.query(Dashboard)
.outerjoin(Slice, Dashboard.slices)
.outerjoin(Slice.table)
- .filter(id_or_slug_filter(id_or_slug))
+ .filter(Dashboard.id == dashboard_id)
.options(contains_eager(Dashboard.slices))
)
# Apply dashboard base filters
diff --git a/superset/views/core.py b/superset/views/core.py
index af2cabe..0093f50 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -800,7 +800,6 @@
"slice": slc.data if slc else None,
"standalone": standalone_mode,
"user_id": user_id,
- "user": bootstrap_user_data(g.user, include_perms=True),
"forced_height": request.args.get("height"),
"common": common_bootstrap_payload(),
}
@@ -1812,11 +1811,13 @@
if not dashboard:
abort(404)
+ data = dashboard.full_data()
+
if config["ENABLE_ACCESS_REQUEST"]:
- for datasource in dashboard.datasources:
+ for datasource in data["datasources"].values():
datasource = ConnectorRegistry.get_datasource(
- datasource_type=datasource.type,
- datasource_id=datasource.id,
+ datasource_type=datasource["type"],
+ datasource_id=datasource["id"],
session=db.session(),
)
if datasource and not security_manager.can_access_datasource(
@@ -1835,6 +1836,10 @@
dash_edit_perm = check_ownership(
dashboard, raise_if_false=False
) and security_manager.can_access("can_save_dash", "Superset")
+ dash_save_perm = security_manager.can_access("can_save_dash", "Superset")
+ superset_can_explore = security_manager.can_access("can_explore", "Superset")
+ superset_can_csv = security_manager.can_access("can_csv", "Superset")
+ slice_can_edit = security_manager.can_access("can_edit", "SliceModelView")
standalone_mode = ReservedUrlParameters.is_standalone_mode()
edit_mode = (
request.args.get(utils.ReservedUrlParameters.EDIT_MODE.value) == "true"
@@ -1847,11 +1852,41 @@
edit_mode=edit_mode,
)
- bootstrap_data = {
- "user": bootstrap_user_data(g.user, include_perms=True),
- "common": common_bootstrap_payload(),
+ if is_feature_enabled("REMOVE_SLICE_LEVEL_LABEL_COLORS"):
+ # dashboard metadata has dashboard-level label_colors,
+ # so remove slice-level label_colors from its form_data
+ for slc in data["slices"]:
+ form_data = slc.get("form_data")
+ form_data.pop("label_colors", None)
+
+ url_params = {
+ key: value
+ for key, value in request.args.items()
+ if key not in [param.value for param in utils.ReservedUrlParameters]
}
+ bootstrap_data = {
+ "user_id": g.user.get_id(),
+ "common": common_bootstrap_payload(),
+ "editMode": edit_mode,
+ "urlParams": url_params,
+ "dashboard_data": {
+ **data["dashboard"],
+ "standalone_mode": standalone_mode,
+ "dash_save_perm": dash_save_perm,
+ "dash_edit_perm": dash_edit_perm,
+ "superset_can_explore": superset_can_explore,
+ "superset_can_csv": superset_can_csv,
+ "slice_can_edit": slice_can_edit,
+ },
+ "datasources": data["datasources"],
+ }
+
+ if request.args.get("json") == "true":
+ return json_success(
+ json.dumps(bootstrap_data, default=utils.pessimistic_json_iso_dttm_ser)
+ )
+
return self.render_template(
"superset/dashboard.html",
entry="dashboard",
diff --git a/tests/dashboard_tests.py b/tests/dashboard_tests.py
index 97d9e7b..f243400 100644
--- a/tests/dashboard_tests.py
+++ b/tests/dashboard_tests.py
@@ -128,10 +128,25 @@
dash_count_before = db.session.query(func.count(Dashboard.id)).first()[0]
url = "/dashboard/new/"
resp = self.get_resp(url)
+ self.assertIn("[ untitled dashboard ]", resp)
dash_count_after = db.session.query(func.count(Dashboard.id)).first()[0]
self.assertEqual(dash_count_before + 1, dash_count_after)
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_dashboard_modes(self):
+ self.login(username="admin")
+ dash = db.session.query(Dashboard).filter_by(slug="births").first()
+ url = dash.url
+ if dash.url.find("?") == -1:
+ url += "?"
+ else:
+ url += "&"
+ resp = self.get_resp(url + "edit=true&standalone=true")
+ self.assertIn("editMode": true", resp)
+ self.assertIn("standalone_mode": true", resp)
+ self.assertIn('<body class="standalone">', resp)
+
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_save_dash(self, username="admin"):
self.login(username=username)
dash = db.session.query(Dashboard).filter_by(slug="births").first()
@@ -175,6 +190,9 @@
self.assertIn("world_health", new_url)
self.assertNotIn("preselect_filters", new_url)
+ resp = self.get_resp(new_url)
+ self.assertIn("North America", resp)
+
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
def test_save_dash_with_invalid_filters(self, username="admin"):
self.login(username=username)
@@ -390,6 +408,8 @@
resp = self.get_resp("/api/v1/dashboard/")
self.assertIn("/superset/dashboard/births/", resp)
+ self.assertIn("Births", self.get_resp("/superset/dashboard/births/"))
+
# Confirm that public doesn't have access to other datasets.
resp = self.get_resp("/api/v1/chart/")
self.assertNotIn("wb_health_population", resp)
diff --git a/tests/dashboards/api_tests.py b/tests/dashboards/api_tests.py
index 4a329e4..fadabe2 100644
--- a/tests/dashboards/api_tests.py
+++ b/tests/dashboards/api_tests.py
@@ -239,22 +239,6 @@
)
@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):
"""
Dashboard API: Test getting charts belonging to a dashboard that does not exist