Compare commits

...
Author SHA1 Message Date
Enzo MartellucciandClaude Sonnet 5 50f4802bbf fix(dashboard): reconcile stale results tab in Chart Data modal
Address PR #43454 review feedback: reuse DataTablesPane's stale-tab
fallback so ResultsPaneOnDashboard doesn't render blank when a mixed
chart's active results tab disappears, and drop CSS rules now
redundant with the fullHeight prop.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 12:16:21 +02:00
Enzo Martellucci fd07663dc1 fix(dashboard): stretch Chart Data modal results grid to fill available height 2026-08-24 13:32:15 +02:00
5 changed files with 112 additions and 27 deletions
@@ -29,22 +29,7 @@ import {
} from 'src/utils/localStorageHelpers';
import { SamplesPane, useResultsPane } from './components';
import { DataTablesPaneProps, ResultTypes } from './types';
/**
* A mixed chart can be reconfigured to return fewer result panes than before
* (e.g. dropping a query), which removes the corresponding results tab. If the
* selected tab was one of those, the active key goes stale and the data panel
* renders blank until the user reselects a valid tab. Returns the first
* results tab to fall back to in that case, otherwise undefined.
*/
export const getStaleResultsTabFallback = (
activeTabKey: string,
resultsTabKeys: string[],
): string | undefined =>
activeTabKey.startsWith(ResultTypes.Results) &&
!resultsTabKeys.includes(activeTabKey)
? ResultTypes.Results
: undefined;
import { getStaleResultsTabFallback } from './utils';
const StyledDiv = styled.div`
${() => `
@@ -20,22 +20,15 @@ import { t } from '@apache-superset/core/translation';
import { styled } from '@apache-superset/core/theme';
import Tabs from '@superset-ui/core/components/Tabs';
import { ResultTypes, ResultsPaneProps } from '../types';
import { getStaleResultsTabFallback } from '../utils';
import { useResultsPane } from './useResultsPane';
import { useState } from 'react';
import { useEffect, useState } from 'react';
const Wrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
.ant-tabs {
height: 100%;
}
.ant-tabs-body {
height: 100%;
}
.ant-tabs-content {
display: flex;
flex-direction: column;
@@ -86,9 +79,25 @@ export const ResultsPaneOnDashboard = ({
};
});
const resultsTabFallback = getStaleResultsTabFallback(
activeTabKey,
items.map(({ key }) => key),
);
useEffect(() => {
if (resultsTabFallback) {
setActiveTabKey(resultsTabFallback);
}
}, [resultsTabFallback]);
return (
<Wrapper>
<Tabs activeKey={activeTabKey} onChange={setActiveTabKey} items={items} />
<Tabs
fullHeight
activeKey={activeTabKey}
onChange={setActiveTabKey}
items={items}
/>
</Wrapper>
);
};
@@ -20,14 +20,34 @@ import fetchMock from 'fetch-mock';
import {
screen,
render,
act,
waitForElementToBeRemoved,
waitFor,
} from 'spec/helpers/testing-library';
import { ChartMetadata, ChartPlugin, VizType } from '@superset-ui/core';
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
import Tabs from '@superset-ui/core/components/Tabs';
import { ResultsPaneOnDashboard } from '../components';
import { useResultsPane } from '../components/useResultsPane';
import { createResultsPaneOnDashboardProps } from './fixture';
// `fullHeight`'s CSS isn't testable under jsdom (no `importSource` for the
// `css` prop in jest's babel config), so spy on call args instead.
jest.mock('@superset-ui/core/components/Tabs', () => {
const actual = jest.requireActual('@superset-ui/core/components/Tabs');
return { __esModule: true, ...actual, default: jest.fn(actual.default) };
});
// Wraps the real hook; only overridden below to avoid mounting a second
// real AG Grid instance, which jsdom doesn't support.
jest.mock('../components/useResultsPane', () => {
const actual = jest.requireActual('../components/useResultsPane');
return {
__esModule: true,
useResultsPane: jest.fn(actual.useResultsPane),
};
});
beforeAll(() => {
setupAGGridModules();
});
@@ -106,6 +126,10 @@ describe('ResultsPaneOnDashboard', () => {
expect(
await findByText('No results were returned for this query'),
).toBeVisible();
expect(Tabs).toHaveBeenCalledWith(
expect.objectContaining({ fullHeight: true }),
expect.anything(),
);
});
test('render errorMessage', async () => {
@@ -219,4 +243,36 @@ describe('ResultsPaneOnDashboard', () => {
expect(tab2).toBeVisible();
expect(tab3).toBeNull();
});
test('falls back to the first results tab when the active one disappears', async () => {
const mockedUseResultsPane = useResultsPane as jest.Mock;
mockedUseResultsPane.mockReturnValue([<div key="a" />, <div key="b" />]);
const props = createResultsPaneOnDashboardProps({ sliceId: 999 });
const { rerender } = render(<ResultsPaneOnDashboard {...props} />, {
useRedux: true,
});
const latestTabsProps = () => {
const { calls } = (Tabs as unknown as jest.Mock).mock;
return calls[calls.length - 1][0];
};
expect(latestTabsProps().items.map((i: { key: string }) => i.key)).toEqual([
'results',
'results 2',
]);
act(() => {
latestTabsProps().onChange('results 2');
});
expect(latestTabsProps().activeKey).toBe('results 2');
// A mixed chart dropped from two query results to one, removing "results 2"
mockedUseResultsPane.mockReturnValue([<div key="a" />]);
rerender(<ResultsPaneOnDashboard {...props} />);
await waitFor(() => {
expect(latestTabsProps().activeKey).toBe('results');
});
});
});
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { getStaleResultsTabFallback } from '../DataTablesPane';
import { getStaleResultsTabFallback } from '../utils';
import { ResultTypes } from '../types';
test('keeps the active tab when it still exists', () => {
@@ -0,0 +1,35 @@
/**
* 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 { ResultTypes } from './types';
/**
* A mixed chart can be reconfigured to return fewer result panes than before
* (e.g. dropping a query), which removes the corresponding results tab. If the
* selected tab was one of those, the active key goes stale and the data panel
* renders blank until the user reselects a valid tab. Returns the first
* results tab to fall back to in that case, otherwise undefined.
*/
export const getStaleResultsTabFallback = (
activeTabKey: string,
resultsTabKeys: string[],
): string | undefined =>
activeTabKey.startsWith(ResultTypes.Results) &&
!resultsTabKeys.includes(activeTabKey)
? ResultTypes.Results
: undefined;