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/07/05 06:28:04 UTC
(superset) branch master updated: perf(explore): reuse chart query data in Results tab (#38165)
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 8229c0183dc perf(explore): reuse chart query data in Results tab (#38165)
8229c0183dc is described below
commit 8229c0183dc030b336c4365b06970be5d7359e44
Author: hayden <sk...@gmail.com>
AuthorDate: Sun Jul 5 15:27:57 2026 +0900
perf(explore): reuse chart query data in Results tab (#38165)
Co-authored-by: devops-sewhan-kim <se...@a-bly.com>
---
superset-frontend/playwright/pages/ExplorePage.ts | 46 +++++
.../experimental/explore/results-tab-reuse.spec.ts | 123 +++++++++++++
.../components/DataTablesPane/DataTablesPane.tsx | 6 +
.../components/ResultsPaneOnDashboard.tsx | 2 +
.../DataTablesPane/components/useResultsPane.tsx | 94 +++++++---
.../components/DataTablesPane/test/fixture.tsx | 8 +-
.../DataTablesPane/test/useResultsPane.test.tsx | 201 +++++++++++++++++++++
.../src/explore/components/DataTablesPane/types.ts | 4 +-
.../explore/components/ExploreChartPanel/index.tsx | 1 +
9 files changed, 457 insertions(+), 28 deletions(-)
diff --git a/superset-frontend/playwright/pages/ExplorePage.ts b/superset-frontend/playwright/pages/ExplorePage.ts
index 6cd79ef34ea..2575b55332b 100644
--- a/superset-frontend/playwright/pages/ExplorePage.ts
+++ b/superset-frontend/playwright/pages/ExplorePage.ts
@@ -19,6 +19,7 @@
import { Page, Locator } from '@playwright/test';
import { TIMEOUT } from '../utils/constants';
+import { AgGrid } from '../components/core/AgGrid';
/**
* Explore Page object
@@ -30,6 +31,11 @@ export class ExplorePage {
DATASOURCE_CONTROL: '[data-test="datasource-control"]',
VIZ_SWITCHER: '[data-test="fast-viz-switcher"]',
CHART_CONTAINER: '[data-test="chart-container"]',
+ // The bottom data panel (DataTablesPane / SouthPane) with Results/Samples tabs
+ SOUTH_PANE: '[data-test="some-purposeful-instance"]',
+ EXPAND_DATA_PANEL: '[aria-label="Expand data panel"]',
+ RESULTS_TAB: '[data-node-key="results"]',
+ ACTIVE_TABPANE: '.ant-tabs-tabpane-active',
} as const;
constructor(page: Page) {
@@ -106,4 +112,44 @@ export class ExplorePage {
getVizSwitcher(): Locator {
return this.page.locator(ExplorePage.SELECTORS.VIZ_SWITCHER);
}
+
+ /**
+ * Expands the bottom data panel if it is currently collapsed.
+ * Safe to call when already expanded (no-op).
+ */
+ async expandDataPanel(): Promise<void> {
+ const expandButton = this.page.locator(
+ ExplorePage.SELECTORS.EXPAND_DATA_PANEL,
+ );
+ if (await expandButton.isVisible().catch(() => false)) {
+ await expandButton.click();
+ }
+ }
+
+ /**
+ * Opens the bottom data panel and activates the "Results" tab. Clicking the
+ * already-active tab collapses the panel, so the click is guarded.
+ */
+ async openResultsTab(): Promise<void> {
+ await this.expandDataPanel();
+ const resultsTab = this.page
+ .locator(ExplorePage.SELECTORS.SOUTH_PANE)
+ .locator(ExplorePage.SELECTORS.RESULTS_TAB);
+ const className = (await resultsTab.getAttribute('class')) ?? '';
+ if (!className.includes('ant-tabs-tab-active')) {
+ await resultsTab.click();
+ }
+ }
+
+ /**
+ * Returns an AgGrid wrapper around the currently active Results tab grid.
+ */
+ getResultsGrid(): AgGrid {
+ const grid = this.page
+ .locator(ExplorePage.SELECTORS.SOUTH_PANE)
+ .locator(ExplorePage.SELECTORS.ACTIVE_TABPANE)
+ .locator('[role="grid"]')
+ .first();
+ return new AgGrid(this.page, grid);
+ }
}
diff --git a/superset-frontend/playwright/tests/experimental/explore/results-tab-reuse.spec.ts b/superset-frontend/playwright/tests/experimental/explore/results-tab-reuse.spec.ts
new file mode 100644
index 00000000000..6a08260d426
--- /dev/null
+++ b/superset-frontend/playwright/tests/experimental/explore/results-tab-reuse.spec.ts
@@ -0,0 +1,123 @@
+/**
+ * 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.
+ */
+
+/**
+ * Regression coverage for #38152 (PR #38165): the Explore "Results" tab reuses
+ * the chart's already-fetched query data instead of firing a second, identical
+ * request to /api/v1/chart/data. The duplicate only exists across the real
+ * backend round-trip; the reuse-vs-fallback and row-limit slicing logic is
+ * unit-tested in useResultsPane.test.tsx, so this E2E asserts only the
+ * end-to-end win: opening Results issues no extra chart/data request.
+ *
+ * Lives under tests/experimental/ until proven stable in CI; run with:
+ * INCLUDE_EXPERIMENTAL=true npm run playwright:test \
+ * tests/experimental/explore/results-tab-reuse.spec.ts -- --headed
+ */
+import { testWithAssets, expect } from '../../../helpers/fixtures';
+import { apiPostChart } from '../../../helpers/api/chart';
+import { getDatasetByName } from '../../../helpers/api/dataset';
+import { ExplorePage } from '../../../pages/ExplorePage';
+import { TIMEOUT } from '../../../utils/constants';
+
+const DATASET_NAME = 'birth_names';
+const CHART_DATA_PATH = '/api/v1/chart/data';
+
+testWithAssets(
+ 'Results tab reuses chart data without a duplicate query (#38165)',
+ async ({ page, testAssets }) => {
+ testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
+
+ let chartDataRequests = 0;
+ page.on('request', request => {
+ if (
+ request.method() === 'POST' &&
+ request.url().includes(CHART_DATA_PATH)
+ ) {
+ chartDataRequests += 1;
+ }
+ });
+
+ const dataset = await getDatasetByName(page, DATASET_NAME);
+ if (!dataset) {
+ throw new Error(`Dataset ${DATASET_NAME} not found`);
+ }
+ const datasetId = dataset.id;
+
+ const params = {
+ datasource: `${datasetId}__table`,
+ viz_type: 'table',
+ query_mode: 'raw',
+ all_columns: ['name', 'gender', 'num'],
+ adhoc_filters: [],
+ order_by_cols: [],
+ row_limit: 1000,
+ server_pagination: false,
+ };
+ const chartResp = await apiPostChart(page, {
+ slice_name: `results_tab_reuse_${Date.now()}`,
+ viz_type: 'table',
+ datasource_id: datasetId,
+ datasource_type: 'table',
+ params: JSON.stringify(params),
+ });
+ expect(chartResp.ok()).toBe(true);
+ const chartBody = await chartResp.json();
+ const chartId: number = chartBody.result?.id ?? chartBody.id;
+ expect(chartId, 'chart creation should return an id').toBeTruthy();
+ testAssets.trackChart(chartId);
+
+ // Wait for the chart query to finish so its result is in Redux; opening
+ // Results before that races and falls back to its own request.
+ const explorePage = new ExplorePage(page);
+ const chartQueryFinished = page.waitForResponse(
+ response =>
+ response.request().method() === 'POST' &&
+ response.url().includes(CHART_DATA_PATH),
+ { timeout: TIMEOUT.API_RESPONSE },
+ );
+ await explorePage.goto(chartId);
+ await chartQueryFinished;
+
+ const chartContainer = explorePage.getChartContainer();
+ await chartContainer.waitFor({
+ state: 'visible',
+ timeout: TIMEOUT.PAGE_LOAD,
+ });
+ await chartContainer
+ .getByRole('status', { name: 'Loading' })
+ .waitFor({ state: 'hidden', timeout: TIMEOUT.API_RESPONSE })
+ .catch(() => {}); // spinner may have already cleared
+
+ expect(chartDataRequests).toBeGreaterThanOrEqual(1);
+ const requestsAfterChartLoad = chartDataRequests;
+
+ await explorePage.openResultsTab();
+ const grid = explorePage.getResultsGrid();
+ await grid.waitForRows({ timeout: TIMEOUT.API_RESPONSE });
+ expect(await grid.getRowCount()).toBeGreaterThan(0);
+
+ // Allow time for an unwanted duplicate request, then assert none fired.
+ // eslint-disable-next-line playwright/no-wait-for-timeout
+ await page.waitForTimeout(1500);
+ expect(
+ chartDataRequests,
+ 'opening Results must not trigger a second chart/data query',
+ ).toBe(requestsAfterChartLoad);
+ },
+);
diff --git a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx
index c7f3fe138c3..03733072063 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx
@@ -103,6 +103,7 @@ export const DataTablesPane = ({
errorMessage,
setForceQuery,
canDownload,
+ queriesResponse,
}: DataTablesPaneProps) => {
const [activeTabKey, setActiveTabKey] = useState<string>(ResultTypes.Results);
const [isRequest, setIsRequest] = useState<Record<ResultTypes, boolean>>({
@@ -128,6 +129,10 @@ export const DataTablesPane = ({
});
}
+ if (panelOpen && chartStatus === 'loading') {
+ setIsRequest(prev => ({ ...prev, results: false }));
+ }
+
if (
panelOpen &&
activeTabKey.startsWith(ResultTypes.Results) &&
@@ -206,6 +211,7 @@ export const DataTablesPane = ({
isRequest: isRequest.results,
setForceQuery,
canDownload,
+ queriesResponse,
}).map((pane, idx) => {
const tabKey =
idx === 0 ? ResultTypes.Results : `${ResultTypes.Results} ${idx + 1}`;
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx
index 70e4f755ebc..1b7ba711bae 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx
@@ -56,6 +56,7 @@ export const ResultsPaneOnDashboard = ({
dataSize = 50,
canDownload,
columnDisplayNames,
+ queriesResponse,
}: ResultsPaneProps) => {
const resultsPanes = useResultsPane({
errorMessage,
@@ -68,6 +69,7 @@ export const ResultsPaneOnDashboard = ({
isVisible,
canDownload,
columnDisplayNames,
+ queriesResponse,
});
if (resultsPanes.length === 1) {
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx
index 7f082c10922..4dcf1b5324a 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx
@@ -20,9 +20,11 @@ import { useState, useEffect, useMemo, ReactElement, useCallback } from 'react';
import { t } from '@apache-superset/core/translation';
import {
+ ChartDataResponseResult,
ensureIsArray,
getChartMetadataRegistry,
getClientErrorObject,
+ QueryData,
} from '@superset-ui/core';
import { styled } from '@apache-superset/core/theme';
import { EmptyState, Loading } from '@superset-ui/core/components';
@@ -45,6 +47,13 @@ const StyledDiv = styled.div`
const cache = new WeakMap();
+// `queriesResponse` is the loose `QueryData`; only reuse it when every entry is
+// a full v1 result (colnames/coltypes/data arrays), else fall back to the API.
+const isV1QueryResult = (query: QueryData): query is ChartDataResponseResult =>
+ Array.isArray((query as ChartDataResponseResult).colnames) &&
+ Array.isArray((query as ChartDataResponseResult).coltypes) &&
+ Array.isArray((query as ChartDataResponseResult).data);
+
export const useResultsPane = ({
isRequest,
queryFormData,
@@ -55,6 +64,7 @@ export const useResultsPane = ({
isVisible,
canDownload,
columnDisplayNames,
+ queriesResponse,
}: ResultsPaneProps): ReactElement[] => {
const metadata = getChartMetadataRegistry().get(
queryFormData?.viz_type || queryFormData?.vizType,
@@ -89,7 +99,38 @@ export const useResultsPane = ({
useEffect(() => {
// it's an invalid formData when gets a errorMessage
if (errorMessage) return;
- if (isRequest && cache.has(cappedFormData)) {
+ if (!isRequest) return;
+
+ // The chart query and the results query produce identical SQL, so reuse the
+ // chart's data instead of a second request. The chart always ran with a
+ // row_limit >= effectiveRowLimit, so its first `effectiveRowLimit` rows
+ // match what a dedicated results query would return — slice locally to keep
+ // the row-limit dropdown working without a duplicate query.
+ if (queriesResponse?.length && queriesResponse.every(isV1QueryResult)) {
+ const mapped = queriesResponse.map(q => {
+ const result = q as ChartDataResponseResult;
+ const limitedData = ensureIsArray(result.data).slice(
+ 0,
+ effectiveRowLimit,
+ );
+ return {
+ colnames: result.colnames,
+ coltypes: result.coltypes,
+ data: limitedData,
+ rowcount: limitedData.length,
+ };
+ }) as unknown as QueryResultInterface[];
+ setResultResp(mapped);
+ setResponseError('');
+ if (queryForce) {
+ setForceQuery?.(false);
+ }
+ setIsLoading(false);
+ return;
+ }
+
+ // Fallback: use cached data
+ if (cache.has(cappedFormData)) {
setResultResp(
ensureIsArray(cache.get(cappedFormData)) as QueryResultInterface[],
);
@@ -98,34 +139,35 @@ export const useResultsPane = ({
setForceQuery?.(false);
}
setIsLoading(false);
+ return;
}
- if (isRequest && !cache.has(cappedFormData)) {
- setIsLoading(true);
- getChartDataRequest({
- formData: cappedFormData,
- force: queryForce,
- resultFormat: 'json',
- resultType: 'results',
- ownState,
+
+ // Fallback: fetch from API (legacy charts without queriesResponse)
+ setIsLoading(true);
+ getChartDataRequest({
+ formData: cappedFormData,
+ force: queryForce,
+ resultFormat: 'json',
+ resultType: 'results',
+ ownState,
+ })
+ .then(({ json }) => {
+ setResultResp(ensureIsArray(json.result) as QueryResultInterface[]);
+ setResponseError('');
+ cache.set(cappedFormData, json.result);
+ if (queryForce) {
+ setForceQuery?.(false);
+ }
})
- .then(({ json }) => {
- setResultResp(ensureIsArray(json.result) as QueryResultInterface[]);
- setResponseError('');
- cache.set(cappedFormData, json.result);
- if (queryForce) {
- setForceQuery?.(false);
- }
- })
- .catch(response => {
- getClientErrorObject(response).then(({ error, message }) => {
- setResponseError(error || message || t('Sorry, an error occurred'));
- });
- })
- .finally(() => {
- setIsLoading(false);
+ .catch(response => {
+ getClientErrorObject(response).then(({ error, message }) => {
+ setResponseError(error || message || t('Sorry, an error occurred'));
});
- }
- }, [cappedFormData, isRequest]);
+ })
+ .finally(() => {
+ setIsLoading(false);
+ });
+ }, [cappedFormData, isRequest, queriesResponse, effectiveRowLimit]);
useEffect(() => {
if (errorMessage) {
diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx
index 8be74a02fac..26604e9393a 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import { ReactElement } from 'react';
-import { DatasourceType, VizType } from '@superset-ui/core';
+import { DatasourceType, QueryData, VizType } from '@superset-ui/core';
import { ChartStatus } from 'src/explore/types';
import {
DataTablesPaneProps,
@@ -106,12 +106,16 @@ export const createResultsPaneOnDashboardProps = ({
vizType = VizType.Table,
queryForce = false,
isRequest = true,
+ queriesResponse,
+ rowLimit,
}: {
sliceId: number;
vizType?: string;
errorMessage?: ReactElement;
queryForce?: boolean;
isRequest?: boolean;
+ queriesResponse?: QueryData[] | null;
+ rowLimit?: number;
}) =>
({
isRequest,
@@ -119,10 +123,12 @@ export const createResultsPaneOnDashboardProps = ({
...queryFormData,
slice_id: sliceId,
viz_type: vizType,
+ ...(rowLimit !== undefined ? { row_limit: rowLimit } : {}),
},
queryForce,
isVisible: true,
setForceQuery: jest.fn(),
errorMessage,
canDownload: true,
+ queriesResponse,
}) as ResultsPaneProps;
diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/useResultsPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/useResultsPane.test.tsx
new file mode 100644
index 00000000000..c9f59a4a112
--- /dev/null
+++ b/superset-frontend/src/explore/components/DataTablesPane/test/useResultsPane.test.tsx
@@ -0,0 +1,201 @@
+/**
+ * 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 { screen, render, waitFor } from 'spec/helpers/testing-library';
+import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
+import { getChartDataRequest } from 'src/components/Chart/chartAction';
+import { ResultsPaneOnDashboard } from '../components';
+import { createResultsPaneOnDashboardProps } from './fixture';
+
+// Mocked so each test can assert whether the results branch hit the network.
+jest.mock('src/components/Chart/chartAction', () => ({
+ getChartDataRequest: jest.fn(),
+}));
+
+const mockedGetChartDataRequest = getChartDataRequest as jest.Mock;
+
+beforeAll(() => {
+ setupAGGridModules();
+});
+
+// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
+describe('useResultsPane query data reuse', () => {
+ beforeEach(() => {
+ mockedGetChartDataRequest.mockReset();
+ });
+
+ test('reuses queriesResponse from Redux and skips the results API call (v1 charts)', async () => {
+ const props = createResultsPaneOnDashboardProps({
+ sliceId: 201,
+ queriesResponse: [
+ {
+ colnames: ['genre'],
+ coltypes: [1],
+ data: [{ genre: 'Action' }, { genre: 'Horror' }],
+ rowcount: 2,
+ },
+ ],
+ });
+
+ render(<ResultsPaneOnDashboard {...props} />, { useRedux: true });
+
+ expect(await screen.findByText('Action')).toBeVisible();
+ expect(screen.getByText('Horror')).toBeVisible();
+ expect(mockedGetChartDataRequest).not.toHaveBeenCalled();
+ });
+
+ test('falls back to the results API for legacy charts without a typed queriesResponse', async () => {
+ mockedGetChartDataRequest.mockResolvedValue({
+ json: {
+ result: [
+ {
+ colnames: ['genre'],
+ coltypes: [1],
+ data: [{ genre: 'Drama' }],
+ rowcount: 1,
+ },
+ ],
+ },
+ });
+ const props = createResultsPaneOnDashboardProps({ sliceId: 202 });
+
+ render(<ResultsPaneOnDashboard {...props} />, { useRedux: true });
+
+ expect(await screen.findByText('Drama')).toBeVisible();
+ expect(mockedGetChartDataRequest).toHaveBeenCalledTimes(1);
+ });
+
+ test('falls back to the results API when queriesResponse has no colnames', async () => {
+ mockedGetChartDataRequest.mockResolvedValue({
+ json: {
+ result: [
+ {
+ colnames: ['genre'],
+ coltypes: [1],
+ data: [{ genre: 'Thriller' }],
+ rowcount: 1,
+ },
+ ],
+ },
+ });
+ const props = createResultsPaneOnDashboardProps({
+ sliceId: 203,
+ queriesResponse: [{ data: [{ genre: 'ignored' }] }],
+ });
+
+ render(<ResultsPaneOnDashboard {...props} />, { useRedux: true });
+
+ expect(await screen.findByText('Thriller')).toBeVisible();
+ expect(mockedGetChartDataRequest).toHaveBeenCalledTimes(1);
+ });
+
+ test('falls back to the API when queriesResponse has colnames but not the full v1 shape', async () => {
+ mockedGetChartDataRequest.mockResolvedValue({
+ json: {
+ result: [
+ {
+ colnames: ['genre'],
+ coltypes: [1],
+ data: [{ genre: 'Indie' }],
+ rowcount: 1,
+ },
+ ],
+ },
+ });
+ const props = createResultsPaneOnDashboardProps({
+ sliceId: 207,
+ // colnames present but coltypes/data missing: not a real v1 result
+ queriesResponse: [{ colnames: ['genre'] }],
+ });
+
+ render(<ResultsPaneOnDashboard {...props} />, { useRedux: true });
+
+ expect(await screen.findByText('Indie')).toBeVisible();
+ expect(mockedGetChartDataRequest).toHaveBeenCalledTimes(1);
+ });
+
+ test('reuse path honors the effective row limit (slices Redux data, no extra query)', async () => {
+ const props = createResultsPaneOnDashboardProps({
+ sliceId: 204,
+ rowLimit: 2,
+ queriesResponse: [
+ {
+ colnames: ['genre'],
+ coltypes: [1],
+ data: [
+ { genre: 'Action' },
+ { genre: 'Horror' },
+ { genre: 'Comedy' },
+ { genre: 'Drama' },
+ { genre: 'Sci-Fi' },
+ ],
+ rowcount: 5,
+ },
+ ],
+ });
+
+ render(<ResultsPaneOnDashboard {...props} />, { useRedux: true });
+
+ expect(await screen.findByText('Action')).toBeVisible();
+ expect(screen.getByText('Horror')).toBeVisible();
+ expect(screen.queryByText('Comedy')).not.toBeInTheDocument();
+ expect(screen.queryByText('Sci-Fi')).not.toBeInTheDocument();
+ expect(screen.getByText('2 rows')).toBeVisible();
+ expect(mockedGetChartDataRequest).not.toHaveBeenCalled();
+ });
+
+ test('renders an empty (0 rows) result from reused data without an API call', async () => {
+ const props = createResultsPaneOnDashboardProps({
+ sliceId: 205,
+ queriesResponse: [
+ { colnames: ['genre'], coltypes: [1], data: [], rowcount: 0 },
+ ],
+ });
+
+ render(<ResultsPaneOnDashboard {...props} />, { useRedux: true });
+
+ expect(await screen.findByText('0 rows')).toBeVisible();
+ expect(mockedGetChartDataRequest).not.toHaveBeenCalled();
+ });
+
+ test('clears the force-query flag when reusing Redux data', async () => {
+ const props = createResultsPaneOnDashboardProps({
+ sliceId: 206,
+ queryForce: true,
+ queriesResponse: [
+ {
+ colnames: ['genre'],
+ coltypes: [1],
+ data: [{ genre: 'Action' }],
+ rowcount: 1,
+ },
+ ],
+ });
+ const setForceQuery = jest.fn();
+
+ render(
+ <ResultsPaneOnDashboard {...props} setForceQuery={setForceQuery} />,
+ {
+ useRedux: true,
+ },
+ );
+
+ await waitFor(() => expect(setForceQuery).toHaveBeenCalledWith(false));
+ expect(mockedGetChartDataRequest).not.toHaveBeenCalled();
+ });
+});
diff --git a/superset-frontend/src/explore/components/DataTablesPane/types.ts b/superset-frontend/src/explore/components/DataTablesPane/types.ts
index 2d89fe5d84f..411e0ef3bff 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/types.ts
+++ b/superset-frontend/src/explore/components/DataTablesPane/types.ts
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { JsonObject, LatestQueryFormData } from '@superset-ui/core';
+import { JsonObject, LatestQueryFormData, QueryData } from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import type { ChartStatus, Datasource } from 'src/explore/types';
@@ -36,6 +36,7 @@ export interface DataTablesPaneProps {
errorMessage?: React.ReactNode;
setForceQuery: SetForceQueryAction;
canDownload: boolean;
+ queriesResponse?: QueryData[] | null;
}
export interface ResultsPaneProps {
@@ -51,6 +52,7 @@ export interface ResultsPaneProps {
canDownload: boolean;
// Optional map of column/metric name -> verbose label
columnDisplayNames?: Record<string, string>;
+ queriesResponse?: QueryData[] | null;
}
export interface SamplesPaneProps {
diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
index 55a3b7647cf..3145bda73a2 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
@@ -560,6 +560,7 @@ const ExploreChartPanel = ({
errorMessage={errorMessage}
setForceQuery={actions.setForceQuery}
canDownload={canDownload}
+ queriesResponse={chart.queriesResponse}
/>
</Split>
{showDatasetModal && (