mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
perf(dashboard): make loading datasets non-blocking (#15699)
This commit is contained in:
@@ -33,7 +33,7 @@ const propTypes = {
|
||||
annotationData: PropTypes.object,
|
||||
actions: PropTypes.object,
|
||||
chartId: PropTypes.number.isRequired,
|
||||
datasource: PropTypes.object.isRequired,
|
||||
datasource: PropTypes.object,
|
||||
// current chart is included by dashboard
|
||||
dashboardId: PropTypes.number,
|
||||
// original selected values for FilterBox viz
|
||||
|
||||
@@ -26,7 +26,7 @@ const propTypes = {
|
||||
annotationData: PropTypes.object,
|
||||
actions: PropTypes.object,
|
||||
chartId: PropTypes.number.isRequired,
|
||||
datasource: PropTypes.object.isRequired,
|
||||
datasource: PropTypes.object,
|
||||
initialValues: PropTypes.object,
|
||||
formData: PropTypes.object.isRequired,
|
||||
height: PropTypes.number,
|
||||
@@ -93,6 +93,7 @@ class ChartRenderer extends React.Component {
|
||||
nextProps.queriesResponse !== this.props.queriesResponse;
|
||||
return (
|
||||
this.hasQueryResponseChange ||
|
||||
nextProps.datasource !== this.props.datasource ||
|
||||
nextProps.annotationData !== this.props.annotationData ||
|
||||
nextProps.ownState !== this.props.ownState ||
|
||||
nextProps.filterState !== this.props.filterState ||
|
||||
@@ -223,7 +224,7 @@ class ChartRenderer extends React.Component {
|
||||
width={width}
|
||||
height={height}
|
||||
annotationData={annotationData}
|
||||
datasource={datasource}
|
||||
datasource={datasource || {}}
|
||||
initialValues={initialValues}
|
||||
formData={formData}
|
||||
ownState={ownState}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Dashboard from 'src/types/Dashboard';
|
||||
import { Dashboard, Datasource } from 'src/dashboard/types';
|
||||
import { Chart } from 'src/types/Chart';
|
||||
import { useApiV1Resource, useTransformedResource } from './apiResources';
|
||||
|
||||
export const useDashboard = (idOrSlug: string | number) =>
|
||||
@@ -33,10 +34,10 @@ export const useDashboard = (idOrSlug: string | number) =>
|
||||
|
||||
// gets the chart definitions for a dashboard
|
||||
export const useDashboardCharts = (idOrSlug: string | number) =>
|
||||
useApiV1Resource(`/api/v1/dashboard/${idOrSlug}/charts`);
|
||||
useApiV1Resource<Chart[]>(`/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`);
|
||||
useApiV1Resource<Datasource[]>(`/api/v1/dashboard/${idOrSlug}/datasets`);
|
||||
|
||||
@@ -16,26 +16,44 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Dispatch } from 'redux';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import { getClientErrorObject } from '../../utils/getClientErrorObject';
|
||||
import { Datasource, RootState } from 'src/dashboard/types';
|
||||
|
||||
export const SET_DATASOURCE = 'SET_DATASOURCE';
|
||||
export function setDatasource(datasource, key) {
|
||||
return { type: SET_DATASOURCE, datasource, key };
|
||||
// update datasources index for Dashboard
|
||||
export enum DatasourcesAction {
|
||||
SET_DATASOURCES = 'SET_DATASOURCES',
|
||||
SET_DATASOURCE = 'SET_DATASOURCE',
|
||||
}
|
||||
|
||||
export const FETCH_DATASOURCE_STARTED = 'FETCH_DATASOURCE_STARTED';
|
||||
export function fetchDatasourceStarted(key) {
|
||||
return { type: FETCH_DATASOURCE_STARTED, key };
|
||||
export type DatasourcesActionPayload =
|
||||
| {
|
||||
type: DatasourcesAction.SET_DATASOURCES;
|
||||
datasources: Datasource[] | null;
|
||||
}
|
||||
| {
|
||||
type: DatasourcesAction.SET_DATASOURCE;
|
||||
key: Datasource['uid'];
|
||||
datasource: Datasource;
|
||||
};
|
||||
|
||||
export function setDatasources(datasources: Datasource[] | null) {
|
||||
return {
|
||||
type: DatasourcesAction.SET_DATASOURCES,
|
||||
datasources,
|
||||
};
|
||||
}
|
||||
|
||||
export const FETCH_DATASOURCE_FAILED = 'FETCH_DATASOURCE_FAILED';
|
||||
export function fetchDatasourceFailed(error, key) {
|
||||
return { type: FETCH_DATASOURCE_FAILED, error, key };
|
||||
export function setDatasource(datasource: Datasource, key: string) {
|
||||
return {
|
||||
type: DatasourcesAction.SET_DATASOURCE,
|
||||
key,
|
||||
datasource,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchDatasourceMetadata(key) {
|
||||
return (dispatch, getState) => {
|
||||
export function fetchDatasourceMetadata(key: string) {
|
||||
return (dispatch: Dispatch, getState: () => RootState) => {
|
||||
const { datasources } = getState();
|
||||
const datasource = datasources[key];
|
||||
|
||||
@@ -45,12 +63,6 @@ export function fetchDatasourceMetadata(key) {
|
||||
|
||||
return SupersetClient.get({
|
||||
endpoint: `/superset/fetch_datasource_metadata?datasourceKey=${key}`,
|
||||
})
|
||||
.then(({ json }) => dispatch(setDatasource(json, key)))
|
||||
.catch(response =>
|
||||
getClientErrorObject(response).then(({ error }) =>
|
||||
dispatch(fetchDatasourceFailed(error, key)),
|
||||
),
|
||||
);
|
||||
}).then(({ json }) => dispatch(setDatasource(json as Datasource, key)));
|
||||
};
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint-disable camelcase */
|
||||
import { isString, keyBy } from 'lodash';
|
||||
import { isString } from 'lodash';
|
||||
import {
|
||||
Behavior,
|
||||
CategoricalColorNamespace,
|
||||
@@ -60,7 +60,7 @@ import extractUrlParams from '../util/extractUrlParams';
|
||||
|
||||
export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';
|
||||
|
||||
export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
|
||||
export const hydrateDashboard = (dashboardData, chartData) => (
|
||||
dispatch,
|
||||
getState,
|
||||
) => {
|
||||
@@ -329,7 +329,6 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
|
||||
return dispatch({
|
||||
type: HYDRATE_DASHBOARD,
|
||||
data: {
|
||||
datasources: keyBy(datasourcesData, 'uid'),
|
||||
sliceEntities: { ...initSliceEntities, slices, isLoading: false },
|
||||
charts: chartQueries,
|
||||
// read-only data
|
||||
|
||||
@@ -51,7 +51,7 @@ const propTypes = {
|
||||
// from redux
|
||||
chart: chartPropShape.isRequired,
|
||||
formData: PropTypes.object.isRequired,
|
||||
datasource: PropTypes.object.isRequired,
|
||||
datasource: PropTypes.object,
|
||||
slice: slicePropShape.isRequired,
|
||||
sliceName: PropTypes.string.isRequired,
|
||||
timeout: PropTypes.number.isRequired,
|
||||
@@ -125,7 +125,8 @@ export default class Chart extends React.Component {
|
||||
if (
|
||||
nextState.width !== this.state.width ||
|
||||
nextState.height !== this.state.height ||
|
||||
nextState.descriptionHeight !== this.state.descriptionHeight
|
||||
nextState.descriptionHeight !== this.state.descriptionHeight ||
|
||||
nextProps.datasource !== this.props.datasource
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -18,19 +18,21 @@
|
||||
*/
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import Dashboard from '../components/Dashboard';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import Dashboard from 'src/dashboard/components/Dashboard';
|
||||
import {
|
||||
addSliceToDashboard,
|
||||
removeSliceFromDashboard,
|
||||
} from '../actions/dashboardState';
|
||||
import { triggerQuery } from '../../chart/chartAction';
|
||||
import { logEvent } from '../../logger/actions';
|
||||
import { getActiveFilters } from '../util/activeDashboardFilters';
|
||||
} from 'src/dashboard/actions/dashboardState';
|
||||
import { setDatasources } from 'src/dashboard/actions/datasources';
|
||||
|
||||
import { triggerQuery } from 'src/chart/chartAction';
|
||||
import { logEvent } from 'src/logger/actions';
|
||||
import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
|
||||
import {
|
||||
getAllActiveFilters,
|
||||
getRelevantDataMask,
|
||||
} from '../util/activeAllDashboardFilters';
|
||||
import { RootState } from '../types';
|
||||
} from 'src/dashboard/util/activeAllDashboardFilters';
|
||||
|
||||
function mapStateToProps(state: RootState) {
|
||||
const {
|
||||
@@ -80,6 +82,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(
|
||||
{
|
||||
setDatasources,
|
||||
addSliceToDashboard,
|
||||
removeSliceFromDashboard,
|
||||
triggerQuery,
|
||||
|
||||
@@ -16,17 +16,19 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useEffect, useState, FC } from 'react';
|
||||
import React, { useEffect, FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useToasts } from 'src/messageToasts/enhancers/withToasts';
|
||||
import Loading from 'src/components/Loading';
|
||||
import {
|
||||
useDashboard,
|
||||
useDashboardCharts,
|
||||
useDashboardDatasets,
|
||||
} from 'src/common/hooks/apiResources';
|
||||
import { ResourceStatus } from 'src/common/hooks/apiResources/apiResources';
|
||||
import { hydrateDashboard } from 'src/dashboard/actions/hydrate';
|
||||
import { setDatasources } from 'src/dashboard/actions/datasources';
|
||||
import injectCustomCss from 'src/dashboard/util/injectCustomCss';
|
||||
|
||||
const DashboardContainer = React.lazy(
|
||||
@@ -40,48 +42,57 @@ const DashboardContainer = React.lazy(
|
||||
|
||||
const DashboardPage: FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { addDangerToast } = useToasts();
|
||||
const { idOrSlug } = useParams<{ idOrSlug: string }>();
|
||||
const [isHydrated, setHydrated] = useState(false);
|
||||
const dashboardResource = useDashboard(idOrSlug);
|
||||
const chartsResource = useDashboardCharts(idOrSlug);
|
||||
const datasetsResource = useDashboardDatasets(idOrSlug);
|
||||
const error = [dashboardResource, chartsResource, datasetsResource].find(
|
||||
resource => resource.status === ResourceStatus.ERROR,
|
||||
)?.error;
|
||||
const { result: dashboard, error: dashboardApiError } = useDashboard(
|
||||
idOrSlug,
|
||||
);
|
||||
const { result: charts, error: chartsApiError } = useDashboardCharts(
|
||||
idOrSlug,
|
||||
);
|
||||
const { result: datasets, error: datasetsApiError } = useDashboardDatasets(
|
||||
idOrSlug,
|
||||
);
|
||||
|
||||
const error = dashboardApiError || chartsApiError;
|
||||
const readyToRender = Boolean(dashboard && charts);
|
||||
const { dashboard_title, css } = dashboard || {};
|
||||
|
||||
useEffect(() => {
|
||||
if (dashboardResource.result) {
|
||||
document.title = dashboardResource.result.dashboard_title;
|
||||
if (dashboardResource.result.css) {
|
||||
// returning will clean up custom css
|
||||
// when dashboard unmounts or changes
|
||||
return injectCustomCss(dashboardResource.result.css);
|
||||
}
|
||||
}
|
||||
return () => {};
|
||||
}, [dashboardResource.result]);
|
||||
|
||||
const shouldBeHydrated =
|
||||
dashboardResource.status === ResourceStatus.COMPLETE &&
|
||||
chartsResource.status === ResourceStatus.COMPLETE &&
|
||||
datasetsResource.status === ResourceStatus.COMPLETE;
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldBeHydrated) {
|
||||
dispatch(
|
||||
hydrateDashboard(
|
||||
dashboardResource.result,
|
||||
chartsResource.result,
|
||||
datasetsResource.result,
|
||||
),
|
||||
);
|
||||
setHydrated(true);
|
||||
if (readyToRender) {
|
||||
dispatch(hydrateDashboard(dashboard, charts));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [shouldBeHydrated]);
|
||||
}, [readyToRender]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dashboard_title) {
|
||||
document.title = dashboard_title;
|
||||
}
|
||||
}, [dashboard_title]);
|
||||
|
||||
useEffect(() => {
|
||||
if (css) {
|
||||
// returning will clean up custom css
|
||||
// when dashboard unmounts or changes
|
||||
return injectCustomCss(css);
|
||||
}
|
||||
return () => {};
|
||||
}, [css]);
|
||||
|
||||
useEffect(() => {
|
||||
if (datasetsApiError) {
|
||||
addDangerToast(
|
||||
t('Error loading chart datasources. Filters may not work correctly.'),
|
||||
);
|
||||
} else {
|
||||
dispatch(setDatasources(datasets));
|
||||
}
|
||||
}, [addDangerToast, datasets, datasetsApiError, dispatch]);
|
||||
|
||||
if (error) throw error; // caught in error boundary
|
||||
if (!isHydrated) return <Loading />;
|
||||
if (!readyToRender) return <Loading />;
|
||||
|
||||
return <DashboardContainer />;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,30 +16,28 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { SET_DATASOURCE } from '../actions/datasources';
|
||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
||||
import { keyBy } from 'lodash';
|
||||
import { DatasourcesState } from 'src/dashboard/types';
|
||||
import {
|
||||
DatasourcesActionPayload,
|
||||
DatasourcesAction,
|
||||
} from '../actions/datasources';
|
||||
|
||||
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]();
|
||||
export default function datasourcesReducer(
|
||||
datasources: DatasourcesState | undefined,
|
||||
action: DatasourcesActionPayload,
|
||||
) {
|
||||
if (action.type === DatasourcesAction.SET_DATASOURCES) {
|
||||
return {
|
||||
...datasources,
|
||||
...keyBy(action.datasources, 'uid'),
|
||||
};
|
||||
}
|
||||
return datasources;
|
||||
if (action.type === DatasourcesAction.SET_DATASOURCE) {
|
||||
return {
|
||||
...datasources,
|
||||
[action.key]: action.datasource,
|
||||
};
|
||||
}
|
||||
return datasources || {};
|
||||
}
|
||||
@@ -25,10 +25,13 @@ import {
|
||||
import { DatasourceMeta } from '@superset-ui/chart-controls';
|
||||
import { chart } from 'src/chart/chartReducer';
|
||||
import componentTypes from 'src/dashboard/util/componentTypes';
|
||||
|
||||
import { DataMaskStateWithId } from '../dataMask/types';
|
||||
import { NativeFiltersState } from './reducers/types';
|
||||
import { ChartState } from '../explore/types';
|
||||
|
||||
export { Dashboard } from 'src/types/Dashboard';
|
||||
|
||||
export type ChartReducerInitialState = typeof chart;
|
||||
|
||||
// chart query built from initialState
|
||||
@@ -72,11 +75,14 @@ export type DashboardInfo = {
|
||||
};
|
||||
|
||||
export type ChartsState = { [key: string]: Chart };
|
||||
|
||||
export type Datasource = DatasourceMeta & {
|
||||
uid: string;
|
||||
column_types: GenericDataType[];
|
||||
table_name: string;
|
||||
};
|
||||
export type DatasourcesState = {
|
||||
[key: string]: DatasourceMeta & {
|
||||
column_types: GenericDataType[];
|
||||
table_name: string;
|
||||
};
|
||||
[key: string]: Datasource;
|
||||
};
|
||||
|
||||
/** Root state of redux */
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, useCallback, useState } from 'react';
|
||||
import { ControlType } from '@superset-ui/chart-controls';
|
||||
import { JsonValue, QueryFormData } from '@superset-ui/core';
|
||||
import ErrorBoundary from 'src/components/ErrorBoundary';
|
||||
@@ -43,53 +43,38 @@ export type ControlProps = {
|
||||
renderTrigger?: boolean;
|
||||
};
|
||||
|
||||
export default class Control extends React.PureComponent<
|
||||
ControlProps,
|
||||
{ hovered: boolean }
|
||||
> {
|
||||
onMouseEnter: () => void;
|
||||
export default function Control(props: ControlProps) {
|
||||
const {
|
||||
actions: { setControlValue },
|
||||
name,
|
||||
type,
|
||||
hidden,
|
||||
} = props;
|
||||
|
||||
onMouseLeave: () => void;
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const onChange = useCallback(
|
||||
(value: any, errors: any[]) => setControlValue(name, value, errors),
|
||||
[name, setControlValue],
|
||||
);
|
||||
|
||||
constructor(props: ControlProps) {
|
||||
super(props);
|
||||
this.state = { hovered: false };
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onMouseEnter = this.setHover.bind(this, true);
|
||||
this.onMouseLeave = this.setHover.bind(this, false);
|
||||
if (!type) return null;
|
||||
|
||||
const ControlComponent = typeof type === 'string' ? controlMap[type] : type;
|
||||
if (!ControlComponent) {
|
||||
return <>Unknown controlType: {type}</>;
|
||||
}
|
||||
|
||||
onChange(value: any, errors: any[]) {
|
||||
this.props.actions.setControlValue(this.props.name, value, errors);
|
||||
}
|
||||
|
||||
setHover(hovered: boolean) {
|
||||
this.setState({ hovered });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { type, hidden } = this.props;
|
||||
if (!type) return null;
|
||||
const ControlComponent = typeof type === 'string' ? controlMap[type] : type;
|
||||
if (!ControlComponent) {
|
||||
return `Unknown controlType: ${type}`;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="Control"
|
||||
data-test={this.props.name}
|
||||
style={hidden ? { display: 'none' } : undefined}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<ControlComponent
|
||||
onChange={this.onChange}
|
||||
hovered={this.state.hovered}
|
||||
{...this.props}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="Control"
|
||||
data-test={name}
|
||||
style={hidden ? { display: 'none' } : undefined}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<ControlComponent onChange={onChange} hovered={hovered} {...props} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import Owner from './Owner';
|
||||
import Role from './Role';
|
||||
|
||||
type Dashboard = {
|
||||
export interface Dashboard {
|
||||
id: number;
|
||||
slug?: string | null;
|
||||
url: string;
|
||||
@@ -35,6 +35,6 @@ type Dashboard = {
|
||||
charts: string[]; // just chart names, unfortunately...
|
||||
owners: Owner[];
|
||||
roles: Role[];
|
||||
};
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
Reference in New Issue
Block a user