You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/06/05 17:58:51 UTC
(superset) branch master updated: chore(lint): convert ChartRenderer, Chart, DrillByChart to function components (#39459)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 550c80f640a chore(lint): convert ChartRenderer, Chart, DrillByChart to function components (#39459)
550c80f640a is described below
commit 550c80f640a7fad2fcbdc62cac65085cb24063ff
Author: Evan Rusackas <ev...@preset.io>
AuthorDate: Fri Jun 5 10:58:44 2026 -0700
chore(lint): convert ChartRenderer, Chart, DrillByChart to function components (#39459)
Co-authored-by: Claude Sonnet 4.6 <no...@anthropic.com>
Co-authored-by: Claude <cl...@anthropic.com>
---
superset-frontend/src/components/Chart/Chart.tsx | 506 +++++++-------
.../src/components/Chart/ChartRenderer.test.tsx | 83 +--
.../src/components/Chart/ChartRenderer.tsx | 732 ++++++++++-----------
.../src/components/Chart/DrillBy/DrillByChart.tsx | 4 +-
4 files changed, 665 insertions(+), 660 deletions(-)
diff --git a/superset-frontend/src/components/Chart/Chart.tsx b/superset-frontend/src/components/Chart/Chart.tsx
index 5f058aa6024..1519837c747 100644
--- a/superset-frontend/src/components/Chart/Chart.tsx
+++ b/superset-frontend/src/components/Chart/Chart.tsx
@@ -16,9 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { ErrorInfo, PureComponent } from 'react';
-import { logging } from '@apache-superset/core/utils';
+import { ErrorInfo, useCallback, useEffect, useRef } from 'react';
import { t } from '@apache-superset/core/translation';
+import { logging } from '@apache-superset/core/utils';
import {
ensureIsArray,
FeatureFlag,
@@ -60,7 +60,7 @@ export interface ChartProps {
sharedLabelColors?: string;
width: number;
height: number;
- setControlValue: (name: string, value: unknown) => void;
+ setControlValue?: (name: string, value: unknown) => void;
timeout?: number;
vizType: string;
triggerRender?: boolean;
@@ -69,7 +69,7 @@ export interface ChartProps {
chartAlert?: string;
chartStatus?: ChartStatus;
chartStackTrace?: string;
- queriesResponse: ChartState['queriesResponse'];
+ queriesResponse?: ChartState['queriesResponse'];
latestQueryFormData?: ChartState['latestQueryFormData'];
triggerQuery?: boolean;
chartIsStale?: boolean;
@@ -126,19 +126,6 @@ const NONEXISTENT_DATASET = t(
'The dataset associated with this chart no longer exists',
);
-const defaultProps: Partial<ChartProps> = {
- addFilter: () => BLANK,
- onFilterMenuOpen: () => BLANK,
- onFilterMenuClose: () => BLANK,
- initialValues: BLANK,
- setControlValue: () => BLANK,
- triggerRender: false,
- dashboardId: undefined,
- chartStackTrace: undefined,
- force: false,
- isInView: true,
-};
-
const Styles = styled.div<{ height: number; width?: number }>`
min-height: ${p => p.height}px;
position: relative;
@@ -186,252 +173,321 @@ const MessageSpan = styled.span`
color: ${({ theme }) => theme.colorText};
`;
-class Chart extends PureComponent<ChartProps, {}> {
- static defaultProps = defaultProps;
-
- renderStartTime: number;
-
- constructor(props: ChartProps) {
- super(props);
- this.renderStartTime = Logger.getTimestamp();
- this.handleRenderContainerFailure =
- this.handleRenderContainerFailure.bind(this);
- }
-
- componentDidMount() {
- if (this.props.triggerQuery) {
- this.runQuery();
- }
- }
-
- componentDidUpdate() {
- if (this.props.triggerQuery) {
- this.runQuery();
- }
- }
-
- shouldRenderChart() {
- return (
- this.props.isInView ||
+function Chart({
+ addFilter = () => BLANK,
+ onFilterMenuOpen = () => BLANK,
+ onFilterMenuClose = () => BLANK,
+ initialValues = BLANK,
+ setControlValue = () => BLANK,
+ triggerRender = false,
+ dashboardId,
+ chartStackTrace,
+ force = false,
+ isInView = true,
+ ...restProps
+}: ChartProps): JSX.Element {
+ const {
+ actions,
+ chartId,
+ datasource,
+ formData,
+ timeout,
+ ownState,
+ chartAlert,
+ chartStatus,
+ queriesResponse = [],
+ errorMessage,
+ chartIsStale,
+ width,
+ height,
+ datasetsStatus,
+ onQuery,
+ annotationData,
+ vizType,
+ latestQueryFormData,
+ triggerQuery,
+ postTransformProps,
+ emitCrossFilters,
+ onChartStateChange,
+ suppressLoadingSpinner,
+ filterState,
+ } = restProps;
+
+ const renderStartTimeRef = useRef<number>(Logger.getTimestamp());
+ // Update on each render to accurately track render duration
+ renderStartTimeRef.current = Logger.getTimestamp();
+
+ const shouldRenderChart = useCallback(
+ () =>
+ isInView ||
!isFeatureEnabled(FeatureFlag.DashboardVirtualization) ||
- isCurrentUserBot()
- );
- }
+ isCurrentUserBot(),
+ [isInView],
+ );
- runQuery() {
+ const runQuery = useCallback(() => {
if (
isFeatureEnabled(FeatureFlag.DashboardVirtualizationDeferData) &&
- !this.shouldRenderChart()
+ !shouldRenderChart()
) {
return;
}
// Create chart with POST request
- this.props.actions.postChartFormData(
- this.props.formData,
- Boolean(this.props.force || getUrlParam(URL_PARAMS.force)), // allow override via url params force=true
- this.props.timeout,
- this.props.chartId,
- this.props.dashboardId,
- this.props.ownState,
- );
- }
-
- handleRenderContainerFailure(error: Error, info: ErrorInfo) {
- const { actions, chartId } = this.props;
- logging.warn(error);
- actions.chartRenderingFailed(
- error.toString(),
+ actions.postChartFormData(
+ formData,
+ Boolean(force || getUrlParam(URL_PARAMS.force)), // allow override via url params force=true
+ timeout,
chartId,
- info?.componentStack ?? null,
+ dashboardId,
+ ownState,
);
+ }, [
+ actions,
+ chartId,
+ dashboardId,
+ formData,
+ force,
+ ownState,
+ shouldRenderChart,
+ timeout,
+ ]);
+
+ const handleRenderContainerFailure = useCallback(
+ (error: Error, info: ErrorInfo) => {
+ logging.warn(error);
+ actions.chartRenderingFailed(
+ error.toString(),
+ chartId,
+ info?.componentStack ?? null,
+ );
- 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,
- });
- }
+ actions.logEvent(LOG_ACTIONS_RENDER_CHART, {
+ slice_id: chartId,
+ has_err: true,
+ error_details: error.toString(),
+ start_offset: renderStartTimeRef.current,
+ ts: new Date().getTime(),
+ duration: Logger.getTimestamp() - renderStartTimeRef.current,
+ });
+ },
+ [actions, chartId],
+ );
- renderErrorMessage(queryResponse: ChartErrorType) {
- const {
- chartId,
+ // componentDidMount and componentDidUpdate combined
+ useEffect(() => {
+ if (triggerQuery) {
+ runQuery();
+ }
+ }, [triggerQuery, runQuery]);
+
+ const renderErrorMessage = useCallback(
+ (queryResponse: ChartErrorType) => {
+ const error = queryResponse?.errors?.[0];
+ const message = chartAlert || queryResponse?.message;
+
+ // if datasource is still loading, don't render JS errors
+ // but always show backend API errors (which have an errors array)
+ // so users can see real issues like auth failures
+ if (
+ !error &&
+ chartAlert !== undefined &&
+ chartAlert !== NONEXISTENT_DATASET &&
+ datasource === PLACEHOLDER_DATASOURCE &&
+ datasetsStatus !== ResourceStatus.Error
+ ) {
+ return (
+ <Styles
+ key={chartId}
+ data-ui-anchor="chart"
+ className="chart-container"
+ data-test="chart-container"
+ height={height}
+ >
+ <Loading size={dashboardId ? 's' : 'm'} muted={!!dashboardId} />
+ </Styles>
+ );
+ }
+
+ return (
+ <ChartErrorMessage
+ key={chartId}
+ chartId={chartId}
+ error={error}
+ subtitle={message}
+ link={queryResponse ? queryResponse.link : undefined}
+ source={dashboardId ? ChartSource.Dashboard : ChartSource.Explore}
+ stackTrace={chartStackTrace}
+ />
+ );
+ },
+ [
chartAlert,
+ chartId,
chartStackTrace,
- datasource,
dashboardId,
- height,
datasetsStatus,
- } = this.props;
- const error = queryResponse?.errors?.[0];
- const message = chartAlert || queryResponse?.message;
+ datasource,
+ height,
+ ],
+ );
+
+ const renderSpinner = useCallback(
+ (databaseName: string | undefined) => {
+ const message = databaseName
+ ? t('Waiting on %s', databaseName)
+ : t('Waiting on database...');
- // if datasource is still loading, don't render JS errors
- // but always show backend API errors (which have an errors array)
- // so users can see real issues like auth failures
- if (
- !error &&
- chartAlert !== undefined &&
- chartAlert !== NONEXISTENT_DATASET &&
- datasource === PLACEHOLDER_DATASOURCE &&
- datasetsStatus !== ResourceStatus.Error
- ) {
return (
- <Styles
- key={chartId}
- data-ui-anchor="chart"
- className="chart-container"
- data-test="chart-container"
- height={height}
- >
+ <LoadingDiv>
<Loading
- size={this.props.dashboardId ? 's' : 'm'}
- muted={!!this.props.dashboardId}
+ position="inline-centered"
+ size={dashboardId ? 's' : 'm'}
+ muted={!!dashboardId}
/>
- </Styles>
+ <MessageSpan>{message}</MessageSpan>
+ </LoadingDiv>
);
- }
-
- return (
- <ChartErrorMessage
- key={chartId}
- chartId={chartId}
- error={error}
- subtitle={message}
- link={queryResponse ? queryResponse.link : undefined}
- source={dashboardId ? ChartSource.Dashboard : ChartSource.Explore}
- stackTrace={chartStackTrace}
- />
- );
- }
-
- renderSpinner(databaseName: string | undefined) {
- const message = databaseName
- ? t('Waiting on %s', databaseName)
- : t('Waiting on database...');
-
- return (
- <LoadingDiv>
- <Loading
- position="inline-centered"
- size={this.props.dashboardId ? 's' : 'm'}
- muted={!!this.props.dashboardId}
- />
- <MessageSpan>{message}</MessageSpan>
- </LoadingDiv>
- );
- }
+ },
+ [dashboardId],
+ );
- renderChartContainer() {
- return (
+ const renderChartContainer = useCallback(
+ () => (
<div className="slice_container" data-test="slice-container">
- {this.shouldRenderChart() ? (
+ {shouldRenderChart() ? (
<ChartRenderer
- {...this.props}
- source={
- this.props.dashboardId
- ? ChartSource.Dashboard
- : ChartSource.Explore
- }
- data-test={this.props.vizType}
+ annotationData={annotationData}
+ actions={actions}
+ chartId={chartId}
+ datasource={datasource}
+ initialValues={initialValues}
+ formData={formData}
+ height={height}
+ width={width}
+ setControlValue={setControlValue}
+ vizType={vizType}
+ triggerRender={triggerRender}
+ chartAlert={chartAlert}
+ chartStatus={chartStatus}
+ queriesResponse={queriesResponse}
+ triggerQuery={triggerQuery}
+ chartIsStale={chartIsStale}
+ addFilter={addFilter}
+ onFilterMenuOpen={onFilterMenuOpen}
+ onFilterMenuClose={onFilterMenuClose}
+ ownState={ownState}
+ postTransformProps={postTransformProps}
+ emitCrossFilters={emitCrossFilters}
+ onChartStateChange={onChartStateChange}
+ latestQueryFormData={latestQueryFormData}
+ filterState={filterState}
+ suppressLoadingSpinner={suppressLoadingSpinner}
+ source={dashboardId ? ChartSource.Dashboard : ChartSource.Explore}
/>
) : (
- <Loading
- size={this.props.dashboardId ? 's' : 'm'}
- muted={!!this.props.dashboardId}
- />
+ <Loading size={dashboardId ? 's' : 'm'} muted={!!dashboardId} />
)}
</div>
- );
- }
-
- render() {
- const {
- height,
+ ),
+ [
+ actions,
+ addFilter,
+ annotationData,
chartAlert,
+ chartId,
+ chartIsStale,
chartStatus,
+ dashboardId,
datasource,
- errorMessage,
- chartIsStale,
- queriesResponse = [],
+ emitCrossFilters,
+ filterState,
+ formData,
+ height,
+ initialValues,
+ latestQueryFormData,
+ onChartStateChange,
+ onFilterMenuClose,
+ onFilterMenuOpen,
+ ownState,
+ postTransformProps,
+ queriesResponse,
+ setControlValue,
+ shouldRenderChart,
+ suppressLoadingSpinner,
+ triggerQuery,
+ triggerRender,
+ vizType,
width,
- } = this.props;
-
- const databaseName =
- datasource?.parent?.name ??
- (datasource?.database?.name as string | undefined);
+ ],
+ );
- const isLoading = chartStatus === 'loading';
- // Suppress spinner during auto-refresh to avoid visual flicker
- const showSpinner = isLoading && !this.props.suppressLoadingSpinner;
+ const databaseName =
+ datasource?.parent?.name ??
+ (datasource?.database?.name as string | undefined);
- if (chartStatus === 'failed') {
- return (
- <ErrorContainer height={height}>
- {queriesResponse?.map(item =>
- this.renderErrorMessage(item as ChartErrorType),
- )}
- </ErrorContainer>
- );
- }
+ const isLoading = chartStatus === 'loading';
+ // Suppress spinner during auto-refresh to avoid visual flicker
+ const showSpinner = isLoading && !suppressLoadingSpinner;
- 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"
- />
- );
- }
- if (
- !isLoading &&
- !chartAlert &&
- !errorMessage &&
- chartIsStale &&
- ensureIsArray(queriesResponse).length === 0
- ) {
- return (
- <EmptyState
- size="large"
- title={t('Your chart is ready to go!')}
- description={
- <span>
- {t(
- 'Click on "Create chart" button in the control panel on the left to preview a visualization or',
- )}{' '}
- <span role="button" tabIndex={0} onClick={this.props.onQuery}>
- {t('click here')}
- </span>
- .
- </span>
- }
- image="chart.svg"
- />
- );
- }
+ if (chartStatus === 'failed') {
+ return (
+ <ErrorContainer height={height}>
+ {queriesResponse?.map(item =>
+ renderErrorMessage(item as ChartErrorType),
+ )}
+ </ErrorContainer>
+ );
+ }
+ if (errorMessage && ensureIsArray(queriesResponse).length === 0) {
return (
- <ErrorBoundary
- onError={this.handleRenderContainerFailure}
- showMessage={false}
- >
- <Styles
- data-ui-anchor="chart"
- className="chart-container"
- data-test="chart-container"
- height={height}
- width={width}
- >
- {showSpinner
- ? this.renderSpinner(databaseName)
- : this.renderChartContainer()}
- </Styles>
- </ErrorBoundary>
+ <EmptyState
+ size="large"
+ title={t('Add required control values to preview chart')}
+ description={getChartRequiredFieldsMissingMessage(true)}
+ image="chart.svg"
+ />
);
}
+ if (
+ !isLoading &&
+ !chartAlert &&
+ !errorMessage &&
+ chartIsStale &&
+ ensureIsArray(queriesResponse).length === 0
+ ) {
+ return (
+ <EmptyState
+ size="large"
+ title={t('Your chart is ready to go!')}
+ description={
+ <span>
+ {t(
+ 'Click on "Create chart" button in the control panel on the left to preview a visualization or',
+ )}{' '}
+ <span role="button" tabIndex={0} onClick={onQuery}>
+ {t('click here')}
+ </span>
+ .
+ </span>
+ }
+ image="chart.svg"
+ />
+ );
+ }
+
+ return (
+ <ErrorBoundary onError={handleRenderContainerFailure} showMessage={false}>
+ <Styles
+ data-ui-anchor="chart"
+ className="chart-container"
+ data-test="chart-container"
+ height={height}
+ width={width}
+ >
+ {showSpinner ? renderSpinner(databaseName) : renderChartContainer()}
+ </Styles>
+ </ErrorBoundary>
+ );
}
export default Chart;
diff --git a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx
index 5d5022b09c3..92a2b8c52db 100644
--- a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx
+++ b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx
@@ -82,7 +82,7 @@ const mockActions: MockActions = {
) => Dispatch,
};
-const requiredProps: Partial<ChartRendererProps> = {
+const requiredProps: ChartRendererProps = {
chartId: 1,
datasource: {} as ChartRendererProps['datasource'],
formData: {
@@ -111,17 +111,14 @@ afterAll(() => {
test('should render SuperChart', () => {
const { getByTestId } = render(
- <ChartRenderer
- {...(requiredProps as ChartRendererProps)}
- chartIsStale={false}
- />,
+ <ChartRenderer {...requiredProps} chartIsStale={false} />,
);
expect(getByTestId('mock-super-chart')).toBeInTheDocument();
});
test('should use latestQueryFormData instead of formData when chartIsStale is true', () => {
const { getByTestId } = render(
- <ChartRenderer {...(requiredProps as ChartRendererProps)} chartIsStale />,
+ <ChartRenderer {...requiredProps} chartIsStale />,
);
expect(getByTestId('mock-super-chart')).toHaveTextContent(
JSON.stringify({
@@ -131,9 +128,7 @@ test('should use latestQueryFormData instead of formData when chartIsStale is tr
});
test('should render chart context menu', () => {
- const { getByTestId } = render(
- <ChartRenderer {...(requiredProps as ChartRendererProps)} />,
- );
+ const { getByTestId } = render(<ChartRenderer {...requiredProps} />);
expect(getByTestId('mock-chart-context-menu')).toBeInTheDocument();
});
@@ -148,16 +143,13 @@ test('should not render chart context menu if the context menu is suppressed for
}),
);
const { queryByTestId } = render(
- <ChartRenderer
- {...(requiredProps as ChartRendererProps)}
- vizType="chart_without_context_menu"
- />,
+ <ChartRenderer {...requiredProps} vizType="chart_without_context_menu" />,
);
expect(queryByTestId('mock-chart-context-menu')).not.toBeInTheDocument();
});
test('should detect changes in matrixify properties', () => {
- const initialProps: Partial<ChartRendererProps> = {
+ const initialProps: ChartRendererProps = {
...requiredProps,
formData: {
...requiredProps.formData,
@@ -173,41 +165,34 @@ test('should detect changes in matrixify properties', () => {
chartStatus: 'success',
};
- render(<ChartRenderer {...(initialProps as ChartRendererProps)} />);
+ const { getByTestId } = render(<ChartRenderer {...initialProps} />);
- // Since we can't directly test shouldComponentUpdate, we verify the component
- // correctly identifies matrixify-related properties by checking the implementation
- expect((initialProps.formData as JsonObject).matrixify_mode_rows).toBe(
- 'metrics',
+ // Verify matrixify-related formData is forwarded through to the chart
+ expect(getByTestId('mock-super-chart')).toHaveTextContent(
+ JSON.stringify(initialProps.formData),
);
- expect((initialProps.formData as JsonObject).matrixify_dimension_x).toEqual({
- dimension: 'country',
- values: ['USA'],
- });
});
test('should detect changes in postTransformProps', () => {
const postTransformProps = jest.fn((x: JsonObject) => x);
- const initialProps: Partial<ChartRendererProps> = {
+ const initialProps: ChartRendererProps = {
...requiredProps,
queriesResponse: [{ data: 'initial' } as unknown as JsonObject],
chartStatus: 'success',
};
- const { rerender } = render(
- <ChartRenderer {...(initialProps as ChartRendererProps)} />,
- );
- const updatedProps: Partial<ChartRendererProps> = {
+ const { rerender } = render(<ChartRenderer {...initialProps} />);
+ const updatedProps: ChartRendererProps = {
...initialProps,
postTransformProps,
};
expect(postTransformProps).toHaveBeenCalledTimes(0);
- rerender(<ChartRenderer {...(updatedProps as ChartRendererProps)} />);
+ rerender(<ChartRenderer {...updatedProps} />);
expect(postTransformProps).toHaveBeenCalledTimes(1);
});
test('should identify matrixify property changes correctly', () => {
// Test that formData with different matrixify properties triggers updates
- const initialProps: Partial<ChartRendererProps> = {
+ const initialProps: ChartRendererProps = {
...requiredProps,
formData: {
datasource: '',
@@ -221,16 +206,14 @@ test('should identify matrixify property changes correctly', () => {
chartStatus: 'success',
};
- const { rerender, getByTestId } = render(
- <ChartRenderer {...(initialProps as ChartRendererProps)} />,
- );
+ const { rerender, getByTestId } = render(<ChartRenderer {...initialProps} />);
expect(getByTestId('mock-super-chart')).toHaveTextContent(
JSON.stringify(initialProps.formData),
);
// Update with changed matrixify_dimension_x values
- const updatedProps: Partial<ChartRendererProps> = {
+ const updatedProps: ChartRendererProps = {
...initialProps,
formData: {
datasource: '',
@@ -245,7 +228,7 @@ test('should identify matrixify property changes correctly', () => {
},
};
- rerender(<ChartRenderer {...(updatedProps as ChartRendererProps)} />);
+ rerender(<ChartRenderer {...updatedProps} />);
// Verify the component re-rendered with new props
expect(getByTestId('mock-super-chart')).toHaveTextContent(
@@ -254,7 +237,7 @@ test('should identify matrixify property changes correctly', () => {
});
test('should handle matrixify-related form data changes', () => {
- const initialProps: Partial<ChartRendererProps> = {
+ const initialProps: ChartRendererProps = {
...requiredProps,
formData: {
datasource: '',
@@ -265,16 +248,14 @@ test('should handle matrixify-related form data changes', () => {
chartStatus: 'success',
};
- const { rerender, getByTestId } = render(
- <ChartRenderer {...(initialProps as ChartRendererProps)} />,
- );
+ const { rerender, getByTestId } = render(<ChartRenderer {...initialProps} />);
expect(getByTestId('mock-super-chart')).toHaveTextContent(
JSON.stringify(initialProps.formData),
);
// Enable matrixify
- const updatedProps: Partial<ChartRendererProps> = {
+ const updatedProps: ChartRendererProps = {
...initialProps,
formData: {
datasource: '',
@@ -285,7 +266,7 @@ test('should handle matrixify-related form data changes', () => {
},
};
- rerender(<ChartRenderer {...(updatedProps as ChartRendererProps)} />);
+ rerender(<ChartRenderer {...updatedProps} />);
// Verify the component re-rendered with matrixify enabled
expect(getByTestId('mock-super-chart')).toHaveTextContent(
@@ -294,7 +275,7 @@ test('should handle matrixify-related form data changes', () => {
});
test('should detect matrixify property addition', () => {
- const initialProps: Partial<ChartRendererProps> = {
+ const initialProps: ChartRendererProps = {
...requiredProps,
formData: {
datasource: '',
@@ -307,16 +288,14 @@ test('should detect matrixify property addition', () => {
chartStatus: 'success',
};
- const { rerender, getByTestId } = render(
- <ChartRenderer {...(initialProps as ChartRendererProps)} />,
- );
+ const { rerender, getByTestId } = render(<ChartRenderer {...initialProps} />);
expect(getByTestId('mock-super-chart')).toHaveTextContent(
JSON.stringify(initialProps.formData),
);
// Add matrixify_dimension_x
- const updatedProps: Partial<ChartRendererProps> = {
+ const updatedProps: ChartRendererProps = {
...initialProps,
formData: {
datasource: '',
@@ -327,7 +306,7 @@ test('should detect matrixify property addition', () => {
},
};
- rerender(<ChartRenderer {...(updatedProps as ChartRendererProps)} />);
+ rerender(<ChartRenderer {...updatedProps} />);
// Verify the component re-rendered with the new property
expect(getByTestId('mock-super-chart')).toHaveTextContent(
@@ -336,7 +315,7 @@ test('should detect matrixify property addition', () => {
});
test('should detect nested matrixify property changes', () => {
- const initialProps: Partial<ChartRendererProps> = {
+ const initialProps: ChartRendererProps = {
...requiredProps,
formData: {
datasource: '',
@@ -353,16 +332,14 @@ test('should detect nested matrixify property changes', () => {
chartStatus: 'success',
};
- const { rerender, getByTestId } = render(
- <ChartRenderer {...(initialProps as ChartRendererProps)} />,
- );
+ const { rerender, getByTestId } = render(<ChartRenderer {...initialProps} />);
expect(getByTestId('mock-super-chart')).toHaveTextContent(
JSON.stringify(initialProps.formData),
);
// Change nested topN value
- const updatedProps: Partial<ChartRendererProps> = {
+ const updatedProps: ChartRendererProps = {
...initialProps,
formData: {
datasource: '',
@@ -377,7 +354,7 @@ test('should detect nested matrixify property changes', () => {
},
};
- rerender(<ChartRenderer {...(updatedProps as ChartRendererProps)} />);
+ rerender(<ChartRenderer {...updatedProps} />);
// Verify the component re-rendered with the nested change
expect(getByTestId('mock-super-chart')).toHaveTextContent(
diff --git a/superset-frontend/src/components/Chart/ChartRenderer.tsx b/superset-frontend/src/components/Chart/ChartRenderer.tsx
index b5fa33c04c1..90a35d75a45 100644
--- a/superset-frontend/src/components/Chart/ChartRenderer.tsx
+++ b/superset-frontend/src/components/Chart/ChartRenderer.tsx
@@ -16,8 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { snakeCase, isEqual, cloneDeep } from 'lodash';
-import { createRef, Component, RefObject, MouseEvent, ReactNode } from 'react';
+import { snakeCase, cloneDeep } from 'lodash';
+import {
+ useCallback,
+ useEffect,
+ useState,
+ useRef,
+ useMemo,
+ MouseEvent,
+ ReactNode,
+ memo,
+} from 'react';
import {
SuperChart,
Behavior,
@@ -28,6 +37,7 @@ import {
QueryFormData,
AnnotationData,
DataMask,
+ FilterState,
QueryData,
JsonObject,
LatestQueryFormData,
@@ -37,6 +47,7 @@ import {
} from '@superset-ui/core';
import { logging } from '@apache-superset/core/utils';
import { t } from '@apache-superset/core/translation';
+import { useTheme } from '@apache-superset/core/theme';
import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
import { EmptyState } from '@superset-ui/core/components';
import { ChartSource } from 'src/types/ChartSource';
@@ -91,12 +102,6 @@ interface OwnState {
[key: string]: unknown;
}
-// Types for filter state
-interface FilterState {
- value?: FilterValue[];
- [key: string]: unknown;
-}
-
// Props interface
export interface ChartRendererProps {
annotationData?: AnnotationData;
@@ -124,7 +129,6 @@ export interface ChartRendererProps {
merge?: boolean,
refresh?: boolean,
) => void;
- setDataMask?: (dataMask: DataMask) => void;
onFilterMenuOpen?: (chartId: number, column: string) => void;
onFilterMenuClose?: (chartId: number, column: string) => void;
ownState?: OwnState;
@@ -137,14 +141,6 @@ export interface ChartRendererProps {
suppressLoadingSpinner?: boolean;
}
-// State interface
-interface ChartRendererState {
- showContextMenu: boolean;
- inContextMenu: boolean;
- legendState: LegendState | undefined;
- legendIndex: number;
-}
-
// Hooks interface
interface ChartHooks {
onAddFilter: (
@@ -175,402 +171,376 @@ const BIG_NO_RESULT_MIN_HEIGHT = 220;
const behaviors = [Behavior.InteractiveChart];
-const defaultProps: Partial<ChartRendererProps> = {
- addFilter: () => BLANK,
- onFilterMenuOpen: () => BLANK,
- onFilterMenuClose: () => BLANK,
- initialValues: BLANK,
- setControlValue: () => {},
- triggerRender: false,
-};
-
-class ChartRenderer extends Component<ChartRendererProps, ChartRendererState> {
- static defaultProps = defaultProps;
-
- private hasQueryResponseChange: boolean;
-
- private contextMenuRef: RefObject<ChartContextMenuRef>;
-
- private hooks: ChartHooks;
-
- private mutableQueriesResponse: QueryData[] | null | undefined;
-
- private renderStartTime: number;
-
- constructor(props: ChartRendererProps) {
- 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,
- legendIndex: 0,
- };
- this.hasQueryResponseChange = false;
- this.renderStartTime = 0;
-
- this.contextMenuRef = createRef<ChartContextMenuRef>();
-
- 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.handleLegendScroll = this.handleLegendScroll.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: DataMask) => {
- this.props.actions?.updateDataMask?.(this.props.chartId, dataMask);
- },
- onLegendScroll: this.handleLegendScroll,
- onChartStateChange: this.props.onChartStateChange,
- };
-
- // 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: ChartRendererProps,
- nextState: ChartRendererState,
- ): boolean {
- const resultsReady =
- nextProps.queriesResponse &&
- ['success', 'rendered'].indexOf(nextProps.chartStatus as string) > -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);
- }
-
- // Check if any matrixify-related properties have changed
- const hasMatrixifyChanges = (): boolean => {
- const nextFormData = nextProps.formData as JsonObject;
- const currentFormData = this.props.formData as JsonObject;
- const isMatrixifyEnabled =
- nextFormData.matrixify_enable === true &&
- ((nextFormData.matrixify_mode_rows !== undefined &&
- nextFormData.matrixify_mode_rows !== 'disabled') ||
- (nextFormData.matrixify_mode_columns !== undefined &&
- nextFormData.matrixify_mode_columns !== 'disabled'));
- if (!isMatrixifyEnabled) return false;
-
- // Check all matrixify-related properties
- const matrixifyKeys = Object.keys(nextFormData).filter(key =>
- key.startsWith('matrixify_'),
- );
-
- return matrixifyKeys.some(
- key => !isEqual(nextFormData[key], currentFormData[key]),
- );
- };
-
- const nextFormData = nextProps.formData as JsonObject;
- const currentFormData = this.props.formData as JsonObject;
-
- 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 === true ||
- nextProps.labelsColor !== this.props.labelsColor ||
- nextProps.labelsColorMap !== this.props.labelsColorMap ||
- nextFormData.color_scheme !== currentFormData.color_scheme ||
- nextFormData.stack !== currentFormData.stack ||
- nextFormData.subcategories !== currentFormData.subcategories ||
- nextProps.cacheBusterProp !== this.props.cacheBusterProp ||
- nextProps.emitCrossFilters !== this.props.emitCrossFilters ||
- nextProps.postTransformProps !== this.props.postTransformProps ||
- hasMatrixifyChanges()
- );
- }
- return false;
- }
+interface ChartRendererState {
+ inContextMenu: boolean;
+ legendState: LegendState | undefined;
+ legendIndex: number;
+}
- handleAddFilter(
- col: string,
- vals: FilterValue[],
- merge = true,
- refresh = true,
- ): void {
- this.props.addFilter?.(col, vals, merge, refresh);
+function ChartRendererComponent({
+ addFilter = () => BLANK,
+ onFilterMenuOpen = () => BLANK,
+ onFilterMenuClose = () => BLANK,
+ initialValues = BLANK,
+ setControlValue = () => {},
+ triggerRender = false,
+ ...restProps
+}: ChartRendererProps): JSX.Element | null {
+ const {
+ annotationData,
+ actions,
+ chartId,
+ datasource,
+ formData,
+ latestQueryFormData,
+ height,
+ width,
+ vizType: propVizType,
+ chartAlert,
+ chartStatus,
+ queriesResponse,
+ chartIsStale,
+ ownState,
+ filterState,
+ postTransformProps,
+ source,
+ emitCrossFilters,
+ onChartStateChange,
+ } = restProps;
+
+ const theme = useTheme();
+
+ const suppressContextMenu = getChartMetadataRegistry().get(
+ formData.viz_type ?? propVizType,
+ )?.suppressContextMenu;
+
+ // Derived from props/feature-flags: must NOT live in state, otherwise a
+ // `source` or viz-type change on the same mounted instance would leave
+ // it stale. (Pre-refactor this was a class-instance field recomputed on
+ // every render — preserve that semantic by using a memo here.)
+ const showContextMenu = useMemo(
+ () =>
+ source === ChartSource.Dashboard &&
+ !suppressContextMenu &&
+ isFeatureEnabled(FeatureFlag.DrillToDetail),
+ [source, suppressContextMenu],
+ );
+
+ const [state, setState] = useState<ChartRendererState>({
+ inContextMenu: false,
+ legendState: undefined,
+ legendIndex: 0,
+ });
+
+ const hasQueryResponseChangeRef = useRef(false);
+ const renderStartTimeRef = useRef(0);
+ const contextMenuRef = useRef<ChartContextMenuRef>(null);
+
+ // Results are "ready" when we have a non-error queriesResponse and the
+ // chartStatus reflects it. This mirrors the gating logic from the former
+ // shouldComponentUpdate implementation.
+ const resultsReady =
+ queriesResponse &&
+ ['success', 'rendered'].indexOf(chartStatus as string) > -1 &&
+ !queriesResponse?.[0]?.error;
+
+ // Track whether queriesResponse changed since the previous render so that
+ // handleRenderSuccess / handleRenderFailure know whether to log render time.
+ // Updating a ref during render is safe when the value doesn't affect the
+ // render output (here it's read asynchronously from SuperChart callbacks).
+ const prevQueriesResponseRef = useRef<QueryData[] | null | undefined>(
+ queriesResponse,
+ );
+ if (resultsReady) {
+ hasQueryResponseChangeRef.current =
+ queriesResponse !== prevQueriesResponseRef.current;
}
+ useEffect(() => {
+ prevQueriesResponseRef.current = queriesResponse;
+ }, [queriesResponse]);
+
+ // Clone queriesResponse to protect against plugin mutation of Redux state.
+ // Gate on `resultsReady` so the deep clone doesn't run for every
+ // queriesResponse identity change during loading/idle (only when results
+ // are actually about to render). Matches the pre-refactor gating.
+ // TODO: remove once reducers use Redux Toolkit with Immer.
+ const mutableQueriesResponse = useMemo(
+ () => (resultsReady ? cloneDeep(queriesResponse) : undefined),
+ [queriesResponse, resultsReady],
+ );
+
+ // Handler functions
+ const handleAddFilter = useCallback(
+ (col: string, vals: FilterValue[], merge = true, refresh = true): void => {
+ addFilter?.(col, vals, merge, refresh);
+ },
+ [addFilter],
+ );
- handleRenderSuccess(): void {
- const { actions, chartStatus, chartId, vizType } = this.props;
+ const handleRenderSuccess = useCallback((): void => {
if (['loading', 'rendered'].indexOf(chartStatus as string) < 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) {
+ if (hasQueryResponseChangeRef.current) {
actions.logEvent(LOG_ACTIONS_RENDER_CHART, {
slice_id: chartId,
- viz_type: vizType,
- start_offset: this.renderStartTime,
+ viz_type: propVizType,
+ start_offset: renderStartTimeRef.current,
ts: new Date().getTime(),
- duration: Logger.getTimestamp() - this.renderStartTime,
+ duration: Logger.getTimestamp() - renderStartTimeRef.current,
});
}
- }
+ }, [actions, chartId, chartStatus, propVizType]);
+
+ const handleRenderFailure = useCallback(
+ (error: Error, info: { componentStack: string } | null): void => {
+ logging.warn(error);
+ actions.chartRenderingFailed(
+ error.toString(),
+ chartId,
+ info ? info.componentStack : null,
+ );
- handleRenderFailure(
- error: Error,
- info: { componentStack: string } | null,
- ): void {
- 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 (hasQueryResponseChangeRef.current) {
+ actions.logEvent(LOG_ACTIONS_RENDER_CHART, {
+ slice_id: chartId,
+ has_err: true,
+ error_details: error.toString(),
+ start_offset: renderStartTimeRef.current,
+ ts: new Date().getTime(),
+ duration: Logger.getTimestamp() - renderStartTimeRef.current,
+ });
+ }
+ },
+ [actions, chartId],
+ );
- // 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,
- });
- }
- }
+ const handleSetControlValue = useCallback(
+ (name: string, value: unknown): void => {
+ if (setControlValue) {
+ setControlValue(name, value);
+ }
+ },
+ [setControlValue],
+ );
- handleSetControlValue(name: string, value: unknown): void {
- const { setControlValue } = this.props;
- if (setControlValue) {
- setControlValue(name, value);
- }
- }
+ const handleOnContextMenu = useCallback(
+ (offsetX: number, offsetY: number, filters?: ContextMenuFilters): void => {
+ contextMenuRef.current?.open(offsetX, offsetY, filters);
+ setState(prev => ({ ...prev, inContextMenu: true }));
+ },
+ [contextMenuRef],
+ );
- handleOnContextMenu(
- offsetX: number,
- offsetY: number,
- filters?: ContextMenuFilters,
- ): void {
- this.contextMenuRef.current?.open(offsetX, offsetY, filters);
- this.setState({ inContextMenu: true });
- }
+ const handleContextMenuSelected = useCallback((): void => {
+ setState(prev => ({ ...prev, inContextMenu: false }));
+ }, []);
- handleContextMenuSelected(): void {
- this.setState({ inContextMenu: false });
- }
+ const handleContextMenuClosed = useCallback((): void => {
+ setState(prev => ({ ...prev, inContextMenu: false }));
+ }, []);
- handleContextMenuClosed(): void {
- this.setState({ inContextMenu: false });
- }
+ const handleLegendStateChanged = useCallback(
+ (legendState: LegendState): void => {
+ setState(prev => ({ ...prev, legendState }));
+ },
+ [],
+ );
- handleLegendStateChanged(legendState: LegendState): void {
- this.setState({ legendState });
- }
+ const handleLegendScroll = useCallback((legendIndex: number): void => {
+ setState(prev => ({ ...prev, legendIndex }));
+ }, []);
// When viz plugins don't handle `contextmenu` event, fallback handler
// calls `handleOnContextMenu` with no `filters` param.
- onContextMenuFallback(event: MouseEvent<HTMLDivElement>): void {
- if (!this.state.inContextMenu) {
- event.preventDefault();
- this.handleOnContextMenu(event.clientX, event.clientY);
- }
- }
+ const onContextMenuFallback = useCallback(
+ (event: MouseEvent<HTMLDivElement>): void => {
+ if (!state.inContextMenu) {
+ event.preventDefault();
+ handleOnContextMenu(event.clientX, event.clientY);
+ }
+ },
+ [handleOnContextMenu, state.inContextMenu],
+ );
- handleLegendScroll(legendIndex: number): void {
- this.setState({ legendIndex });
+ const setDataMaskCallback = useCallback(
+ (dataMask: DataMask) => {
+ actions?.updateDataMask?.(chartId, dataMask);
+ },
+ [actions, chartId],
+ );
+
+ // Hooks object - memoized
+ const hooks = useMemo<ChartHooks>(
+ () => ({
+ onAddFilter: handleAddFilter,
+ onContextMenu: showContextMenu ? handleOnContextMenu : undefined,
+ onError: handleRenderFailure,
+ setControlValue: handleSetControlValue,
+ onFilterMenuOpen,
+ onFilterMenuClose,
+ onLegendStateChanged: handleLegendStateChanged,
+ setDataMask: setDataMaskCallback,
+ onLegendScroll: handleLegendScroll,
+ onChartStateChange,
+ }),
+ [
+ handleAddFilter,
+ handleLegendScroll,
+ handleLegendStateChanged,
+ handleOnContextMenu,
+ handleRenderFailure,
+ handleSetControlValue,
+ onChartStateChange,
+ onFilterMenuClose,
+ onFilterMenuOpen,
+ setDataMaskCallback,
+ showContextMenu,
+ ],
+ );
+
+ const hasAnyErrors = queriesResponse?.some(item => item?.error);
+ const hasValidPreviousData =
+ (queriesResponse?.length ?? 0) > 0 && !hasAnyErrors;
+
+ if (!!chartAlert || chartStatus === null) {
+ return null;
}
- render(): ReactNode {
- const { chartAlert, chartStatus, chartId, emitCrossFilters } = this.props;
-
- const hasAnyErrors = this.props.queriesResponse?.some(item => item?.error);
- const hasValidPreviousData =
- (this.props.queriesResponse?.length ?? 0) > 0 && !hasAnyErrors;
-
- if (!!chartAlert || chartStatus === null) {
+ if (chartStatus === 'loading') {
+ if (!restProps.suppressLoadingSpinner || !hasValidPreviousData) {
return null;
}
+ }
- if (chartStatus === 'loading') {
- if (!this.props.suppressLoadingSpinner || !hasValidPreviousData) {
- 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: ReactNode;
- 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 ?? 0) > BIG_NO_RESULT_MIN_WIDTH &&
- (height ?? 0) > BIG_NO_RESULT_MIN_HEIGHT
- ) {
- noResultsComponent = (
- <EmptyState
- size="large"
- title={noResultTitle}
- description={noResultDescription}
- image={noResultImage}
- />
- );
- } else {
- noResultsComponent = (
- <EmptyState title={noResultTitle} image={noResultImage} size="small" />
- );
- }
-
- // 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(vizType)
- ?.behaviors.find(behavior => behavior === Behavior.DrillToDetail)
- ? { inContextMenu: this.state.inContextMenu }
- : {};
- // By pass no result component when server pagination is enabled & the table has:
- // - a backend search query, OR
- // - non-empty AG Grid filter model
- const hasSearchText = (ownState?.searchText?.length || 0) > 0;
- const hasAgGridFilters =
- ownState?.agGridFilterModel &&
- Object.keys(ownState.agGridFilterModel).length > 0;
-
- const currentFormDataExtended = currentFormData as JsonObject;
- const bypassNoResult = !(
- currentFormDataExtended?.server_pagination &&
- (hasSearchText || hasAgGridFilters)
+ renderStartTimeRef.current = Logger.getTimestamp();
+
+ const currentFormData =
+ chartIsStale && latestQueryFormData ? latestQueryFormData : formData;
+ const vizType = currentFormData.viz_type || propVizType;
+
+ // 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: ReactNode;
+ 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 (
+ (width ?? 0) > BIG_NO_RESULT_MIN_WIDTH &&
+ (height ?? 0) > BIG_NO_RESULT_MIN_HEIGHT
+ ) {
+ noResultsComponent = (
+ <EmptyState
+ size="large"
+ title={noResultTitle}
+ description={noResultDescription}
+ image={noResultImage}
+ />
);
-
- return (
- <>
- {this.state.showContextMenu && (
- <ChartContextMenu
- ref={this.contextMenuRef}
- id={chartId}
- formData={currentFormData as QueryFormData}
- 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}
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- hooks={this.hooks as any}
- behaviors={behaviors}
- queriesData={this.mutableQueriesResponse ?? undefined}
- onRenderSuccess={this.handleRenderSuccess}
- onRenderFailure={this.handleRenderFailure}
- noResults={noResultsComponent}
- postTransformProps={postTransformProps}
- emitCrossFilters={emitCrossFilters}
- legendState={this.state.legendState}
- enableNoResults={bypassNoResult}
- legendIndex={this.state.legendIndex}
- isRefreshing={
- Boolean(this.props.suppressLoadingSpinner) &&
- chartStatus === 'loading'
- }
- {...drillToDetailProps}
- />
- </div>
- </>
+ } else {
+ noResultsComponent = (
+ <EmptyState title={noResultTitle} image={noResultImage} size="small" />
);
}
+
+ // 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(vizType)
+ ?.behaviors.find(behavior => behavior === Behavior.DrillToDetail)
+ ? { inContextMenu: state.inContextMenu }
+ : {};
+ // By pass no result component when server pagination is enabled & the table has:
+ // - a backend search query, OR
+ // - non-empty AG Grid filter model
+ const hasSearchText = (ownState?.searchText?.length || 0) > 0;
+ const hasAgGridFilters =
+ ownState?.agGridFilterModel &&
+ Object.keys(ownState.agGridFilterModel).length > 0;
+
+ const currentFormDataExtended = currentFormData as JsonObject;
+ const bypassNoResult = !(
+ currentFormDataExtended?.server_pagination &&
+ (hasSearchText || hasAgGridFilters)
+ );
+
+ return (
+ <>
+ {showContextMenu && (
+ <ChartContextMenu
+ ref={contextMenuRef}
+ id={chartId}
+ formData={currentFormData as QueryFormData}
+ onSelection={handleContextMenuSelected}
+ onClose={handleContextMenuClosed}
+ />
+ )}
+ <div onContextMenu={showContextMenu ? onContextMenuFallback : undefined}>
+ <SuperChart
+ disableErrorBoundary
+ key={`${chartId}${webpackHash}`}
+ id={`chart-id-${chartId}`}
+ className={chartClassName}
+ chartType={vizType}
+ width={width}
+ height={height}
+ theme={theme}
+ annotationData={annotationData}
+ datasource={datasource}
+ initialValues={initialValues}
+ formData={currentFormData}
+ ownState={ownState}
+ filterState={filterState}
+ hooks={hooks as unknown as Parameters<typeof SuperChart>[0]['hooks']}
+ behaviors={behaviors}
+ queriesData={mutableQueriesResponse ?? undefined}
+ onRenderSuccess={handleRenderSuccess}
+ onRenderFailure={handleRenderFailure}
+ noResults={noResultsComponent}
+ postTransformProps={postTransformProps}
+ emitCrossFilters={emitCrossFilters}
+ legendState={state.legendState}
+ enableNoResults={bypassNoResult}
+ legendIndex={state.legendIndex}
+ isRefreshing={
+ Boolean(restProps.suppressLoadingSpinner) &&
+ chartStatus === 'loading'
+ }
+ {...drillToDetailProps}
+ />
+ </div>
+ </>
+ );
}
+const ChartRenderer = memo(ChartRendererComponent);
+
export default ChartRenderer;
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByChart.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByChart.tsx
index b977433e853..db1670f924b 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByChart.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByChart.tsx
@@ -23,7 +23,7 @@ import {
SuperChart,
ContextMenuFilters,
} from '@superset-ui/core';
-import { css } from '@apache-superset/core/theme';
+import { css, useTheme } from '@apache-superset/core/theme';
import { Dataset } from '../types';
interface DrillByChartProps {
@@ -45,6 +45,7 @@ export default function DrillByChart({
onContextMenu,
inContextMenu,
}: DrillByChartProps) {
+ const theme = useTheme();
const hooks = useMemo(() => ({ onContextMenu }), [onContextMenu]);
return (
@@ -67,6 +68,7 @@ export default function DrillByChart({
inContextMenu={inContextMenu}
height="100%"
width="100%"
+ theme={theme}
/>
</div>
);