mirror of
https://github.com/apache/superset.git
synced 2026-07-07 15:25:36 +00:00
Compare commits
3 Commits
chore/ci/s
...
v2021.15.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e26e198c5a | ||
|
|
068165f601 | ||
|
|
cb9be0b9b3 |
@@ -23,28 +23,12 @@ import {
|
|||||||
} from './dashboard.helper';
|
} from './dashboard.helper';
|
||||||
|
|
||||||
describe('Dashboard load', () => {
|
describe('Dashboard load', () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.login();
|
cy.login();
|
||||||
|
cy.visit(WORLD_HEALTH_DASHBOARD);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load dashboard', () => {
|
it('should load dashboard', () => {
|
||||||
cy.visit(WORLD_HEALTH_DASHBOARD);
|
|
||||||
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
|
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');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,12 +61,9 @@ describe('Nativefilters', () => {
|
|||||||
.click()
|
.click()
|
||||||
.type('Country name');
|
.type('Country name');
|
||||||
|
|
||||||
cy.get('.ant-modal')
|
cy.get('.ant-modal').find('[data-test="datasource-input"]').click();
|
||||||
.find('[data-test="datasource-input"]')
|
|
||||||
.click()
|
|
||||||
.type('wb_health_population');
|
|
||||||
|
|
||||||
cy.get('.ant-modal [data-test="datasource-input"] .Select__menu')
|
cy.get('[data-test="datasource-input"]')
|
||||||
.contains('wb_health_population')
|
.contains('wb_health_population')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
@@ -158,12 +155,9 @@ describe('Nativefilters', () => {
|
|||||||
.click()
|
.click()
|
||||||
.type('Country name');
|
.type('Country name');
|
||||||
|
|
||||||
cy.get('.ant-modal')
|
cy.get('.ant-modal').find('[data-test="datasource-input"]').click();
|
||||||
.find('[data-test="datasource-input"]')
|
|
||||||
.click()
|
|
||||||
.type('wb_health_population');
|
|
||||||
|
|
||||||
cy.get('.ant-modal [data-test="datasource-input"] .Select__menu')
|
cy.get('[data-test="datasource-input"]')
|
||||||
.contains('wb_health_population')
|
.contains('wb_health_population')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
@@ -193,10 +187,9 @@ describe('Nativefilters', () => {
|
|||||||
cy.get('.ant-modal')
|
cy.get('.ant-modal')
|
||||||
.find('[data-test="datasource-input"]')
|
.find('[data-test="datasource-input"]')
|
||||||
.last()
|
.last()
|
||||||
.click()
|
.click();
|
||||||
.type('wb_health_population');
|
|
||||||
|
|
||||||
cy.get('.ant-modal [data-test="datasource-input"] .Select__menu')
|
cy.get('[data-test="datasource-input"]')
|
||||||
.last()
|
.last()
|
||||||
.contains('wb_health_population')
|
.contains('wb_health_population')
|
||||||
.click();
|
.click();
|
||||||
|
|||||||
@@ -18,10 +18,9 @@
|
|||||||
*/
|
*/
|
||||||
/* eslint camelcase: 0 */
|
/* eslint camelcase: 0 */
|
||||||
import { t } from '@superset-ui/core';
|
import { t } from '@superset-ui/core';
|
||||||
import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate';
|
|
||||||
import { ChartState } from 'src/explore/types';
|
import { ChartState } from 'src/explore/types';
|
||||||
import { getFormDataFromControls } from 'src/explore/controlUtils';
|
import { getFormDataFromControls } from 'src/explore/controlUtils';
|
||||||
import { now } from 'src/modules/dates';
|
import { now } from '../modules/dates';
|
||||||
import * as actions from './chartAction';
|
import * as actions from './chartAction';
|
||||||
|
|
||||||
export const chart: ChartState = {
|
export const chart: ChartState = {
|
||||||
@@ -193,9 +192,7 @@ export default function chartReducer(
|
|||||||
delete charts[key];
|
delete charts[key];
|
||||||
return charts;
|
return charts;
|
||||||
}
|
}
|
||||||
if (action.type === HYDRATE_DASHBOARD) {
|
|
||||||
return { ...action.data.charts };
|
|
||||||
}
|
|
||||||
if (action.type in actionHandlers) {
|
if (action.type in actionHandlers) {
|
||||||
return {
|
return {
|
||||||
...charts,
|
...charts,
|
||||||
|
|||||||
@@ -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`);
|
|
||||||
@@ -26,5 +26,4 @@ export {
|
|||||||
// A central catalog of API Resource hooks.
|
// A central catalog of API Resource hooks.
|
||||||
// Add new API hooks here, organized under
|
// Add new API hooks here, organized under
|
||||||
// different files for different resource types.
|
// different files for different resource types.
|
||||||
export * from './charts';
|
export { useChartOwnerNames } from './charts';
|
||||||
export * from './dashboards';
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default class ErrorBoundary extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error, info) {
|
componentDidCatch(error, info) {
|
||||||
if (this.props.onError) this.props.onError(error, info);
|
this.props.onError(error, info);
|
||||||
this.setState({ error, info });
|
this.setState({ error, info });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,28 +25,22 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
|
|||||||
import { DynamicPluginProvider } from 'src/components/DynamicPlugins';
|
import { DynamicPluginProvider } from 'src/components/DynamicPlugins';
|
||||||
import setupApp from '../setup/setupApp';
|
import setupApp from '../setup/setupApp';
|
||||||
import setupPlugins from '../setup/setupPlugins';
|
import setupPlugins from '../setup/setupPlugins';
|
||||||
import DashboardPage from './containers/DashboardPage';
|
import DashboardContainer from './containers/Dashboard';
|
||||||
import { theme } from '../preamble';
|
import { theme } from '../preamble';
|
||||||
|
|
||||||
setupApp();
|
setupApp();
|
||||||
setupPlugins();
|
setupPlugins();
|
||||||
|
|
||||||
const App = ({ store }) => {
|
const App = ({ store }) => (
|
||||||
const dashboardIdOrSlug = window.location.pathname.split('/')[3];
|
<Provider store={store}>
|
||||||
return (
|
<DndProvider backend={HTML5Backend}>
|
||||||
<Provider store={store}>
|
<ThemeProvider theme={theme}>
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DynamicPluginProvider>
|
||||||
<ThemeProvider theme={theme}>
|
<DashboardContainer />
|
||||||
<DynamicPluginProvider>
|
</DynamicPluginProvider>
|
||||||
<DashboardPage
|
</ThemeProvider>
|
||||||
store={store}
|
</DndProvider>
|
||||||
dashboardIdOrSlug={dashboardIdOrSlug}
|
</Provider>
|
||||||
/>
|
);
|
||||||
</DynamicPluginProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
</DndProvider>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default hot(App);
|
export default hot(App);
|
||||||
|
|||||||
@@ -19,16 +19,12 @@
|
|||||||
|
|
||||||
import { makeApi } from '@superset-ui/core';
|
import { makeApi } from '@superset-ui/core';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import {
|
import { FilterConfiguration } from 'src/dashboard/components/nativeFilters/types';
|
||||||
Filter,
|
|
||||||
FilterConfiguration,
|
|
||||||
} from 'src/dashboard/components/nativeFilters/types';
|
|
||||||
import { DataMaskType, DataMaskStateWithId } from 'src/dataMask/types';
|
import { DataMaskType, DataMaskStateWithId } from 'src/dataMask/types';
|
||||||
import {
|
import {
|
||||||
SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE,
|
SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE,
|
||||||
SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL,
|
SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL,
|
||||||
} from 'src/dataMask/actions';
|
} from 'src/dataMask/actions';
|
||||||
import { HYDRATE_DASHBOARD } from './hydrate';
|
|
||||||
import { dashboardInfoChanged } from './dashboardInfo';
|
import { dashboardInfoChanged } from './dashboardInfo';
|
||||||
import { DashboardInfo, FilterSet } from '../reducers/types';
|
import { DashboardInfo, FilterSet } from '../reducers/types';
|
||||||
|
|
||||||
@@ -109,18 +105,6 @@ export const setFilterConfiguration = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
type BootstrapData = {
|
|
||||||
nativeFilters: {
|
|
||||||
filters: Filter;
|
|
||||||
filtersState: object;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface SetBooststapData {
|
|
||||||
type: typeof HYDRATE_DASHBOARD;
|
|
||||||
data: BootstrapData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setFilterSetsConfiguration = (
|
export const setFilterSetsConfiguration = (
|
||||||
filterSetsConfig: FilterSet[],
|
filterSetsConfig: FilterSet[],
|
||||||
) => async (dispatch: Dispatch, getState: () => any) => {
|
) => async (dispatch: Dispatch, getState: () => any) => {
|
||||||
@@ -189,5 +173,4 @@ export type AnyFilterAction =
|
|||||||
| SetFilterSetsConfigBegin
|
| SetFilterSetsConfigBegin
|
||||||
| SetFilterSetsConfigComplete
|
| SetFilterSetsConfigComplete
|
||||||
| SetFilterSetsConfigFail
|
| SetFilterSetsConfigFail
|
||||||
| SaveFilterSets
|
| SaveFilterSets;
|
||||||
| SetBooststapData;
|
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class DashboardGrid extends React.PureComponent {
|
|||||||
width,
|
width,
|
||||||
isComponentVisible,
|
isComponentVisible,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const columnPlusGutterWidth =
|
const columnPlusGutterWidth =
|
||||||
(width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
|
(width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class SaveModal extends React.PureComponent<SaveModalProps, SaveModalState> {
|
|||||||
// check refresh frequency is for current session or persist
|
// check refresh frequency is for current session or persist
|
||||||
const refreshFrequency = shouldPersistRefreshFrequency
|
const refreshFrequency = shouldPersistRefreshFrequency
|
||||||
? currentRefreshFrequency
|
? currentRefreshFrequency
|
||||||
: dashboardInfo.metadata?.refresh_frequency; // eslint-disable camelcase
|
: dashboardInfo.metadata.refresh_frequency; // eslint-disable camelcase
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
positions,
|
positions,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ function mapStateToProps({
|
|||||||
maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded,
|
maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded,
|
||||||
lastModifiedTime: Math.max(
|
lastModifiedTime: Math.max(
|
||||||
dashboardState.lastModifiedTime,
|
dashboardState.lastModifiedTime,
|
||||||
dashboardInfo.last_modified_time,
|
dashboardInfo.lastModifiedTime,
|
||||||
),
|
),
|
||||||
editMode: !!dashboardState.editMode,
|
editMode: !!dashboardState.editMode,
|
||||||
slug: dashboardInfo.slug,
|
slug: dashboardInfo.slug,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -22,6 +22,7 @@ import thunk from 'redux-thunk';
|
|||||||
import { createStore, applyMiddleware, compose } from 'redux';
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
import { initFeatureFlags } from 'src/featureFlags';
|
import { initFeatureFlags } from 'src/featureFlags';
|
||||||
import { initEnhancer } from '../reduxUtils';
|
import { initEnhancer } from '../reduxUtils';
|
||||||
|
import getInitialState from './reducers/getInitialState';
|
||||||
import rootReducer from './reducers/index';
|
import rootReducer from './reducers/index';
|
||||||
import logger from '../middleware/loggerMiddleware';
|
import logger from '../middleware/loggerMiddleware';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
@@ -29,16 +30,10 @@ import App from './App';
|
|||||||
const appContainer = document.getElementById('app');
|
const appContainer = document.getElementById('app');
|
||||||
const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
|
const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
|
||||||
initFeatureFlags(bootstrapData.common.feature_flags);
|
initFeatureFlags(bootstrapData.common.feature_flags);
|
||||||
|
const initState = getInitialState(bootstrapData);
|
||||||
const initialState = {
|
|
||||||
user: bootstrapData.user,
|
|
||||||
common: bootstrapData.common,
|
|
||||||
datasources: bootstrapData.datasources,
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = createStore(
|
const store = createStore(
|
||||||
rootReducer,
|
rootReducer,
|
||||||
initialState,
|
initState,
|
||||||
compose(applyMiddleware(thunk, logger), initEnhancer(false)),
|
compose(applyMiddleware(thunk, logger), initEnhancer(false)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
UPDATE_LAYOUT_COMPONENTS,
|
UPDATE_LAYOUT_COMPONENTS,
|
||||||
UPDATE_DASHBOARD_FILTERS_SCOPE,
|
UPDATE_DASHBOARD_FILTERS_SCOPE,
|
||||||
} from '../actions/dashboardFilters';
|
} from '../actions/dashboardFilters';
|
||||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
|
||||||
import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox';
|
import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox';
|
||||||
import { DASHBOARD_ROOT_ID } from '../util/constants';
|
import { DASHBOARD_ROOT_ID } from '../util/constants';
|
||||||
import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata';
|
import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata';
|
||||||
@@ -162,10 +161,6 @@ export default function dashboardFiltersReducer(dashboardFilters = {}, action) {
|
|||||||
|
|
||||||
return updatedFilters;
|
return updatedFilters;
|
||||||
}
|
}
|
||||||
if (action.type === HYDRATE_DASHBOARD) {
|
|
||||||
return action.data.dashboardFilters;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.type in actionHandlers) {
|
if (action.type in actionHandlers) {
|
||||||
const updatedFilters = {
|
const updatedFilters = {
|
||||||
...dashboardFilters,
|
...dashboardFilters,
|
||||||
@@ -173,6 +168,7 @@ export default function dashboardFiltersReducer(dashboardFilters = {}, action) {
|
|||||||
dashboardFilters[action.chartId],
|
dashboardFilters[action.chartId],
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (CHANGE_FILTER_VALUE_ACTIONS.includes(action.type)) {
|
if (CHANGE_FILTER_VALUE_ACTIONS.includes(action.type)) {
|
||||||
buildActiveFilters({ dashboardFilters: updatedFilters });
|
buildActiveFilters({ dashboardFilters: updatedFilters });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { DASHBOARD_INFO_UPDATED } from '../actions/dashboardInfo';
|
import { DASHBOARD_INFO_UPDATED } from '../actions/dashboardInfo';
|
||||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
|
||||||
|
|
||||||
export default function dashboardStateReducer(state = {}, action) {
|
export default function dashboardStateReducer(state = {}, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@@ -27,13 +26,7 @@ export default function dashboardStateReducer(state = {}, action) {
|
|||||||
...state,
|
...state,
|
||||||
...action.newInfo,
|
...action.newInfo,
|
||||||
// server-side compare last_modified_time in second level
|
// server-side compare last_modified_time in second level
|
||||||
last_modified_time: Math.round(new Date().getTime() / 1000),
|
lastModifiedTime: Math.round(new Date().getTime() / 1000),
|
||||||
};
|
|
||||||
case HYDRATE_DASHBOARD:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
...action.data.dashboardInfo,
|
|
||||||
// set async api call data
|
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
@@ -43,15 +43,7 @@ import {
|
|||||||
DASHBOARD_TITLE_CHANGED,
|
DASHBOARD_TITLE_CHANGED,
|
||||||
} from '../actions/dashboardLayout';
|
} from '../actions/dashboardLayout';
|
||||||
|
|
||||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
|
||||||
|
|
||||||
const actionHandlers = {
|
const actionHandlers = {
|
||||||
[HYDRATE_DASHBOARD](state, action) {
|
|
||||||
return {
|
|
||||||
...action.data.dashboardLayout.present,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
[UPDATE_COMPONENTS](state, action) {
|
[UPDATE_COMPONENTS](state, action) {
|
||||||
const {
|
const {
|
||||||
payload: { nextComponents },
|
payload: { nextComponents },
|
||||||
|
|||||||
@@ -36,13 +36,9 @@ import {
|
|||||||
SET_FOCUSED_FILTER_FIELD,
|
SET_FOCUSED_FILTER_FIELD,
|
||||||
UNSET_FOCUSED_FILTER_FIELD,
|
UNSET_FOCUSED_FILTER_FIELD,
|
||||||
} from '../actions/dashboardState';
|
} from '../actions/dashboardState';
|
||||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
|
||||||
|
|
||||||
export default function dashboardStateReducer(state = {}, action) {
|
export default function dashboardStateReducer(state = {}, action) {
|
||||||
const actionHandlers = {
|
const actionHandlers = {
|
||||||
[HYDRATE_DASHBOARD]() {
|
|
||||||
return { ...state, ...action.data.dashboardState };
|
|
||||||
},
|
|
||||||
[UPDATE_CSS]() {
|
[UPDATE_CSS]() {
|
||||||
return { ...state, css: action.css };
|
return { ...state, css: action.css };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,29 +17,22 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { SET_DATASOURCE } from '../actions/datasources';
|
import { SET_DATASOURCE } from '../actions/datasources';
|
||||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
|
||||||
|
|
||||||
export default function datasourceReducer(datasources = {}, action) {
|
export default function datasourceReducer(datasources = {}, action) {
|
||||||
const actionHandlers = {
|
const actionHandlers = {
|
||||||
[HYDRATE_DASHBOARD]() {
|
|
||||||
return action.data.datasources;
|
|
||||||
},
|
|
||||||
[SET_DATASOURCE]() {
|
[SET_DATASOURCE]() {
|
||||||
return action.datasource;
|
return action.datasource;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (action.type in actionHandlers) {
|
if (action.type in actionHandlers) {
|
||||||
if (action.key) {
|
return {
|
||||||
return {
|
...datasources,
|
||||||
...datasources,
|
[action.key]: actionHandlers[action.type](
|
||||||
[action.key]: actionHandlers[action.type](
|
datasources[action.key],
|
||||||
datasources[action.key],
|
action,
|
||||||
action,
|
),
|
||||||
),
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
return actionHandlers[action.type]();
|
|
||||||
}
|
}
|
||||||
return datasources;
|
return datasources;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,82 +17,46 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
import { isString, keyBy } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
import shortid from 'shortid';
|
import shortid from 'shortid';
|
||||||
import { CategoricalColorNamespace } from '@superset-ui/core';
|
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 { initSliceEntities } from 'src/dashboard/reducers/sliceEntities';
|
||||||
import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters';
|
import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters';
|
||||||
import { getParam } from 'src/modules/utils';
|
import { getParam } from 'src/modules/utils';
|
||||||
import { applyDefaultFormData } from 'src/explore/store';
|
import { applyDefaultFormData } from 'src/explore/store';
|
||||||
import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
|
import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
|
||||||
import getPermissions from 'src/dashboard/util/getPermissions';
|
|
||||||
import {
|
import {
|
||||||
DASHBOARD_FILTER_SCOPE_GLOBAL,
|
DASHBOARD_FILTER_SCOPE_GLOBAL,
|
||||||
dashboardFilter,
|
dashboardFilter,
|
||||||
} from 'src/dashboard/reducers/dashboardFilters';
|
} from './dashboardFilters';
|
||||||
|
import { chart } from '../../chart/chartReducer';
|
||||||
import {
|
import {
|
||||||
DASHBOARD_HEADER_ID,
|
DASHBOARD_HEADER_ID,
|
||||||
GRID_DEFAULT_CHART_WIDTH,
|
GRID_DEFAULT_CHART_WIDTH,
|
||||||
GRID_COLUMN_COUNT,
|
GRID_COLUMN_COUNT,
|
||||||
} from 'src/dashboard/util/constants';
|
} from '../util/constants';
|
||||||
import {
|
import {
|
||||||
DASHBOARD_HEADER_TYPE,
|
DASHBOARD_HEADER_TYPE,
|
||||||
CHART_TYPE,
|
CHART_TYPE,
|
||||||
ROW_TYPE,
|
ROW_TYPE,
|
||||||
} from 'src/dashboard/util/componentTypes';
|
} from '../util/componentTypes';
|
||||||
import findFirstParentContainerId from 'src/dashboard/util/findFirstParentContainer';
|
import findFirstParentContainerId from '../util/findFirstParentContainer';
|
||||||
import getEmptyLayout from 'src/dashboard/util/getEmptyLayout';
|
import getEmptyLayout from '../util/getEmptyLayout';
|
||||||
import getFilterConfigsFromFormdata from 'src/dashboard/util/getFilterConfigsFromFormdata';
|
import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata';
|
||||||
import getLocationHash from 'src/dashboard/util/getLocationHash';
|
import getLocationHash from '../util/getLocationHash';
|
||||||
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
|
import newComponentFactory from '../util/newComponentFactory';
|
||||||
import { TIME_RANGE } from 'src/visualizations/FilterBox/FilterBox';
|
import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox';
|
||||||
|
|
||||||
const reservedQueryParams = new Set(['standalone', 'edit']);
|
export default function getInitialState(bootstrapData) {
|
||||||
|
const { user_id, datasources, common, editMode, urlParams } = bootstrapData;
|
||||||
/**
|
|
||||||
* 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';
|
|
||||||
|
|
||||||
|
const dashboard = { ...bootstrapData.dashboard_data };
|
||||||
let preselectFilters = {};
|
let preselectFilters = {};
|
||||||
|
|
||||||
chartData.forEach(chart => {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
chart.slice_id = chart.form_data.slice_id;
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
// allow request parameter overwrite dashboard metadata
|
// allow request parameter overwrite dashboard metadata
|
||||||
preselectFilters = JSON.parse(
|
preselectFilters = JSON.parse(
|
||||||
getParam('preselect_filters') || metadata.default_filters,
|
getParam('preselect_filters') || dashboard.metadata.default_filters,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
@@ -100,12 +64,12 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
|
|||||||
|
|
||||||
// Priming the color palette with user's label-color mapping provided in
|
// Priming the color palette with user's label-color mapping provided in
|
||||||
// the dashboard's JSON metadata
|
// the dashboard's JSON metadata
|
||||||
if (metadata?.label_colors) {
|
if (dashboard.metadata && dashboard.metadata.label_colors) {
|
||||||
const scheme = metadata.color_scheme;
|
const scheme = dashboard.metadata.color_scheme;
|
||||||
const namespace = metadata.color_namespace;
|
const namespace = dashboard.metadata.color_namespace;
|
||||||
const colorMap = isString(metadata.label_colors)
|
const colorMap = isString(dashboard.metadata.label_colors)
|
||||||
? JSON.parse(metadata.label_colors)
|
? JSON.parse(dashboard.metadata.label_colors)
|
||||||
: metadata.label_colors;
|
: dashboard.metadata.label_colors;
|
||||||
Object.keys(colorMap).forEach(label => {
|
Object.keys(colorMap).forEach(label => {
|
||||||
CategoricalColorNamespace.getScale(scheme, namespace).setColor(
|
CategoricalColorNamespace.getScale(scheme, namespace).setColor(
|
||||||
label,
|
label,
|
||||||
@@ -115,11 +79,11 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// dashboard layout
|
// dashboard layout
|
||||||
const { position_data } = dashboardData;
|
const { position_json: positionJson } = dashboard;
|
||||||
// new dash: position_json could be {} or null
|
// new dash: positionJson could be {} or null
|
||||||
const layout =
|
const layout =
|
||||||
position_data && Object.keys(position_data).length > 0
|
positionJson && Object.keys(positionJson).length > 0
|
||||||
? position_data
|
? positionJson
|
||||||
: getEmptyLayout();
|
: getEmptyLayout();
|
||||||
|
|
||||||
// create a lookup to sync layout names with slice names
|
// create a lookup to sync layout names with slice names
|
||||||
@@ -136,13 +100,13 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
|
|||||||
let newSlicesContainer;
|
let newSlicesContainer;
|
||||||
let newSlicesContainerWidth = 0;
|
let newSlicesContainerWidth = 0;
|
||||||
|
|
||||||
const filterScopes = metadata?.filter_scopes || {};
|
const filterScopes = dashboard.metadata.filter_scopes || {};
|
||||||
|
|
||||||
const chartQueries = {};
|
const chartQueries = {};
|
||||||
const dashboardFilters = {};
|
const dashboardFilters = {};
|
||||||
const slices = {};
|
const slices = {};
|
||||||
const sliceIds = new Set();
|
const sliceIds = new Set();
|
||||||
chartData.forEach(slice => {
|
dashboard.slices.forEach(slice => {
|
||||||
const key = slice.slice_id;
|
const key = slice.slice_id;
|
||||||
const form_data = {
|
const form_data = {
|
||||||
...slice.form_data,
|
...slice.form_data,
|
||||||
@@ -276,7 +240,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
|
|||||||
id: DASHBOARD_HEADER_ID,
|
id: DASHBOARD_HEADER_ID,
|
||||||
type: DASHBOARD_HEADER_TYPE,
|
type: DASHBOARD_HEADER_TYPE,
|
||||||
meta: {
|
meta: {
|
||||||
text: dashboardData.dashboard_title,
|
text: dashboard.dashboard_title,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -295,67 +259,54 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nativeFilters = getInitialNativeFilterState({
|
const nativeFilters = getInitialNativeFilterState({
|
||||||
filterConfig: metadata?.native_filter_configuration || [],
|
filterConfig: dashboard.metadata.native_filter_configuration || [],
|
||||||
filterSetsConfig: metadata?.filter_sets_configuration || [],
|
filterSetsConfig: dashboard.metadata.filter_sets_configuration || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { roles } = getState().user;
|
return {
|
||||||
|
datasources,
|
||||||
return dispatch({
|
sliceEntities: { ...initSliceEntities, slices, isLoading: false },
|
||||||
type: HYDRATE_DASHBOARD,
|
charts: chartQueries,
|
||||||
data: {
|
// read-only data
|
||||||
datasources: keyBy(datasourcesData, 'uid'),
|
dashboardInfo: {
|
||||||
sliceEntities: { ...initSliceEntities, slices, isLoading: false },
|
id: dashboard.id,
|
||||||
charts: chartQueries,
|
slug: dashboard.slug,
|
||||||
// read-only data
|
metadata: dashboard.metadata,
|
||||||
dashboardInfo: {
|
userId: user_id,
|
||||||
...dashboardData,
|
dash_edit_perm: dashboard.dash_edit_perm,
|
||||||
userId: String(user.userId), // legacy, please use state.user instead
|
dash_save_perm: dashboard.dash_save_perm,
|
||||||
dash_edit_perm: getPermissions('can_write', 'Dashboard', roles),
|
superset_can_explore: dashboard.superset_can_explore,
|
||||||
dash_save_perm: getPermissions('can_save_dash', 'Superset', roles),
|
superset_can_csv: dashboard.superset_can_csv,
|
||||||
dash_share_perm: getPermissions(
|
slice_can_edit: dashboard.slice_can_edit,
|
||||||
'can_share_dashboard',
|
common: {
|
||||||
'Superset',
|
flash_messages: common.flash_messages,
|
||||||
roles,
|
conf: common.conf,
|
||||||
),
|
|
||||||
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,
|
lastModifiedTime: dashboard.last_modified_time,
|
||||||
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(),
|
|
||||||
},
|
},
|
||||||
});
|
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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -32,8 +32,6 @@ import messageToasts from '../../messageToasts/reducers';
|
|||||||
const impressionId = (state = '') => state;
|
const impressionId = (state = '') => state;
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
user: (state = null) => state,
|
|
||||||
common: (state = null) => state,
|
|
||||||
charts,
|
charts,
|
||||||
datasources,
|
datasources,
|
||||||
dashboardInfo,
|
dashboardInfo,
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
} from 'src/dashboard/actions/nativeFilters';
|
} from 'src/dashboard/actions/nativeFilters';
|
||||||
import { FilterSet, NativeFiltersState } from './types';
|
import { FilterSet, NativeFiltersState } from './types';
|
||||||
import { FilterConfiguration } from '../components/nativeFilters/types';
|
import { FilterConfiguration } from '../components/nativeFilters/types';
|
||||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
|
||||||
|
|
||||||
export function getInitialState({
|
export function getInitialState({
|
||||||
filterSetsConfig,
|
filterSetsConfig,
|
||||||
@@ -70,10 +69,6 @@ export default function nativeFilterReducer(
|
|||||||
) {
|
) {
|
||||||
const { filterSets } = state;
|
const { filterSets } = state;
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case HYDRATE_DASHBOARD:
|
|
||||||
return {
|
|
||||||
filters: action.data.nativeFilters.filters,
|
|
||||||
};
|
|
||||||
case SAVE_FILTER_SETS:
|
case SAVE_FILTER_SETS:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
FETCH_ALL_SLICES_STARTED,
|
FETCH_ALL_SLICES_STARTED,
|
||||||
SET_ALL_SLICES,
|
SET_ALL_SLICES,
|
||||||
} from '../actions/sliceEntities';
|
} from '../actions/sliceEntities';
|
||||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
|
||||||
|
|
||||||
export const initSliceEntities = {
|
export const initSliceEntities = {
|
||||||
slices: {},
|
slices: {},
|
||||||
@@ -37,11 +36,6 @@ export default function sliceEntitiesReducer(
|
|||||||
action,
|
action,
|
||||||
) {
|
) {
|
||||||
const actionHandlers = {
|
const actionHandlers = {
|
||||||
[HYDRATE_DASHBOARD]() {
|
|
||||||
return {
|
|
||||||
...action.data.sliceEntities,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[FETCH_ALL_SLICES_STARTED]() {
|
[FETCH_ALL_SLICES_STARTED]() {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -29,17 +29,13 @@ import {
|
|||||||
HANDLE_COMPONENT_DROP,
|
HANDLE_COMPONENT_DROP,
|
||||||
} from '../actions/dashboardLayout';
|
} from '../actions/dashboardLayout';
|
||||||
|
|
||||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
|
||||||
|
|
||||||
import dashboardLayout from './dashboardLayout';
|
import dashboardLayout from './dashboardLayout';
|
||||||
|
|
||||||
export default undoable(dashboardLayout, {
|
export default undoable(dashboardLayout, {
|
||||||
// +1 because length of history seems max out at limit - 1
|
// +1 because length of history seems max out at limit - 1
|
||||||
// +1 again so we can detect if we've exceeded the limit
|
// +1 again so we can detect if we've exceeded the limit
|
||||||
limit: UNDO_LIMIT + 2,
|
limit: UNDO_LIMIT + 2,
|
||||||
ignoreInitialState: true,
|
|
||||||
filter: includeAction([
|
filter: includeAction([
|
||||||
HYDRATE_DASHBOARD,
|
|
||||||
UPDATE_COMPONENTS,
|
UPDATE_COMPONENTS,
|
||||||
DELETE_COMPONENT,
|
DELETE_COMPONENT,
|
||||||
CREATE_COMPONENT,
|
CREATE_COMPONENT,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -45,7 +45,6 @@ interface ChartCardProps {
|
|||||||
chartFilter?: string;
|
chartFilter?: string;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
showThumbnails?: boolean;
|
showThumbnails?: boolean;
|
||||||
featureFlag?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChartCard({
|
export default function ChartCard({
|
||||||
@@ -62,7 +61,6 @@ export default function ChartCard({
|
|||||||
favoriteStatus,
|
favoriteStatus,
|
||||||
chartFilter,
|
chartFilter,
|
||||||
userId,
|
userId,
|
||||||
featureFlag,
|
|
||||||
}: ChartCardProps) {
|
}: ChartCardProps) {
|
||||||
const canEdit = hasPerm('can_write');
|
const canEdit = hasPerm('can_write');
|
||||||
const canDelete = hasPerm('can_write');
|
const canDelete = hasPerm('can_write');
|
||||||
@@ -142,7 +140,11 @@ export default function ChartCard({
|
|||||||
<ListViewCard
|
<ListViewCard
|
||||||
loading={loading}
|
loading={loading}
|
||||||
title={chart.slice_name}
|
title={chart.slice_name}
|
||||||
cover={!featureFlag || !showThumbnails ? <></> : null}
|
cover={
|
||||||
|
!isFeatureEnabled(FeatureFlag.THUMBNAILS) || !showThumbnails ? (
|
||||||
|
<></>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
url={bulkSelectEnabled ? undefined : chart.url}
|
url={bulkSelectEnabled ? undefined : chart.url}
|
||||||
imgURL={chart.thumbnail_url || ''}
|
imgURL={chart.thumbnail_url || ''}
|
||||||
imgFallbackURL="/static/assets/images/chart-card-fallback.svg"
|
imgFallbackURL="/static/assets/images/chart-card-fallback.svg"
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import ListView, {
|
|||||||
SelectOption,
|
SelectOption,
|
||||||
FilterOperators,
|
FilterOperators,
|
||||||
} from 'src/components/ListView';
|
} from 'src/components/ListView';
|
||||||
|
import { getFromLocalStorage } from 'src/utils/localStorageHelpers';
|
||||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||||
import ImportModelsModal from 'src/components/ImportModal/index';
|
import ImportModelsModal from 'src/components/ImportModal/index';
|
||||||
@@ -506,9 +507,16 @@ function ChartList(props: ChartListProps) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
function renderCard(chart: Chart) {
|
function renderCard(chart: Chart) {
|
||||||
|
const { userId } = props.user;
|
||||||
|
const userKey = getFromLocalStorage(userId.toString(), null);
|
||||||
return (
|
return (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
chart={chart}
|
chart={chart}
|
||||||
|
showThumbnails={
|
||||||
|
userKey
|
||||||
|
? userKey.thumbnails
|
||||||
|
: isFeatureEnabled(FeatureFlag.THUMBNAILS)
|
||||||
|
}
|
||||||
hasPerm={hasPerm}
|
hasPerm={hasPerm}
|
||||||
openChartEditModal={openChartEditModal}
|
openChartEditModal={openChartEditModal}
|
||||||
bulkSelectEnabled={bulkSelectEnabled}
|
bulkSelectEnabled={bulkSelectEnabled}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
handleBulkDashboardExport,
|
handleBulkDashboardExport,
|
||||||
CardStyles,
|
CardStyles,
|
||||||
} from 'src/views/CRUD/utils';
|
} from 'src/views/CRUD/utils';
|
||||||
|
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||||
import { Dropdown, Menu } from 'src/common/components';
|
import { Dropdown, Menu } from 'src/common/components';
|
||||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||||
import ListViewCard from 'src/components/ListViewCard';
|
import ListViewCard from 'src/components/ListViewCard';
|
||||||
@@ -47,7 +48,6 @@ interface DashboardCardProps {
|
|||||||
dashboardFilter?: string;
|
dashboardFilter?: string;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
showThumbnails?: boolean;
|
showThumbnails?: boolean;
|
||||||
featureFlag?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DashboardCard({
|
function DashboardCard({
|
||||||
@@ -63,7 +63,6 @@ function DashboardCard({
|
|||||||
favoriteStatus,
|
favoriteStatus,
|
||||||
saveFavoriteStatus,
|
saveFavoriteStatus,
|
||||||
showThumbnails,
|
showThumbnails,
|
||||||
featureFlag,
|
|
||||||
}: DashboardCardProps) {
|
}: DashboardCardProps) {
|
||||||
const canEdit = hasPerm('can_write');
|
const canEdit = hasPerm('can_write');
|
||||||
const canDelete = hasPerm('can_write');
|
const canDelete = hasPerm('can_write');
|
||||||
@@ -150,7 +149,11 @@ function DashboardCard({
|
|||||||
titleRight={
|
titleRight={
|
||||||
<Label>{dashboard.published ? t('published') : t('draft')}</Label>
|
<Label>{dashboard.published ? t('published') : t('draft')}</Label>
|
||||||
}
|
}
|
||||||
cover={!featureFlag || !showThumbnails ? <></> : null}
|
cover={
|
||||||
|
!isFeatureEnabled(FeatureFlag.THUMBNAILS) || !showThumbnails ? (
|
||||||
|
<></>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
url={bulkSelectEnabled ? undefined : dashboard.url}
|
url={bulkSelectEnabled ? undefined : dashboard.url}
|
||||||
imgURL={dashboard.thumbnail_url}
|
imgURL={dashboard.thumbnail_url}
|
||||||
imgFallbackURL="/static/assets/images/dashboard-card-fallback.svg"
|
imgFallbackURL="/static/assets/images/dashboard-card-fallback.svg"
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import ListView, {
|
|||||||
Filters,
|
Filters,
|
||||||
FilterOperators,
|
FilterOperators,
|
||||||
} from 'src/components/ListView';
|
} from 'src/components/ListView';
|
||||||
|
import { getFromLocalStorage } from 'src/utils/localStorageHelpers';
|
||||||
import Owner from 'src/types/Owner';
|
import Owner from 'src/types/Owner';
|
||||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
import FacePile from 'src/components/FacePile';
|
import FacePile from 'src/components/FacePile';
|
||||||
@@ -452,12 +453,19 @@ function DashboardList(props: DashboardListProps) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
function renderCard(dashboard: Dashboard) {
|
function renderCard(dashboard: Dashboard) {
|
||||||
|
const { userId } = props.user;
|
||||||
|
const userKey = getFromLocalStorage(userId.toString(), null);
|
||||||
return (
|
return (
|
||||||
<DashboardCard
|
<DashboardCard
|
||||||
dashboard={dashboard}
|
dashboard={dashboard}
|
||||||
hasPerm={hasPerm}
|
hasPerm={hasPerm}
|
||||||
bulkSelectEnabled={bulkSelectEnabled}
|
bulkSelectEnabled={bulkSelectEnabled}
|
||||||
refreshData={refreshData}
|
refreshData={refreshData}
|
||||||
|
showThumbnails={
|
||||||
|
userKey
|
||||||
|
? userKey.thumbnails
|
||||||
|
: isFeatureEnabled(FeatureFlag.THUMBNAILS)
|
||||||
|
}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
addDangerToast={addDangerToast}
|
addDangerToast={addDangerToast}
|
||||||
addSuccessToast={addSuccessToast}
|
addSuccessToast={addSuccessToast}
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ interface ChartTableProps {
|
|||||||
user?: User;
|
user?: User;
|
||||||
mine: Array<any>;
|
mine: Array<any>;
|
||||||
showThumbnails: boolean;
|
showThumbnails: boolean;
|
||||||
featureFlag: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChartTable({
|
function ChartTable({
|
||||||
@@ -54,7 +53,6 @@ function ChartTable({
|
|||||||
addSuccessToast,
|
addSuccessToast,
|
||||||
mine,
|
mine,
|
||||||
showThumbnails,
|
showThumbnails,
|
||||||
featureFlag,
|
|
||||||
}: ChartTableProps) {
|
}: ChartTableProps) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const {
|
const {
|
||||||
@@ -186,7 +184,6 @@ function ChartTable({
|
|||||||
hasPerm={hasPerm}
|
hasPerm={hasPerm}
|
||||||
showThumbnails={showThumbnails}
|
showThumbnails={showThumbnails}
|
||||||
bulkSelectEnabled={bulkSelectEnabled}
|
bulkSelectEnabled={bulkSelectEnabled}
|
||||||
featureFlag={featureFlag}
|
|
||||||
refreshData={refreshData}
|
refreshData={refreshData}
|
||||||
addDangerToast={addDangerToast}
|
addDangerToast={addDangerToast}
|
||||||
addSuccessToast={addSuccessToast}
|
addSuccessToast={addSuccessToast}
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ function DashboardTable({
|
|||||||
addSuccessToast,
|
addSuccessToast,
|
||||||
mine,
|
mine,
|
||||||
showThumbnails,
|
showThumbnails,
|
||||||
featureFlag,
|
|
||||||
}: DashboardTableProps) {
|
}: DashboardTableProps) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const {
|
const {
|
||||||
@@ -193,7 +192,6 @@ function DashboardTable({
|
|||||||
dashboard={e}
|
dashboard={e}
|
||||||
hasPerm={hasPerm}
|
hasPerm={hasPerm}
|
||||||
bulkSelectEnabled={false}
|
bulkSelectEnabled={false}
|
||||||
featureFlag={featureFlag}
|
|
||||||
showThumbnails={showThumbnails}
|
showThumbnails={showThumbnails}
|
||||||
dashboardFilter={dashboardFilter}
|
dashboardFilter={dashboardFilter}
|
||||||
refreshData={refreshData}
|
refreshData={refreshData}
|
||||||
|
|||||||
@@ -228,7 +228,6 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
|
|||||||
user={user}
|
user={user}
|
||||||
mine={dashboardData}
|
mine={dashboardData}
|
||||||
showThumbnails={checked}
|
showThumbnails={checked}
|
||||||
featureFlag={isFeatureEnabled(FeatureFlag.THUMBNAILS)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
@@ -248,12 +247,7 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
|
|||||||
{!chartData ? (
|
{!chartData ? (
|
||||||
<Loading position="inline" />
|
<Loading position="inline" />
|
||||||
) : (
|
) : (
|
||||||
<ChartTable
|
<ChartTable showThumbnails={checked} user={user} mine={chartData} />
|
||||||
showThumbnails={checked}
|
|
||||||
user={user}
|
|
||||||
mine={chartData}
|
|
||||||
featureFlag={isFeatureEnabled(FeatureFlag.THUMBNAILS)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ class ChartEntityResponseSchema(Schema):
|
|||||||
slice_name = fields.String(description=slice_name_description)
|
slice_name = fields.String(description=slice_name_description)
|
||||||
cache_timeout = fields.Integer(description=cache_timeout_description)
|
cache_timeout = fields.Integer(description=cache_timeout_description)
|
||||||
changed_on = fields.String(description=changed_on_description)
|
changed_on = fields.String(description=changed_on_description)
|
||||||
modified = fields.String()
|
|
||||||
datasource = fields.String(description=datasource_name_description)
|
datasource = fields.String(description=datasource_name_description)
|
||||||
description = fields.String(description=description_description)
|
description = fields.String(description=description_description)
|
||||||
description_markeddown = fields.String(
|
description_markeddown = fields.String(
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||||||
except DashboardNotFoundError:
|
except DashboardNotFoundError:
|
||||||
return self.response_404()
|
return self.response_404()
|
||||||
|
|
||||||
@expose("/<id_or_slug>/charts", methods=["GET"])
|
@expose("/<pk>/charts", methods=["GET"])
|
||||||
@protect()
|
@protect()
|
||||||
@safe
|
@safe
|
||||||
@statsd_metrics
|
@statsd_metrics
|
||||||
@@ -315,7 +315,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||||||
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_charts",
|
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_charts",
|
||||||
log_to_statsd=False,
|
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
|
"""Gets the chart definitions for a given dashboard
|
||||||
---
|
---
|
||||||
get:
|
get:
|
||||||
@@ -324,8 +324,8 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: integer
|
||||||
name: id_or_slug
|
name: pk
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Dashboard chart definitions
|
description: Dashboard chart definitions
|
||||||
@@ -348,16 +348,8 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||||||
$ref: '#/components/responses/404'
|
$ref: '#/components/responses/404'
|
||||||
"""
|
"""
|
||||||
try:
|
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]
|
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)
|
return self.response(200, result=result)
|
||||||
except DashboardNotFoundError:
|
except DashboardNotFoundError:
|
||||||
return self.response_404()
|
return self.response_404()
|
||||||
|
|||||||
@@ -82,12 +82,12 @@ class DashboardDAO(BaseDAO):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_charts_for_dashboard(id_or_slug: str) -> List[Slice]:
|
def get_charts_for_dashboard(dashboard_id: int) -> List[Slice]:
|
||||||
query = (
|
query = (
|
||||||
db.session.query(Dashboard)
|
db.session.query(Dashboard)
|
||||||
.outerjoin(Slice, Dashboard.slices)
|
.outerjoin(Slice, Dashboard.slices)
|
||||||
.outerjoin(Slice.table)
|
.outerjoin(Slice.table)
|
||||||
.filter(id_or_slug_filter(id_or_slug))
|
.filter(Dashboard.id == dashboard_id)
|
||||||
.options(contains_eager(Dashboard.slices))
|
.options(contains_eager(Dashboard.slices))
|
||||||
)
|
)
|
||||||
# Apply dashboard base filters
|
# Apply dashboard base filters
|
||||||
|
|||||||
@@ -800,7 +800,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||||||
"slice": slc.data if slc else None,
|
"slice": slc.data if slc else None,
|
||||||
"standalone": standalone_mode,
|
"standalone": standalone_mode,
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"user": bootstrap_user_data(g.user, include_perms=True),
|
|
||||||
"forced_height": request.args.get("height"),
|
"forced_height": request.args.get("height"),
|
||||||
"common": common_bootstrap_payload(),
|
"common": common_bootstrap_payload(),
|
||||||
}
|
}
|
||||||
@@ -1812,11 +1811,13 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||||||
if not dashboard:
|
if not dashboard:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
data = dashboard.full_data()
|
||||||
|
|
||||||
if config["ENABLE_ACCESS_REQUEST"]:
|
if config["ENABLE_ACCESS_REQUEST"]:
|
||||||
for datasource in dashboard.datasources:
|
for datasource in data["datasources"].values():
|
||||||
datasource = ConnectorRegistry.get_datasource(
|
datasource = ConnectorRegistry.get_datasource(
|
||||||
datasource_type=datasource.type,
|
datasource_type=datasource["type"],
|
||||||
datasource_id=datasource.id,
|
datasource_id=datasource["id"],
|
||||||
session=db.session(),
|
session=db.session(),
|
||||||
)
|
)
|
||||||
if datasource and not security_manager.can_access_datasource(
|
if datasource and not security_manager.can_access_datasource(
|
||||||
@@ -1835,6 +1836,10 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||||||
dash_edit_perm = check_ownership(
|
dash_edit_perm = check_ownership(
|
||||||
dashboard, raise_if_false=False
|
dashboard, raise_if_false=False
|
||||||
) and security_manager.can_access("can_save_dash", "Superset")
|
) 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()
|
standalone_mode = ReservedUrlParameters.is_standalone_mode()
|
||||||
edit_mode = (
|
edit_mode = (
|
||||||
request.args.get(utils.ReservedUrlParameters.EDIT_MODE.value) == "true"
|
request.args.get(utils.ReservedUrlParameters.EDIT_MODE.value) == "true"
|
||||||
@@ -1847,11 +1852,41 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||||||
edit_mode=edit_mode,
|
edit_mode=edit_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
bootstrap_data = {
|
if is_feature_enabled("REMOVE_SLICE_LEVEL_LABEL_COLORS"):
|
||||||
"user": bootstrap_user_data(g.user, include_perms=True),
|
# dashboard metadata has dashboard-level label_colors,
|
||||||
"common": common_bootstrap_payload(),
|
# 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(
|
return self.render_template(
|
||||||
"superset/dashboard.html",
|
"superset/dashboard.html",
|
||||||
entry="dashboard",
|
entry="dashboard",
|
||||||
|
|||||||
@@ -1362,8 +1362,10 @@ class NVD3TimeSeriesViz(NVD3Viz):
|
|||||||
|
|
||||||
df2 = self.get_df_payload(query_object, time_compare=option).get("df")
|
df2 = self.get_df_payload(query_object, time_compare=option).get("df")
|
||||||
if df2 is not None and DTTM_ALIAS in df2:
|
if df2 is not None and DTTM_ALIAS in df2:
|
||||||
|
dttm_series = df2[DTTM_ALIAS] + delta
|
||||||
|
df2 = df2.drop(DTTM_ALIAS, axis=1)
|
||||||
|
df2 = pd.concat([dttm_series, df2], axis=1)
|
||||||
label = "{} offset".format(option)
|
label = "{} offset".format(option)
|
||||||
df2[DTTM_ALIAS] += delta
|
|
||||||
df2 = self.process_data(df2)
|
df2 = self.process_data(df2)
|
||||||
self._extra_chart_data.append((label, df2))
|
self._extra_chart_data.append((label, df2))
|
||||||
|
|
||||||
|
|||||||
@@ -128,9 +128,24 @@ class TestDashboard(SupersetTestCase):
|
|||||||
dash_count_before = db.session.query(func.count(Dashboard.id)).first()[0]
|
dash_count_before = db.session.query(func.count(Dashboard.id)).first()[0]
|
||||||
url = "/dashboard/new/"
|
url = "/dashboard/new/"
|
||||||
resp = self.get_resp(url)
|
resp = self.get_resp(url)
|
||||||
|
self.assertIn("[ untitled dashboard ]", resp)
|
||||||
dash_count_after = db.session.query(func.count(Dashboard.id)).first()[0]
|
dash_count_after = db.session.query(func.count(Dashboard.id)).first()[0]
|
||||||
self.assertEqual(dash_count_before + 1, dash_count_after)
|
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")
|
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
|
||||||
def test_save_dash(self, username="admin"):
|
def test_save_dash(self, username="admin"):
|
||||||
self.login(username=username)
|
self.login(username=username)
|
||||||
@@ -175,6 +190,9 @@ class TestDashboard(SupersetTestCase):
|
|||||||
self.assertIn("world_health", new_url)
|
self.assertIn("world_health", new_url)
|
||||||
self.assertNotIn("preselect_filters", 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")
|
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
|
||||||
def test_save_dash_with_invalid_filters(self, username="admin"):
|
def test_save_dash_with_invalid_filters(self, username="admin"):
|
||||||
self.login(username=username)
|
self.login(username=username)
|
||||||
@@ -390,6 +408,8 @@ class TestDashboard(SupersetTestCase):
|
|||||||
resp = self.get_resp("/api/v1/dashboard/")
|
resp = self.get_resp("/api/v1/dashboard/")
|
||||||
self.assertIn("/superset/dashboard/births/", resp)
|
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.
|
# Confirm that public doesn't have access to other datasets.
|
||||||
resp = self.get_resp("/api/v1/chart/")
|
resp = self.get_resp("/api/v1/chart/")
|
||||||
self.assertNotIn("wb_health_population", resp)
|
self.assertNotIn("wb_health_population", resp)
|
||||||
|
|||||||
@@ -238,22 +238,6 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
|
|||||||
data["result"][0]["slice_name"], dashboard.slices[0].slice_name
|
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")
|
@pytest.mark.usefixtures("create_dashboards")
|
||||||
def test_get_dashboard_charts_not_found(self):
|
def test_get_dashboard_charts_not_found(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user