diff --git a/superset-frontend/src/chart/Chart.jsx b/superset-frontend/src/chart/Chart.jsx index d9f7e93ccf7..d056fe9dbe1 100644 --- a/superset-frontend/src/chart/Chart.jsx +++ b/superset-frontend/src/chart/Chart.jsx @@ -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 diff --git a/superset-frontend/src/chart/ChartRenderer.jsx b/superset-frontend/src/chart/ChartRenderer.jsx index f40e46e83cb..fbf05b714f0 100644 --- a/superset-frontend/src/chart/ChartRenderer.jsx +++ b/superset-frontend/src/chart/ChartRenderer.jsx @@ -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} diff --git a/superset-frontend/src/common/hooks/apiResources/dashboards.ts b/superset-frontend/src/common/hooks/apiResources/dashboards.ts index 553049aac6a..99707c19f6f 100644 --- a/superset-frontend/src/common/hooks/apiResources/dashboards.ts +++ b/superset-frontend/src/common/hooks/apiResources/dashboards.ts @@ -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(`/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(`/api/v1/dashboard/${idOrSlug}/datasets`); diff --git a/superset-frontend/src/dashboard/actions/datasources.js b/superset-frontend/src/dashboard/actions/datasources.ts similarity index 53% rename from superset-frontend/src/dashboard/actions/datasources.js rename to superset-frontend/src/dashboard/actions/datasources.ts index 4277edc661f..42004272ccb 100644 --- a/superset-frontend/src/dashboard/actions/datasources.js +++ b/superset-frontend/src/dashboard/actions/datasources.ts @@ -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))); }; } diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 21b2ffa48da..8eebbba2383 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -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 diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index e0a8cc5c895..cbeaa66fe27 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -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; } diff --git a/superset-frontend/src/dashboard/containers/Dashboard.ts b/superset-frontend/src/dashboard/containers/Dashboard.ts index 398dff7f29a..ccca038dfdf 100644 --- a/superset-frontend/src/dashboard/containers/Dashboard.ts +++ b/superset-frontend/src/dashboard/containers/Dashboard.ts @@ -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, diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx index e5897d18c46..cc39b3b212f 100644 --- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx +++ b/superset-frontend/src/dashboard/containers/DashboardPage.tsx @@ -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 ; + if (!readyToRender) return ; + return ; }; diff --git a/superset-frontend/src/dashboard/reducers/datasources.js b/superset-frontend/src/dashboard/reducers/datasources.ts similarity index 55% rename from superset-frontend/src/dashboard/reducers/datasources.js rename to superset-frontend/src/dashboard/reducers/datasources.ts index 616c3c134ff..864641645ed 100644 --- a/superset-frontend/src/dashboard/reducers/datasources.js +++ b/superset-frontend/src/dashboard/reducers/datasources.ts @@ -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 || {}; } diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index 6ebbe37dfe2..c2b316b9549 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -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 */ diff --git a/superset-frontend/src/explore/components/Control.tsx b/superset-frontend/src/explore/components/Control.tsx index eb53337dce4..04c1267974c 100644 --- a/superset-frontend/src/explore/components/Control.tsx +++ b/superset-frontend/src/explore/components/Control.tsx @@ -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 ( -
- - - -
- ); - } + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + + + +
+ ); } diff --git a/superset-frontend/src/types/Dashboard.ts b/superset-frontend/src/types/Dashboard.ts index 14de6d110c4..96a92d567f1 100644 --- a/superset-frontend/src/types/Dashboard.ts +++ b/superset-frontend/src/types/Dashboard.ts @@ -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;