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;

Reply via email to