diff --git a/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.test.tsx
index e01556f33696..9a8a33d4de21 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.test.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.test.tsx
@@ -20,25 +20,25 @@
import { fireEvent, render } from '@superset-ui/core/spec';
import Tabs, { EditableTabs, LineEditableTabs } from './Tabs';
-describe('Tabs', () => {
- const defaultItems = [
- {
- key: '1',
- label: 'Tab 1',
- children:
Tab 1 content
,
- },
- {
- key: '2',
- label: 'Tab 2',
- children: Tab 2 content
,
- },
- {
- key: '3',
- label: 'Tab 3',
- children: Tab 3 content
,
- },
- ];
+const defaultItems = [
+ {
+ key: '1',
+ label: 'Tab 1',
+ children: Tab 1 content
,
+ },
+ {
+ key: '2',
+ label: 'Tab 2',
+ children: Tab 2 content
,
+ },
+ {
+ key: '3',
+ label: 'Tab 3',
+ children: Tab 3 content
,
+ },
+];
+describe('Tabs', () => {
describe('Basic Tabs', () => {
it('should render tabs with default props', () => {
const { getByText, container } = render();
@@ -284,6 +284,7 @@ describe('Tabs', () => {
describe('Styling Integration', () => {
it('should accept and apply custom CSS classes', () => {
const { container } = render(
+ // eslint-disable-next-line react/forbid-component-props
,
);
@@ -295,6 +296,7 @@ describe('Tabs', () => {
it('should accept and apply custom styles', () => {
const customStyle = { minHeight: '200px' };
const { container } = render(
+ // eslint-disable-next-line react/forbid-component-props
,
);
@@ -304,3 +306,72 @@ describe('Tabs', () => {
});
});
});
+
+test('fullHeight prop renders component hierarchy correctly', () => {
+ const { container } = render();
+
+ const tabsElement = container.querySelector('.ant-tabs');
+ const contentHolder = container.querySelector('.ant-tabs-content-holder');
+ const content = container.querySelector('.ant-tabs-content');
+ const tabPane = container.querySelector('.ant-tabs-tabpane');
+
+ expect(tabsElement).toBeInTheDocument();
+ expect(contentHolder).toBeInTheDocument();
+ expect(content).toBeInTheDocument();
+ expect(tabPane).toBeInTheDocument();
+ expect(tabsElement?.contains(contentHolder as Node)).toBe(true);
+ expect(contentHolder?.contains(content as Node)).toBe(true);
+ expect(content?.contains(tabPane as Node)).toBe(true);
+});
+
+test('fullHeight prop maintains structure when content updates', () => {
+ const { container, rerender } = render(
+ ,
+ );
+
+ const initialTabsElement = container.querySelector('.ant-tabs');
+
+ const newItems = [
+ ...defaultItems,
+ {
+ key: '4',
+ label: 'Tab 4',
+ children: New tab content
,
+ },
+ ];
+
+ rerender();
+
+ const updatedTabsElement = container.querySelector('.ant-tabs');
+ const updatedContentHolder = container.querySelector(
+ '.ant-tabs-content-holder',
+ );
+
+ expect(updatedTabsElement).toBeInTheDocument();
+ expect(updatedContentHolder).toBeInTheDocument();
+ expect(initialTabsElement).toBe(updatedTabsElement);
+});
+
+test('fullHeight prop works with allowOverflow to handle tall content', () => {
+ const { container } = render(
+ ,
+ );
+
+ const tabsElement = container.querySelector('.ant-tabs') as HTMLElement;
+ const contentHolder = container.querySelector(
+ '.ant-tabs-content-holder',
+ ) as HTMLElement;
+
+ expect(tabsElement).toBeInTheDocument();
+ expect(contentHolder).toBeInTheDocument();
+
+ // Verify overflow handling is not restricted
+ const holderStyles = window.getComputedStyle(contentHolder);
+ expect(holderStyles.overflow).not.toBe('hidden');
+});
+
+test('fullHeight prop handles empty items array', () => {
+ const { container } = render();
+
+ expect(container.querySelector('.ant-tabs')).toBeInTheDocument();
+});
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.tsx b/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.tsx
index 2d99a0593bf1..291b16efea14 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.tsx
@@ -25,12 +25,14 @@ import type { SerializedStyles } from '@emotion/react';
export interface TabsProps extends AntdTabsProps {
allowOverflow?: boolean;
+ fullHeight?: boolean;
contentStyle?: SerializedStyles;
}
const StyledTabs = ({
animated = false,
allowOverflow = true,
+ fullHeight = false,
tabBarStyle,
contentStyle,
...props
@@ -46,9 +48,17 @@ const StyledTabs = ({
tabBarStyle={mergedStyle}
css={theme => css`
overflow: ${allowOverflow ? 'visible' : 'hidden'};
+ ${fullHeight && 'height: 100%;'}
.ant-tabs-content-holder {
overflow: ${allowOverflow ? 'visible' : 'auto'};
+ ${fullHeight && 'height: 100%;'}
+ }
+ .ant-tabs-content {
+ ${fullHeight && 'height: 100%;'}
+ }
+ .ant-tabs-tabpane {
+ ${fullHeight && 'height: 100%;'}
${contentStyle}
}
.ant-tabs-tab {
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx
index de27490662a3..713b3fdb8835 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx
@@ -58,30 +58,52 @@ jest.mock('src/dashboard/actions/dashboardState', () => ({
jest.mock('src/components/ResizableSidebar/useStoredSidebarWidth');
// mock following dependent components to fix the prop warnings
-jest.mock('@superset-ui/core/components/Select/Select', () => () => (
-
-));
-jest.mock('@superset-ui/core/components/Select/AsyncSelect', () => () => (
-
-));
-jest.mock('@superset-ui/core/components/PageHeaderWithActions', () => ({
- PageHeaderWithActions: () => (
+jest.mock('@superset-ui/core/components/Select/Select', () => {
+ const MockSelect = () => ;
+ MockSelect.displayName = 'MockSelect';
+ return MockSelect;
+});
+jest.mock('@superset-ui/core/components/Select/AsyncSelect', () => {
+ const MockAsyncSelect = () => ;
+ MockAsyncSelect.displayName = 'MockAsyncSelect';
+ return MockAsyncSelect;
+});
+jest.mock('@superset-ui/core/components/PageHeaderWithActions', () => {
+ const MockPageHeaderWithActions = () => (
- ),
-}));
+ );
+ MockPageHeaderWithActions.displayName = 'MockPageHeaderWithActions';
+ return {
+ PageHeaderWithActions: MockPageHeaderWithActions,
+ };
+});
jest.mock(
'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal',
- () => () => ,
+ () => {
+ const MockFiltersConfigModal = () => (
+
+ );
+ MockFiltersConfigModal.displayName = 'MockFiltersConfigModal';
+ return MockFiltersConfigModal;
+ },
);
-jest.mock('src/dashboard/components/BuilderComponentPane', () => () => (
-
-));
-jest.mock('src/dashboard/components/nativeFilters/FilterBar', () => () => (
-
-));
-jest.mock('src/dashboard/containers/DashboardGrid', () => () => (
-
-));
+jest.mock('src/dashboard/components/BuilderComponentPane', () => {
+ const MockBuilderComponentPane = () => (
+
+ );
+ MockBuilderComponentPane.displayName = 'MockBuilderComponentPane';
+ return MockBuilderComponentPane;
+});
+jest.mock('src/dashboard/components/nativeFilters/FilterBar', () => {
+ const MockFilterBar = () => ;
+ MockFilterBar.displayName = 'MockFilterBar';
+ return MockFilterBar;
+});
+jest.mock('src/dashboard/containers/DashboardGrid', () => {
+ const MockDashboardGrid = () => ;
+ MockDashboardGrid.displayName = 'MockDashboardGrid';
+ return MockDashboardGrid;
+});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('DashboardBuilder', () => {
@@ -178,8 +200,8 @@ describe('DashboardBuilder', () => {
dashboardLayout: undoableDashboardLayoutWithTabs,
});
const parentSize = await findByTestId('grid-container');
- const first_tab = screen.getByText('tab1');
- expect(first_tab).toBeInTheDocument();
+ const firstTab = screen.getByText('tab1');
+ expect(firstTab).toBeInTheDocument();
const tabPanels = within(parentSize).getAllByRole('tabpanel', {
// to include invisible tab panels
hidden: false,
@@ -198,9 +220,9 @@ describe('DashboardBuilder', () => {
},
});
const parentSize = await findByTestId('grid-container');
- const second_tab = screen.getByText('tab2');
- expect(second_tab).toBeInTheDocument();
- fireEvent.click(second_tab);
+ const secondTab = screen.getByText('tab2');
+ expect(secondTab).toBeInTheDocument();
+ fireEvent.click(secondTab);
const tabPanels = within(parentSize).getAllByRole('tabpanel', {
// to include invisible tab panels
hidden: true,
@@ -356,3 +378,67 @@ describe('DashboardBuilder', () => {
expect(queryByTestId('dashboard-filters-panel')).not.toBeInTheDocument();
});
});
+
+test('should render ParentSize wrapper with height 100% for tabs', async () => {
+ (useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
+ 100,
+ jest.fn(),
+ ]);
+ (fetchFaveStar as jest.Mock).mockReturnValue({ type: 'mock-action' });
+ (setActiveTab as jest.Mock).mockReturnValue({ type: 'mock-action' });
+
+ const { findByTestId } = render(, {
+ useRedux: true,
+ store: storeWithState({
+ ...mockState,
+ dashboardLayout: undoableDashboardLayoutWithTabs,
+ }),
+ useDnd: true,
+ useTheme: true,
+ });
+
+ const gridContainer = await findByTestId('grid-container');
+ const parentSizeWrapper = gridContainer.querySelector('div');
+ const tabPanels = within(gridContainer).getAllByRole('tabpanel', {
+ hidden: true,
+ });
+
+ expect(gridContainer).toBeInTheDocument();
+ expect(parentSizeWrapper).toBeInTheDocument();
+ expect(tabPanels.length).toBeGreaterThan(0);
+});
+
+test('should maintain layout when switching between tabs', async () => {
+ (useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
+ 100,
+ jest.fn(),
+ ]);
+ (fetchFaveStar as jest.Mock).mockReturnValue({ type: 'mock-action' });
+ (setActiveTab as jest.Mock).mockReturnValue({ type: 'mock-action' });
+ (setDirectPathToChild as jest.Mock).mockImplementation(arg0 => ({
+ type: 'type',
+ arg0,
+ }));
+
+ const { findByTestId } = render(, {
+ useRedux: true,
+ store: storeWithState({
+ ...mockState,
+ dashboardLayout: undoableDashboardLayoutWithTabs,
+ }),
+ useDnd: true,
+ useTheme: true,
+ });
+
+ const gridContainer = await findByTestId('grid-container');
+
+ fireEvent.click(screen.getByText('tab1'));
+ fireEvent.click(screen.getByText('tab2'));
+
+ const tabPanels = within(gridContainer).getAllByRole('tabpanel', {
+ hidden: true,
+ });
+
+ expect(gridContainer).toBeInTheDocument();
+ expect(tabPanels.length).toBeGreaterThan(0);
+});
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
index f4a75bb405cf..d425ddcc45ee 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
@@ -298,7 +298,7 @@ const StyledDashboardContent = styled.div<{
/* this is the ParentSize wrapper */
& > div:first-child {
- height: inherit !important;
+ height: 100% !important;
}
}
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
index 9ff41a99c294..0eb3f8f014ca 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
@@ -305,6 +305,7 @@ const DashboardContainer: FC = ({ topLevelTabs }) => {
renderTabBar={renderTabBar}
animated={false}
allowOverflow
+ fullHeight
onFocus={handleFocus}
items={tabItems}
tabBarStyle={{ paddingLeft: 0 }}
diff --git a/superset-frontend/src/dashboard/components/DashboardGrid.jsx b/superset-frontend/src/dashboard/components/DashboardGrid.jsx
index a344ab5d0bc7..1ea7fec11d89 100644
--- a/superset-frontend/src/dashboard/components/DashboardGrid.jsx
+++ b/superset-frontend/src/dashboard/components/DashboardGrid.jsx
@@ -54,6 +54,9 @@ const DashboardEmptyStateContainer = styled.div`
bottom: 0;
left: 0;
right: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
`;
const GridContent = styled.div`
diff --git a/superset-frontend/src/dashboard/components/DashboardGrid.test.jsx b/superset-frontend/src/dashboard/components/DashboardGrid.test.jsx
index a81d6f4e9a41..9f21e35d1073 100644
--- a/superset-frontend/src/dashboard/components/DashboardGrid.test.jsx
+++ b/superset-frontend/src/dashboard/components/DashboardGrid.test.jsx
@@ -24,22 +24,22 @@ import newComponentFactory from 'src/dashboard/util/newComponentFactory';
import { DASHBOARD_GRID_TYPE } from 'src/dashboard/util/componentTypes';
import { GRID_COLUMN_COUNT } from 'src/dashboard/util/constants';
-jest.mock(
- 'src/dashboard/containers/DashboardComponent',
- () =>
- ({ onResizeStart, onResizeStop }) => (
-
- ),
-);
+jest.mock('src/dashboard/containers/DashboardComponent', () => {
+ const MockDashboardComponent = ({ onResizeStart, onResizeStop }) => (
+
+ );
+ MockDashboardComponent.displayName = 'MockDashboardComponent';
+ return MockDashboardComponent;
+});
const props = {
depth: 1,
@@ -106,3 +106,51 @@ test('should call resizeComponent when a child DashboardComponent calls resizeSt
height: 3,
});
});
+
+test('should apply flexbox centering and absolute positioning to empty state', () => {
+ const { container } = setup({
+ gridComponent: { ...props.gridComponent, children: [] },
+ editMode: true,
+ canEdit: true,
+ setEditMode: jest.fn(),
+ dashboardId: 1,
+ });
+
+ const dashboardGrid = container.querySelector('.dashboard-grid');
+ const emptyState = dashboardGrid?.previousElementSibling;
+
+ expect(emptyState).toBeInTheDocument();
+
+ const styles = window.getComputedStyle(emptyState);
+ expect(styles.display).toBe('flex');
+ expect(styles.alignItems).toBe('center');
+ expect(styles.justifyContent).toBe('center');
+ expect(styles.position).toBe('absolute');
+});
+
+test('should render empty state in both edit and view modes', () => {
+ const { container: editContainer } = setup({
+ gridComponent: { ...props.gridComponent, children: [] },
+ editMode: true,
+ canEdit: true,
+ setEditMode: jest.fn(),
+ dashboardId: 1,
+ });
+
+ const { container: viewContainer } = setup({
+ gridComponent: { ...props.gridComponent, children: [] },
+ editMode: false,
+ canEdit: true,
+ setEditMode: jest.fn(),
+ dashboardId: 1,
+ });
+
+ const editDashboardGrid = editContainer.querySelector('.dashboard-grid');
+ const editEmptyState = editDashboardGrid?.previousElementSibling;
+
+ const viewDashboardGrid = viewContainer.querySelector('.dashboard-grid');
+ const viewEmptyState = viewDashboardGrid?.previousElementSibling;
+
+ expect(editEmptyState).toBeInTheDocument();
+ expect(viewEmptyState).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
index b1bb5a48e7ec..11c724971859 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
@@ -111,6 +111,7 @@ const TabsRenderer = memo(
type={editMode ? 'editable-card' : 'card'}
items={tabItems}
tabBarStyle={{ paddingLeft: tabBarPaddingLeft }}
+ fullHeight
/>
),