mirror of
https://github.com/apache/superset.git
synced 2026-04-28 12:34:23 +00:00
Compare commits
4 Commits
upgrade-sq
...
enxdev/ref
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bec3d94a5b | ||
|
|
c805c96f5a | ||
|
|
a3ec4080e6 | ||
|
|
3f6e511048 |
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
418
superset-frontend/src/components/Chart/ChartRenderer.tsx
Normal file
418
superset-frontend/src/components/Chart/ChartRenderer.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user