This is an automated email from the ASF dual-hosted git repository. enzomartellucci pushed a commit to branch enxdev/refactor/typescript-migration-ChartRenderer in repository https://gitbox.apache.org/repos/asf/superset.git
commit 3f6e5110484176822618365de0123702061a63a6 Author: Enzo Martellucci <[email protected]> AuthorDate: Thu Feb 6 14:41:31 2025 +0100 wip(ChartRenderer): migrates ChartRenderer to Ts --- superset-frontend/src/components/Chart/Chart.tsx | 99 +++--- .../src/components/Chart/ChartRenderer.jsx | 381 --------------------- .../src/components/Chart/ChartRenderer.tsx | 367 ++++++++++++++++++++ 3 files changed, 418 insertions(+), 429 deletions(-) diff --git a/superset-frontend/src/components/Chart/Chart.tsx b/superset-frontend/src/components/Chart/Chart.tsx index 0389ebd309..b4a26675a8 100644 --- a/superset-frontend/src/components/Chart/Chart.tsx +++ b/superset-frontend/src/components/Chart/Chart.tsx @@ -17,22 +17,22 @@ * under the License. */ import { PureComponent } from 'react'; +import { Dispatch } from 'redux'; import { ensureIsArray, FeatureFlag, isFeatureEnabled, logging, + SqlaFormData, QueryFormData, styled, - ErrorTypeEnum, t, - SqlaFormData, ClientErrorObject, ChartDataResponse, } from '@superset-ui/core'; import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants'; import Loading from 'src/components/Loading'; -import { EmptyState } from 'src/components/EmptyState'; +import { EmptyStateBig } from 'src/components/EmptyState'; import ErrorBoundary from 'src/components/ErrorBoundary'; import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils'; import { URL_PARAMS } from 'src/constants'; @@ -40,12 +40,40 @@ import { getUrlParam } from 'src/utils/urlUtils'; import { isCurrentUserBot } from 'src/utils/isBot'; import { ChartSource } from 'src/types/ChartSource'; import { ResourceStatus } from 'src/hooks/apiResources/apiResources'; -import { Dispatch } from 'redux'; import { Annotation } from 'src/explore/components/controls/AnnotationLayerControl'; import ChartRenderer from './ChartRenderer'; import { ChartErrorMessage } from './ChartErrorMessage'; import { getChartRequiredFieldsMissingMessage } from '../../utils/getChartRequiredFieldsMissingMessage'; +export type Actions = { + logEvent( + LOG_ACTIONS_RENDER_CHART: string, + arg1: { + slice_id: string; + has_err: boolean; + error_details: string; + start_offset: number; + ts: number; + duration: number; + }, + ): Dispatch; + chartRenderingFailed( + arg0: string, + chartId: string, + arg2: string | null, + ): Dispatch; + postChartFormData( + formData: SqlaFormData, + arg1: boolean, + timeout: number | undefined, + chartId: string, + dashboardId: number | undefined, + ownState: boolean, + ): Dispatch; + chartRenderingSucceeded(arg0: { key: string }): Dispatch; + updateDataMask(chartId: string, dataMask: { dataMask: any }): Dispatch; +}; + export type ChartErrorType = Partial<ClientErrorObject>; export interface ChartProps { annotationData?: Annotation; @@ -68,14 +96,12 @@ export interface ChartProps { vizType: string; triggerRender?: boolean; force?: boolean; - isFiltersInitialized?: boolean; chartAlert?: string; chartStatus?: string; chartStackTrace?: string; queriesResponse: ChartDataResponse[]; triggerQuery?: boolean; chartIsStale?: boolean; - errorMessage?: React.ReactNode; addFilter?: (type: string) => void; onQuery?: () => void; onFilterMenuOpen?: (chartId: string, column: string) => void; @@ -83,36 +109,12 @@ export interface ChartProps { ownState: boolean; postTransformProps?: Function; datasetsStatus?: 'loading' | 'error' | 'complete'; - isInView?: boolean; emitCrossFilters?: boolean; + errorMessage?: React.ReactNode; + isInView?: boolean; + filters?: string | string[]; } -export type Actions = { - logEvent( - LOG_ACTIONS_RENDER_CHART: string, - arg1: { - slice_id: string; - has_err: boolean; - error_details: string; - start_offset: number; - ts: number; - duration: number; - }, - ): Dispatch; - chartRenderingFailed( - arg0: string, - chartId: string, - arg2: string | null, - ): Dispatch; - postChartFormData( - formData: SqlaFormData, - arg1: boolean, - timeout: number | undefined, - chartId: string, - dashboardId: number | undefined, - ownState: boolean, - ): Dispatch; -}; const BLANK = {}; const NONEXISTENT_DATASET = t( 'The dataset associated with this chart no longer exists', @@ -173,6 +175,12 @@ const MessageSpan = styled.span` color: ${({ theme }) => theme.colors.grayscale.base}; `; +const MonospaceDiv = styled.div` + font-family: ${({ theme }) => theme.typography.families.monospace}; + word-break: break-word; + overflow-x: auto; + white-space: pre-wrap; +`; class Chart extends PureComponent<ChartProps, {}> { static defaultProps = defaultProps; @@ -240,15 +248,7 @@ class Chart extends PureComponent<ChartProps, {}> { height, datasetsStatus, } = this.props; - let error = queryResponse?.errors?.[0]; - if (error === undefined) { - error = { - error_type: ErrorTypeEnum.FRONTEND_NETWORK_ERROR, - level: 'error', - message: t('Check your network connection'), - extra: null, - }; - } + const error = queryResponse?.errors?.[0]; const message = chartAlert || queryResponse?.message; // if datasource is still loading, don't render JS errors @@ -276,7 +276,8 @@ class Chart extends PureComponent<ChartProps, {}> { key={chartId} chartId={chartId} error={error} - subtitle={message} + subtitle={<MonospaceDiv>{message}</MonospaceDiv>} + copyText={message} link={queryResponse ? queryResponse.link : undefined} source={dashboardId ? ChartSource.Dashboard : ChartSource.Explore} stackTrace={chartStackTrace} @@ -305,7 +306,11 @@ class Chart extends PureComponent<ChartProps, {}> { isCurrentUserBot() ? ( <ChartRenderer {...this.props} - source={this.props.dashboardId ? 'dashboard' : 'explore'} + source={ + this.props.dashboardId + ? ChartSource.Dashboard + : ChartSource.Explore + } data-test={this.props.vizType} /> ) : ( @@ -339,8 +344,7 @@ class Chart extends PureComponent<ChartProps, {}> { if (errorMessage && ensureIsArray(queriesResponse).length === 0) { return ( - <EmptyState - size="large" + <EmptyStateBig title={t('Add required control values to preview chart')} description={getChartRequiredFieldsMissingMessage(true)} image="chart.svg" @@ -355,8 +359,7 @@ class Chart extends PureComponent<ChartProps, {}> { ensureIsArray(queriesResponse).length === 0 ) { return ( - <EmptyState - size="large" + <EmptyStateBig title={t('Your chart is ready to go!')} description={ <span> diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx deleted file mode 100644 index 25fc045d21..0000000000 --- a/superset-frontend/src/components/Chart/ChartRenderer.jsx +++ /dev/null @@ -1,381 +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 { snakeCase, isEqual, cloneDeep } from 'lodash'; -import PropTypes from 'prop-types'; -import { createRef, Component } from 'react'; -import { - SuperChart, - logging, - Behavior, - t, - getChartMetadataRegistry, - VizType, - isFeatureEnabled, - FeatureFlag, -} from '@superset-ui/core'; -import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils'; -import { EmptyState } from 'src/components/EmptyState'; -import { ChartSource } from 'src/types/ChartSource'; -import ChartContextMenu from './ChartContextMenu/ChartContextMenu'; - -const propTypes = { - annotationData: PropTypes.object, - actions: PropTypes.object, - chartId: PropTypes.number.isRequired, - datasource: PropTypes.object, - initialValues: PropTypes.object, - formData: PropTypes.object.isRequired, - latestQueryFormData: PropTypes.object, - labelsColor: PropTypes.object, - labelsColorMap: PropTypes.object, - height: PropTypes.number, - width: PropTypes.number, - setControlValue: PropTypes.func, - vizType: PropTypes.string.isRequired, - triggerRender: PropTypes.bool, - // state - chartAlert: PropTypes.string, - chartStatus: PropTypes.string, - queriesResponse: PropTypes.arrayOf(PropTypes.object), - triggerQuery: PropTypes.bool, - chartIsStale: PropTypes.bool, - // dashboard callbacks - addFilter: PropTypes.func, - setDataMask: PropTypes.func, - onFilterMenuOpen: PropTypes.func, - onFilterMenuClose: PropTypes.func, - ownState: PropTypes.object, - postTransformProps: PropTypes.func, - source: PropTypes.oneOf([ChartSource.Dashboard, ChartSource.Explore]), - emitCrossFilters: PropTypes.bool, -}; - -const BLANK = {}; - -const BIG_NO_RESULT_MIN_WIDTH = 300; -const BIG_NO_RESULT_MIN_HEIGHT = 220; - -const behaviors = [Behavior.InteractiveChart]; - -const defaultProps = { - addFilter: () => BLANK, - onFilterMenuOpen: () => BLANK, - onFilterMenuClose: () => BLANK, - initialValues: BLANK, - setControlValue() {}, - triggerRender: false, -}; - -class ChartRenderer extends Component { - constructor(props) { - super(props); - const suppressContextMenu = getChartMetadataRegistry().get( - props.formData.viz_type ?? props.vizType, - )?.suppressContextMenu; - this.state = { - showContextMenu: - props.source === ChartSource.Dashboard && - !suppressContextMenu && - isFeatureEnabled(FeatureFlag.DrillToDetail), - inContextMenu: false, - legendState: undefined, - }; - this.hasQueryResponseChange = false; - - this.contextMenuRef = createRef(); - - this.handleAddFilter = this.handleAddFilter.bind(this); - this.handleRenderSuccess = this.handleRenderSuccess.bind(this); - this.handleRenderFailure = this.handleRenderFailure.bind(this); - this.handleSetControlValue = this.handleSetControlValue.bind(this); - this.handleOnContextMenu = this.handleOnContextMenu.bind(this); - this.handleContextMenuSelected = this.handleContextMenuSelected.bind(this); - this.handleContextMenuClosed = this.handleContextMenuClosed.bind(this); - this.handleLegendStateChanged = this.handleLegendStateChanged.bind(this); - this.onContextMenuFallback = this.onContextMenuFallback.bind(this); - - this.hooks = { - onAddFilter: this.handleAddFilter, - onContextMenu: this.state.showContextMenu - ? this.handleOnContextMenu - : undefined, - onError: this.handleRenderFailure, - setControlValue: this.handleSetControlValue, - onFilterMenuOpen: this.props.onFilterMenuOpen, - onFilterMenuClose: this.props.onFilterMenuClose, - onLegendStateChanged: this.handleLegendStateChanged, - setDataMask: dataMask => { - this.props.actions?.updateDataMask(this.props.chartId, dataMask); - }, - }; - - // TODO: queriesResponse comes from Redux store but it's being edited by - // the plugins, hence we need to clone it to avoid state mutation - // until we change the reducers to use Redux Toolkit with Immer - this.mutableQueriesResponse = cloneDeep(this.props.queriesResponse); - } - - shouldComponentUpdate(nextProps, nextState) { - const resultsReady = - nextProps.queriesResponse && - ['success', 'rendered'].indexOf(nextProps.chartStatus) > -1 && - !nextProps.queriesResponse?.[0]?.error; - - if (resultsReady) { - if (!isEqual(this.state, nextState)) { - return true; - } - this.hasQueryResponseChange = - nextProps.queriesResponse !== this.props.queriesResponse; - - if (this.hasQueryResponseChange) { - this.mutableQueriesResponse = cloneDeep(nextProps.queriesResponse); - } - - return ( - this.hasQueryResponseChange || - !isEqual(nextProps.datasource, this.props.datasource) || - nextProps.annotationData !== this.props.annotationData || - nextProps.ownState !== this.props.ownState || - nextProps.filterState !== this.props.filterState || - nextProps.height !== this.props.height || - nextProps.width !== this.props.width || - nextProps.triggerRender || - nextProps.labelsColor !== this.props.labelsColor || - nextProps.labelsColorMap !== this.props.labelsColorMap || - nextProps.formData.color_scheme !== this.props.formData.color_scheme || - nextProps.formData.stack !== this.props.formData.stack || - nextProps.cacheBusterProp !== this.props.cacheBusterProp || - nextProps.emitCrossFilters !== this.props.emitCrossFilters - ); - } - return false; - } - - handleAddFilter(col, vals, merge = true, refresh = true) { - this.props.addFilter(col, vals, merge, refresh); - } - - handleRenderSuccess() { - const { actions, chartStatus, chartId, vizType } = this.props; - if (['loading', 'rendered'].indexOf(chartStatus) < 0) { - actions.chartRenderingSucceeded(chartId); - } - - // only log chart render time which is triggered by query results change - // currently we don't log chart re-render time, like window resize etc - if (this.hasQueryResponseChange) { - actions.logEvent(LOG_ACTIONS_RENDER_CHART, { - slice_id: chartId, - viz_type: vizType, - start_offset: this.renderStartTime, - ts: new Date().getTime(), - duration: Logger.getTimestamp() - this.renderStartTime, - }); - } - } - - handleRenderFailure(error, info) { - const { actions, chartId } = this.props; - logging.warn(error); - actions.chartRenderingFailed( - error.toString(), - chartId, - info ? info.componentStack : null, - ); - - // only trigger render log when query is changed - if (this.hasQueryResponseChange) { - actions.logEvent(LOG_ACTIONS_RENDER_CHART, { - slice_id: chartId, - has_err: true, - error_details: error.toString(), - start_offset: this.renderStartTime, - ts: new Date().getTime(), - duration: Logger.getTimestamp() - this.renderStartTime, - }); - } - } - - handleSetControlValue(...args) { - const { setControlValue } = this.props; - if (setControlValue) { - setControlValue(...args); - } - } - - handleOnContextMenu(offsetX, offsetY, filters) { - this.contextMenuRef.current.open(offsetX, offsetY, filters); - this.setState({ inContextMenu: true }); - } - - handleContextMenuSelected() { - this.setState({ inContextMenu: false }); - } - - handleContextMenuClosed() { - this.setState({ inContextMenu: false }); - } - - handleLegendStateChanged(legendState) { - this.setState({ legendState }); - } - - // When viz plugins don't handle `contextmenu` event, fallback handler - // calls `handleOnContextMenu` with no `filters` param. - onContextMenuFallback(event) { - if (!this.state.inContextMenu) { - event.preventDefault(); - this.handleOnContextMenu(event.clientX, event.clientY); - } - } - - render() { - const { chartAlert, chartStatus, chartId, emitCrossFilters } = this.props; - - // Skip chart rendering - if (chartStatus === 'loading' || !!chartAlert || chartStatus === null) { - return null; - } - - this.renderStartTime = Logger.getTimestamp(); - - const { - width, - height, - datasource, - annotationData, - initialValues, - ownState, - filterState, - chartIsStale, - formData, - latestQueryFormData, - postTransformProps, - } = this.props; - - const currentFormData = - chartIsStale && latestQueryFormData ? latestQueryFormData : formData; - const vizType = currentFormData.viz_type || this.props.vizType; - - // It's bad practice to use unprefixed `vizType` as classnames for chart - // container. It may cause css conflicts as in the case of legacy table chart. - // When migrating charts, we should gradually add a `superset-chart-` prefix - // to each one of them. - const snakeCaseVizType = snakeCase(vizType); - const chartClassName = - vizType === VizType.Table - ? `superset-chart-${snakeCaseVizType}` - : snakeCaseVizType; - - const webpackHash = - process.env.WEBPACK_MODE === 'development' - ? `-${ - // eslint-disable-next-line camelcase - typeof __webpack_require__ !== 'undefined' && - // eslint-disable-next-line camelcase, no-undef - typeof __webpack_require__.h === 'function' && - // eslint-disable-next-line no-undef, camelcase - __webpack_require__.h() - }` - : ''; - - let noResultsComponent; - const noResultTitle = t('No results were returned for this query'); - const noResultDescription = - this.props.source === ChartSource.Explore - ? t( - 'Make sure that the controls are configured properly and the datasource contains data for the selected time range', - ) - : undefined; - const noResultImage = 'chart.svg'; - if (width > BIG_NO_RESULT_MIN_WIDTH && height > BIG_NO_RESULT_MIN_HEIGHT) { - noResultsComponent = ( - <EmptyState - size="large" - title={noResultTitle} - description={noResultDescription} - image={noResultImage} - /> - ); - } else { - noResultsComponent = ( - <EmptyState size="small" title={noResultTitle} image={noResultImage} /> - ); - } - - // Check for Behavior.DRILL_TO_DETAIL to tell if chart can receive Drill to - // Detail props or if it'll cause side-effects (e.g. excessive re-renders). - const drillToDetailProps = getChartMetadataRegistry() - .get(formData.viz_type) - ?.behaviors.find(behavior => behavior === Behavior.DrillToDetail) - ? { inContextMenu: this.state.inContextMenu } - : {}; - - return ( - <> - {this.state.showContextMenu && ( - <ChartContextMenu - ref={this.contextMenuRef} - id={chartId} - formData={currentFormData} - onSelection={this.handleContextMenuSelected} - onClose={this.handleContextMenuClosed} - /> - )} - <div - onContextMenu={ - this.state.showContextMenu ? this.onContextMenuFallback : undefined - } - > - <SuperChart - disableErrorBoundary - key={`${chartId}${webpackHash}`} - id={`chart-id-${chartId}`} - className={chartClassName} - chartType={vizType} - width={width} - height={height} - annotationData={annotationData} - datasource={datasource} - initialValues={initialValues} - formData={currentFormData} - ownState={ownState} - filterState={filterState} - hooks={this.hooks} - behaviors={behaviors} - queriesData={this.mutableQueriesResponse} - onRenderSuccess={this.handleRenderSuccess} - onRenderFailure={this.handleRenderFailure} - noResults={noResultsComponent} - postTransformProps={postTransformProps} - emitCrossFilters={emitCrossFilters} - legendState={this.state.legendState} - {...drillToDetailProps} - /> - </div> - </> - ); - } -} - -ChartRenderer.propTypes = propTypes; -ChartRenderer.defaultProps = defaultProps; - -export default ChartRenderer; diff --git a/superset-frontend/src/components/Chart/ChartRenderer.tsx b/superset-frontend/src/components/Chart/ChartRenderer.tsx new file mode 100644 index 0000000000..4079c31311 --- /dev/null +++ b/superset-frontend/src/components/Chart/ChartRenderer.tsx @@ -0,0 +1,367 @@ +/** + * 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 { snakeCase, cloneDeep } from 'lodash'; +import { useState, useRef, useCallback, useEffect, useMemo } from 'react'; +import { + SuperChart, + logging, + Behavior, + t, + isFeatureEnabled, + FeatureFlag, + getChartMetadataRegistry, + QueryFormData, + ChartDataResponse, + VizType as enumVizType, + JsonObject, + FilterState, +} from '@superset-ui/core'; +import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils'; +import { EmptyState } from 'src/components/EmptyState'; +import { ChartSource } from 'src/types/ChartSource'; +import { Annotation } from 'src/explore/components/controls/AnnotationLayerControl'; +import ChartContextMenu from './ChartContextMenu/ChartContextMenu'; +import { Actions } from './Chart'; + +type ChartRendererProps = { + dashboardId?: number; + latestQueryFormData?: QueryFormData; + labelsColorMap?: string; + setDataMask?: (dataMask: any) => void; + source?: ChartSource; + annotationData?: Annotation; + actions: Actions; + chartId: string; + datasource?: { + database?: { + name: string; + }; + }; + initialValues?: object; + formData: QueryFormData; + labelsColor?: string; + height?: number; + width?: number; + setControlValue?: Function; + vizType: string; + triggerRender?: boolean; + chartAlert?: string; + chartStatus?: string; + queriesResponse?: ChartDataResponse[]; + triggerQuery?: boolean; + chartIsStale?: boolean; + filterState?: FilterState[]; + addFilter?: ( + col: string, + vals: string | string[], + merge: boolean, + refresh: boolean, + ) => void; + onFilterMenuOpen?: (chartId: string, column: string) => void; + onFilterMenuClose?: (chartId: string, column: string) => void; + ownState?: boolean | JsonObject; + postTransformProps?: Function; + emitCrossFilters?: boolean; +}; + +const BLANK = {}; +const BIG_NO_RESULT_MIN_WIDTH = 300; +const BIG_NO_RESULT_MIN_HEIGHT = 220; +const behaviors = [Behavior.InteractiveChart]; + +const ChartRenderer = (props: ChartRendererProps) => { + const { + annotationData, + actions, + chartId, + datasource, + initialValues = BLANK, + formData, + latestQueryFormData, + height, + width, + setControlValue, + vizType, + chartStatus, + queriesResponse = [], + chartIsStale, + chartAlert, + addFilter = () => BLANK, + setDataMask, + onFilterMenuOpen = () => BLANK, + onFilterMenuClose = () => BLANK, + ownState, + filterState, + postTransformProps, + source, + } = props; + + const hasQueryResponseChange = false; + const suppressContextMenu = getChartMetadataRegistry().get( + formData.viz_type ?? vizType, + )?.suppressContextMenu; + + const [showContextMenu, setShowContextMenu] = useState<Boolean>(false); + const [inContextMenu, setInContextMenu] = useState(false); + const [legendState, setLegendState] = useState<any>(undefined); + const contextMenuRef = useRef<any>(null); + const mutableQueriesResponse = useRef(cloneDeep(queriesResponse)); + const renderStartTime = useRef<number>(0); + + if (chartStatus === 'loading' || !!chartAlert || chartStatus === null) { + return null; + } + + useEffect(() => { + mutableQueriesResponse.current = cloneDeep(queriesResponse); + }, [queriesResponse]); + + useEffect(() => { + const shouldShowContextMenu = + source === ChartSource.Dashboard && + !suppressContextMenu && + isFeatureEnabled(FeatureFlag.DrillToDetail); + + setShowContextMenu(shouldShowContextMenu); + }, [source, suppressContextMenu]); + + // only log chart render time which is triggered by query results change + // currently we don't log chart re-render time, like window resize etc + if (hasQueryResponseChange) { + actions.logEvent(LOG_ACTIONS_RENDER_CHART, { + slice_id: chartId, + has_err: false, + error_details: '', + start_offset: renderStartTime.current, + ts: new Date().getTime(), + duration: Logger.getTimestamp() - renderStartTime.current, + }); + } + const handleAddFilter = ( + col: string, + vals: string | string[], + merge = true, + refresh = true, + ) => { + alert(col); + console.log(col, vals, merge, refresh); + addFilter(col, vals, merge, refresh); + }; + + const handleOnContextMenu = ( + offsetX: number, + offsetY: number, + filters: undefined, + ) => { + contextMenuRef.current.open(offsetX, offsetY, filters); + // setInContextMenu({ inContextMenu: true }); + setInContextMenu(true); + }; + + const handleSetControlValue = (...args: string[]) => { + setControlValue; + if (setControlValue) { + setControlValue(...args); + } + }; + + const handleRenderFailure = ( + error: { toString: () => string }, + info: { componentStack: string } | null, + ) => { + logging.warn(error); + actions.chartRenderingFailed( + error.toString(), + chartId, + info ? info.componentStack : null, + ); + + // only trigger render log when query is changed + if (hasQueryResponseChange) { + actions.logEvent(LOG_ACTIONS_RENDER_CHART, { + slice_id: chartId, + has_err: true, + error_details: error.toString(), + start_offset: renderStartTime.current, + ts: new Date().getTime(), + duration: Logger.getTimestamp() - renderStartTime.current, + }); + } + }; + + const handleRenderSuccess = useCallback(() => { + if (!['loading', 'rendered'].includes(chartStatus || '')) { + actions.chartRenderingSucceeded({ key: chartId }); + } + actions.logEvent(LOG_ACTIONS_RENDER_CHART, { + slice_id: chartId, + has_err: false, + error_details: '', + start_offset: renderStartTime.current, + ts: new Date().getTime(), + duration: Logger.getTimestamp() - renderStartTime.current, + }); + }, [actions, chartId, chartStatus, vizType]); + + // renderStartTime.current = Logger.getTimestamp(); + + const currentFormData = + chartIsStale && latestQueryFormData ? latestQueryFormData : formData; + + const snakeCaseVizType = snakeCase(currentFormData.viz_type || vizType); + + const chartClassName = + vizType === enumVizType.Table + ? `superset-chart-${snakeCaseVizType}` + : snakeCaseVizType; + + const webpackHash = + process.env.WEBPACK_MODE === 'development' + ? `-${ + // eslint-disable-next-line camelcase + // @ts-ignore + typeof __webpack_require__ !== 'undefined' && + // @ts-ignore + typeof __webpack_require__.h === 'function' && + // eslint-disable-next-line camelcase, no-undef + // @ts-ignore + typeof __webpack_require__.h === 'function' && + // eslint-disable-next-line no-undef, camelcase + // @ts-ignore + __webpack_require__.h() + }` + : ''; + + let noResultsComponent; + const noResultTitle = t('No results were returned for this query'); + const noResultDescription = + source === ChartSource.Explore + ? t( + 'Make sure that the controls are configured properly and the datasource contains data for the selected time range', + ) + : undefined; + const noResultImage = 'chart.svg'; + + if ( + typeof width === 'number' && + typeof height === 'number' && + width > BIG_NO_RESULT_MIN_WIDTH && + height > BIG_NO_RESULT_MIN_HEIGHT + ) { + noResultsComponent = ( + <EmptyState + size="large" + title={noResultTitle} + description={noResultDescription} + image={noResultImage} + /> + ); + } else { + noResultsComponent = ( + <EmptyState size="small" title={noResultTitle} image={noResultImage} /> + ); + } + + // Check for Behavior.DRILL_TO_DETAIL to tell if chart can receive Drill to + // Detail props or if it'll cause side-effects (e.g. excessive re-renders). + const drillToDetailProps = getChartMetadataRegistry() + .get(formData.viz_type) + ?.behaviors.find(behavior => behavior === Behavior.DrillToDetail) + ? { inContextMenu } + : {}; + + const hooks = useMemo( + () => ({ + onAddFilter: handleAddFilter, + onContextMenu: showContextMenu ? handleOnContextMenu : undefined, + onError: handleRenderFailure, + setControlValue: handleSetControlValue, + onFilterMenuOpen, + onFilterMenuClose, + onLegendChange: setLegendState, + setDataMask: (dataMask: any) => + actions?.updateDataMask(chartId, { dataMask }), + }), + [ + handleAddFilter, + showContextMenu, + handleOnContextMenu, + handleRenderFailure, + handleSetControlValue, + onFilterMenuOpen, + onFilterMenuClose, + setLegendState, + legendState, + setDataMask, + chartId, + ], + ); + + return ( + <> + {showContextMenu && ( + <ChartContextMenu + ref={contextMenuRef} + id={chartId as unknown as number} + formData={currentFormData} + onSelection={() => setInContextMenu(false)} + onClose={() => setInContextMenu(false)} + /> + )} + <div + onContextMenu={ + showContextMenu + ? event => { + event.preventDefault(); + handleOnContextMenu(event.clientX, event.clientY, undefined); + } + : undefined + } + > + <SuperChart + disableErrorBoundary + key={`${chartId}${webpackHash}`} + id={`chart-id-${chartId}`} + className={chartClassName} + chartType={vizType} + width={width} + height={height} + annotationData={annotationData} + datasource={datasource} + initialValues={initialValues} + formData={currentFormData} + ownState={ownState} + filterState={filterState} + hooks={hooks} + behaviors={behaviors} + queriesData={mutableQueriesResponse.current} + onRenderSuccess={handleRenderSuccess} + onRenderFailure={handleRenderFailure} + noResults={noResultsComponent} + postTransformProps={postTransformProps} + // emitCrossFilters={emitCrossFilters} + legendState={legendState} + {...drillToDetailProps} + /> + </div> + </> + ); +}; + +export default ChartRenderer;
