Compare commits

...

4 Commits

Author SHA1 Message Date
Enzo Martellucci
bec3d94a5b Merge branch master into enxdev/refactor/typescript-migration-ChartRenderer 2025-03-15 17:07:39 +01:00
Enzo Martellucci
c805c96f5a Merge branch 'master' into enxdev/refactor/typescript-migration-ChartRenderer 2025-02-25 09:29:36 +01:00
Enzo Martellucci
a3ec4080e6 wip(ChartRenderer) 2025-02-10 11:01:19 +01:00
Enzo Martellucci
3f6e511048 wip(ChartRenderer): migrates ChartRenderer to Ts 2025-02-06 14:41:31 +01:00
3 changed files with 477 additions and 428 deletions

View File

@@ -17,15 +17,16 @@
* under the License.
*/
import { PureComponent } from 'react';
import { Dispatch } from 'redux';
import {
ensureIsArray,
FeatureFlag,
isFeatureEnabled,
logging,
SqlaFormData,
QueryFormData,
styled,
t,
SqlaFormData,
ClientErrorObject,
ChartDataResponse,
} from '@superset-ui/core';
@@ -39,53 +40,11 @@ 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 ChartErrorType = Partial<ClientErrorObject>;
export interface ChartProps {
annotationData?: Annotation;
actions: Actions;
chartId: string;
datasource?: {
database?: {
name: string;
};
};
dashboardId?: number;
initialValues?: object;
formData: QueryFormData;
labelColors?: string;
sharedLabelColors?: string;
width: number;
height: number;
setControlValue: Function;
timeout?: number;
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;
onFilterMenuClose?: (chartId: string, column: string) => void;
ownState: boolean;
postTransformProps?: Function;
datasetsStatus?: 'loading' | 'error' | 'complete';
isInView?: boolean;
emitCrossFilters?: boolean;
}
export type Actions = {
logEvent(
LOG_ACTIONS_RENDER_CHART: string,
@@ -111,7 +70,51 @@ export type Actions = {
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;
actions: Actions;
chartId: string;
datasource?: {
database?: {
name: string;
};
};
dashboardId?: number;
initialValues?: object;
formData: QueryFormData;
labelColors?: string;
sharedLabelColors?: string;
width: number;
height: number;
setControlValue: Function;
timeout?: number;
vizType: string;
triggerRender?: boolean;
force?: boolean;
chartAlert?: string;
chartStatus?: string;
chartStackTrace?: string;
queriesResponse: ChartDataResponse[];
triggerQuery?: boolean;
chartIsStale?: boolean;
addFilter?: (type: string) => void;
onQuery?: () => void;
onFilterMenuOpen?: (chartId: string, column: string) => void;
onFilterMenuClose?: (chartId: string, column: string) => void;
ownState: boolean;
postTransformProps?: Function;
datasetsStatus?: 'loading' | 'error' | 'complete';
emitCrossFilters?: boolean;
errorMessage?: React.ReactNode;
isInView?: boolean;
filters?: string | string[];
}
const BLANK = {};
const NONEXISTENT_DATASET = t(
'The dataset associated with this chart no longer exists',
@@ -172,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;
@@ -267,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}
@@ -296,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}
/>
) : (
@@ -331,7 +345,6 @@ class Chart extends PureComponent<ChartProps, {}> {
if (errorMessage && ensureIsArray(queriesResponse).length === 0) {
return (
<EmptyState
size="large"
title={t('Add required control values to preview chart')}
description={getChartRequiredFieldsMissingMessage(true)}
image="chart.svg"
@@ -347,7 +360,6 @@ class Chart extends PureComponent<ChartProps, {}> {
) {
return (
<EmptyState
size="large"
title={t('Your chart is ready to go!')}
description={
<span>

View File

@@ -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;

View File

@@ -0,0 +1,418 @@
/**
* 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, isEqual } 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,
emitCrossFilters,
triggerRender,
labelsColor,
labelsColorMap,
} = props;
if (chartStatus === 'loading' || !!chartAlert || chartStatus === null) {
return null;
}
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 prevProps = useRef(props);
const mutableQueriesResponse = useRef(cloneDeep(queriesResponse));
const [hasQueryResponseChange, setHasQueryResponseChange] = useState(false);
const renderStartTime = useRef<number>(0);
const resultsReady = useMemo(
() =>
queriesResponse &&
['success', 'rendered'].includes(chartStatus) &&
!queriesResponse?.[0]?.error,
[queriesResponse, chartStatus],
);
const queryResponseChanged = useMemo(
() => queriesResponse !== prevProps.current.queriesResponse,
[queriesResponse],
);
const shouldRender = useMemo(
() =>
resultsReady &&
(queryResponseChanged ||
!isEqual(datasource, prevProps.current.datasource) ||
annotationData !== prevProps.current.annotationData ||
ownState !== prevProps.current.ownState ||
filterState !== prevProps.current.filterState ||
height !== prevProps.current.height ||
width !== prevProps.current.width ||
triggerRender ||
labelsColor !== prevProps.current.labelsColor ||
labelsColorMap !== prevProps.current.labelsColorMap ||
formData.color_scheme !== prevProps.current.formData.color_scheme ||
formData.stack !== prevProps.current.formData.stack ||
emitCrossFilters !== prevProps.current.emitCrossFilters),
[resultsReady, queryResponseChanged, props],
);
if (!shouldRender) {
return null;
}
useEffect(() => {
if (queryResponseChanged) {
setHasQueryResponseChange(true);
mutableQueriesResponse.current = cloneDeep(queriesResponse);
}
}, [queryResponseChanged, queriesResponse]);
useEffect(() => {
prevProps.current = props;
}, [props]);
useEffect(() => {
mutableQueriesResponse.current = cloneDeep(queriesResponse);
}, [queriesResponse]);
useEffect(() => {
setShowContextMenu(
source === ChartSource.Dashboard &&
!suppressContextMenu &&
isFeatureEnabled(FeatureFlag.DrillToDetail),
);
}, [source, suppressContextMenu]);
useEffect(() => {
// 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,
});
}
}, [hasQueryResponseChange]);
/**
* Hooks region
*/
const handleAddFilter = useCallback(
(col: string, vals: string | string[], merge = true, refresh = true) => {
alert(col);
console.log(col, vals, merge, refresh);
addFilter(col, vals, merge, refresh);
},
[],
);
const handleOnContextMenu = useCallback(
(offsetX: number, offsetY: number, filters: undefined) => {
if (contextMenuRef.current) {
contextMenuRef.current.open(offsetX, offsetY, filters);
}
// setInContextMenu({ inContextMenu: true });
// setInContextMenu(true);
},
[],
);
const handleSetControlValue = useCallback(
(...args: string[]) => {
if (setControlValue) {
setControlValue(...args);
}
},
[setControlValue],
);
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]);
// end Hooks region
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,
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;