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 /> ),