This is an automated email from the ASF dual-hosted git repository.
jli pushed a commit to branch 6.0
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/6.0 by this push:
new d94c92db01 fix(dashboard): normalize spacings and background colors
(#35001)
d94c92db01 is described below
commit d94c92db016f96cd8c6ab02fea53c75af4203c54
Author: Gabriel Torres Ruiz <[email protected]>
AuthorDate: Fri Sep 5 23:13:42 2025 -0300
fix(dashboard): normalize spacings and background colors (#35001)
(cherry picked from commit 0fce5ecfa5295f9978f873f2930ca972b868cfe5)
---
.../src/components/Tabs/Tabs.test.tsx | 306 +++++++++++++++++++
.../superset-ui-core/src/components/Tabs/Tabs.tsx | 6 +-
.../src/dashboard/actions/dashboardState.js | 5 +
.../BuilderComponentPane.test.tsx | 9 +-
.../components/BuilderComponentPane/index.tsx | 152 +++++-----
.../DashboardBuilder/DashboardBuilder.tsx | 16 +-
.../DashboardBuilder/DashboardContainer.tsx | 8 +-
.../DashboardBuilder/DashboardWrapper.tsx | 6 +-
.../dashboard/components/DashboardBuilder/state.ts | 29 +-
.../gridComponents/{ => Chart}/Chart.jsx | 20 +-
.../gridComponents/{ => Chart}/Chart.test.jsx | 2 +-
.../Chart/index.js} | 19 +-
.../{ => ChartHolder}/ChartHolder.test.tsx | 8 +-
.../{ => ChartHolder}/ChartHolder.tsx | 0
.../ChartHolder/index.ts} | 19 +-
.../gridComponents/{ => Column}/Column.jsx | 0
.../gridComponents/{ => Column}/Column.test.jsx | 2 +-
.../Column/index.js} | 19 +-
.../gridComponents/{ => Divider}/Divider.jsx | 8 +-
.../gridComponents/{ => Divider}/Divider.test.jsx | 2 +-
.../Divider/index.js} | 19 +-
.../DynamicComponent/DynamicComponent.test.tsx | 329 +++++++++++++++++++++
.../{ => DynamicComponent}/DynamicComponent.tsx | 34 +--
.../DynamicComponent/index.ts} | 19 +-
.../gridComponents/{ => Header}/Header.jsx | 0
.../gridComponents/{ => Header}/Header.test.jsx | 2 +-
.../Header/index.js} | 19 +-
.../gridComponents/{ => Markdown}/Markdown.jsx | 0
.../{ => Markdown}/Markdown.test.jsx | 2 +-
.../Markdown/index.js} | 19 +-
.../components/gridComponents/{ => Row}/Row.jsx | 2 +-
.../gridComponents/{ => Row}/Row.test.jsx | 2 +-
.../Row/index.js} | 19 +-
.../components/gridComponents/Tab.test.jsx | 141 ---------
.../components/gridComponents/{ => Tab}/Tab.jsx | 0
.../gridComponents/{ => Tab}/Tab.test.tsx | 2 +-
.../Tab/index.js} | 20 +-
.../components/gridComponents/Tabs.test.jsx | 203 -------------
.../components/gridComponents/{ => Tabs}/Tabs.jsx | 159 +++++-----
.../gridComponents/{ => Tabs}/Tabs.test.tsx | 33 ++-
.../Tabs/index.js} | 19 +-
.../TabsRenderer/TabsRenderer.test.tsx | 201 +++++++++++++
.../gridComponents/TabsRenderer/TabsRenderer.tsx | 121 ++++++++
.../TabsRenderer/index.ts} | 20 +-
.../dashboard/components/gridComponents/index.js | 10 -
.../src/dashboard/reducers/dashboardState.js | 7 +
.../src/dashboard/reducers/dashboardState.test.js | 17 ++
.../src/dashboard/reducers/dashboardState.test.ts | 140 ++++++---
superset-frontend/src/dashboard/types.ts | 1 +
49 files changed, 1383 insertions(+), 813 deletions(-)
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
new file mode 100644
index 0000000000..e01556f336
--- /dev/null
+++
b/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.test.tsx
@@ -0,0 +1,306 @@
+/**
+ * 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 { fireEvent, render } from '@superset-ui/core/spec';
+import Tabs, { EditableTabs, LineEditableTabs } from './Tabs';
+
+describe('Tabs', () => {
+ const defaultItems = [
+ {
+ key: '1',
+ label: 'Tab 1',
+ children: <div data-testid="tab1-content">Tab 1 content</div>,
+ },
+ {
+ key: '2',
+ label: 'Tab 2',
+ children: <div data-testid="tab2-content">Tab 2 content</div>,
+ },
+ {
+ key: '3',
+ label: 'Tab 3',
+ children: <div data-testid="tab3-content">Tab 3 content</div>,
+ },
+ ];
+
+ describe('Basic Tabs', () => {
+ it('should render tabs with default props', () => {
+ const { getByText, container } = render(<Tabs items={defaultItems} />);
+
+ expect(getByText('Tab 1')).toBeInTheDocument();
+ expect(getByText('Tab 2')).toBeInTheDocument();
+ expect(getByText('Tab 3')).toBeInTheDocument();
+
+ const activeTabContent = container.querySelector(
+ '.ant-tabs-tabpane-active',
+ );
+
+ expect(activeTabContent).toBeDefined();
+ expect(
+ activeTabContent?.querySelector('[data-testid="tab1-content"]'),
+ ).toBeDefined();
+ });
+
+ it('should render tabs component structure', () => {
+ const { container } = render(<Tabs items={defaultItems} />);
+ const tabsElement = container.querySelector('.ant-tabs');
+ const tabsNav = container.querySelector('.ant-tabs-nav');
+ const tabsContent = container.querySelector('.ant-tabs-content-holder');
+
+ expect(tabsElement).toBeDefined();
+ expect(tabsNav).toBeDefined();
+ expect(tabsContent).toBeDefined();
+ });
+
+ it('should apply default tabBarStyle with padding', () => {
+ const { container } = render(<Tabs items={defaultItems} />);
+ const tabsNav = container.querySelector('.ant-tabs-nav') as HTMLElement;
+
+ // Check that tabBarStyle is applied (default padding is added)
+ expect(tabsNav?.style?.paddingLeft).toBeDefined();
+ });
+
+ it('should merge custom tabBarStyle with defaults', () => {
+ const customStyle = { paddingRight: '20px', backgroundColor: 'red' };
+ const { container } = render(
+ <Tabs items={defaultItems} tabBarStyle={customStyle} />,
+ );
+ const tabsNav = container.querySelector('.ant-tabs-nav') as HTMLElement;
+
+ expect(tabsNav?.style?.paddingLeft).toBeDefined();
+ expect(tabsNav?.style?.paddingRight).toBe('20px');
+ expect(tabsNav?.style?.backgroundColor).toBe('red');
+ });
+
+ it('should handle allowOverflow prop', () => {
+ const { container: allowContainer } = render(
+ <Tabs items={defaultItems} allowOverflow />,
+ );
+ const { container: disallowContainer } = render(
+ <Tabs items={defaultItems} allowOverflow={false} />,
+ );
+
+ expect(allowContainer.querySelector('.ant-tabs')).toBeDefined();
+ expect(disallowContainer.querySelector('.ant-tabs')).toBeDefined();
+ });
+
+ it('should disable animation by default', () => {
+ const { container } = render(<Tabs items={defaultItems} />);
+ const tabsElement = container.querySelector('.ant-tabs');
+
+ expect(tabsElement?.className).not.toContain('ant-tabs-animated');
+ });
+
+ it('should handle tab change events', () => {
+ const onChangeMock = jest.fn();
+ const { getByText } = render(
+ <Tabs items={defaultItems} onChange={onChangeMock} />,
+ );
+
+ fireEvent.click(getByText('Tab 2'));
+
+ expect(onChangeMock).toHaveBeenCalledWith('2');
+ });
+
+ it('should pass through additional props to Antd Tabs', () => {
+ const onTabClickMock = jest.fn();
+ const { getByText } = render(
+ <Tabs
+ items={defaultItems}
+ onTabClick={onTabClickMock}
+ size="large"
+ centered
+ />,
+ );
+
+ fireEvent.click(getByText('Tab 2'));
+
+ expect(onTabClickMock).toHaveBeenCalled();
+ });
+ });
+
+ describe('EditableTabs', () => {
+ it('should render with editable features', () => {
+ const { container } = render(<EditableTabs items={defaultItems} />);
+
+ const tabsElement = container.querySelector('.ant-tabs');
+
+ expect(tabsElement?.className).toContain('ant-tabs-card');
+ expect(tabsElement?.className).toContain('ant-tabs-editable-card');
+ });
+
+ it('should handle onEdit callback for add/remove actions', () => {
+ const onEditMock = jest.fn();
+ const itemsWithRemove = defaultItems.map(item => ({
+ ...item,
+ closable: true,
+ }));
+
+ const { container } = render(
+ <EditableTabs items={itemsWithRemove} onEdit={onEditMock} />,
+ );
+
+ const removeButton = container.querySelector('.ant-tabs-tab-remove');
+ expect(removeButton).toBeDefined();
+
+ fireEvent.click(removeButton!);
+ expect(onEditMock).toHaveBeenCalledWith(expect.any(String), 'remove');
+ });
+
+ it('should have default props set correctly', () => {
+ expect(EditableTabs.defaultProps?.type).toBe('editable-card');
+ expect(EditableTabs.defaultProps?.animated).toEqual({
+ inkBar: true,
+ tabPane: false,
+ });
+ });
+ });
+
+ describe('LineEditableTabs', () => {
+ it('should render as line-style editable tabs', () => {
+ const { container } = render(<LineEditableTabs items={defaultItems} />);
+
+ const tabsElement = container.querySelector('.ant-tabs');
+
+ expect(tabsElement?.className).toContain('ant-tabs-card');
+ expect(tabsElement?.className).toContain('ant-tabs-editable-card');
+ });
+
+ it('should render with line-specific styling', () => {
+ const { container } = render(<LineEditableTabs items={defaultItems} />);
+
+ const inkBar = container.querySelector('.ant-tabs-ink-bar');
+ expect(inkBar).toBeDefined();
+ });
+ });
+
+ describe('TabPane Legacy Support', () => {
+ it('should support TabPane component access', () => {
+ expect(Tabs.TabPane).toBeDefined();
+ expect(EditableTabs.TabPane).toBeDefined();
+ expect(LineEditableTabs.TabPane).toBeDefined();
+ });
+
+ it('should render using legacy TabPane syntax', () => {
+ const { getByText, container } = render(
+ <Tabs>
+ <Tabs.TabPane tab="Legacy Tab 1" key="1">
+ <div data-testid="legacy-content-1">Legacy content 1</div>
+ </Tabs.TabPane>
+ <Tabs.TabPane tab="Legacy Tab 2" key="2">
+ <div data-testid="legacy-content-2">Legacy content 2</div>
+ </Tabs.TabPane>
+ </Tabs>,
+ );
+
+ expect(getByText('Legacy Tab 1')).toBeInTheDocument();
+ expect(getByText('Legacy Tab 2')).toBeInTheDocument();
+
+ const activeTabContent = container.querySelector(
+ '.ant-tabs-tabpane-active [data-testid="legacy-content-1"]',
+ );
+
+ expect(activeTabContent).toBeDefined();
+ expect(activeTabContent?.textContent).toBe('Legacy content 1');
+ });
+ });
+
+ describe('Edge Cases', () => {
+ it('should handle empty items array', () => {
+ const { container } = render(<Tabs items={[]} />);
+ const tabsElement = container.querySelector('.ant-tabs');
+
+ expect(tabsElement).toBeDefined();
+ });
+
+ it('should handle undefined items', () => {
+ const { container } = render(<Tabs />);
+ const tabsElement = container.querySelector('.ant-tabs');
+
+ expect(tabsElement).toBeDefined();
+ });
+
+ it('should handle tabs with no content', () => {
+ const itemsWithoutContent = [
+ { key: '1', label: 'Tab 1' },
+ { key: '2', label: 'Tab 2' },
+ ];
+
+ const { getByText } = render(<Tabs items={itemsWithoutContent} />);
+
+ expect(getByText('Tab 1')).toBeInTheDocument();
+ expect(getByText('Tab 2')).toBeInTheDocument();
+ });
+
+ it('should handle allowOverflow default value', () => {
+ const { container } = render(<Tabs items={defaultItems} />);
+ expect(container.querySelector('.ant-tabs')).toBeDefined();
+ });
+ });
+
+ describe('Accessibility', () => {
+ it('should render with proper ARIA roles', () => {
+ const { container } = render(<Tabs items={defaultItems} />);
+
+ const tablist = container.querySelector('[role="tablist"]');
+ const tabs = container.querySelectorAll('[role="tab"]');
+
+ expect(tablist).toBeDefined();
+ expect(tabs.length).toBe(3);
+ });
+
+ it('should support keyboard navigation', () => {
+ const { container, getByText } = render(<Tabs items={defaultItems} />);
+
+ const firstTab = container.querySelector('[role="tab"]');
+ const secondTab = getByText('Tab 2');
+
+ if (firstTab) {
+ fireEvent.keyDown(firstTab, { key: 'ArrowRight', code: 'ArrowRight' });
+ }
+
+ fireEvent.click(secondTab);
+
+ expect(secondTab).toBeInTheDocument();
+ });
+ });
+
+ describe('Styling Integration', () => {
+ it('should accept and apply custom CSS classes', () => {
+ const { container } = render(
+ <Tabs items={defaultItems} className="custom-tabs-class" />,
+ );
+
+ const tabsElement = container.querySelector('.ant-tabs');
+
+ expect(tabsElement?.className).toContain('custom-tabs-class');
+ });
+
+ it('should accept and apply custom styles', () => {
+ const customStyle = { minHeight: '200px' };
+ const { container } = render(
+ <Tabs items={defaultItems} style={customStyle} />,
+ );
+
+ const tabsElement = container.querySelector('.ant-tabs') as HTMLElement;
+
+ expect(tabsElement?.style?.minHeight).toBe('200px');
+ });
+ });
+});
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 5ea5c0e509..d834cd05f2 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
@@ -29,14 +29,18 @@ export interface TabsProps extends AntdTabsProps {
const StyledTabs = ({
animated = false,
allowOverflow = true,
+ tabBarStyle,
...props
}: TabsProps) => {
const theme = useTheme();
+ const defaultTabBarStyle = { paddingLeft: theme.sizeUnit * 4 };
+ const mergedStyle = { ...defaultTabBarStyle, ...tabBarStyle };
+
return (
<AntdTabs
animated={animated}
{...props}
- tabBarStyle={{ paddingLeft: theme.sizeUnit * 4 }}
+ tabBarStyle={mergedStyle}
css={theme => css`
overflow: ${allowOverflow ? 'visible' : 'hidden'};
diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js
b/superset-frontend/src/dashboard/actions/dashboardState.js
index 65bc7edc2a..ebd0a6b65b 100644
--- a/superset-frontend/src/dashboard/actions/dashboardState.js
+++ b/superset-frontend/src/dashboard/actions/dashboardState.js
@@ -79,6 +79,11 @@ import {
getDynamicLabelsColors,
} from '../../utils/colorScheme';
+export const TOGGLE_NATIVE_FILTERS_BAR = 'TOGGLE_NATIVE_FILTERS_BAR';
+export function toggleNativeFiltersBar(isOpen) {
+ return { type: TOGGLE_NATIVE_FILTERS_BAR, isOpen };
+}
+
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
export function setUnsavedChanges(hasUnsavedChanges) {
return { type: SET_UNSAVED_CHANGES, payload: { hasUnsavedChanges } };
diff --git
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
b/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
index 24a6a78d3f..fbfebe9a60 100644
---
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
+++
b/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
@@ -25,7 +25,14 @@ jest.mock('src/dashboard/containers/SliceAdder', () => () =>
(
));
test('BuilderComponentPane has correct tabs in correct order', () => {
- render(<BuilderComponentPane topOffset={115} />);
+ render(<BuilderComponentPane topOffset={115} />, {
+ useRedux: true,
+ initialState: {
+ dashboardState: {
+ nativeFiltersBarOpen: false,
+ },
+ },
+ });
const tabs = screen.getAllByRole('tab');
expect(tabs).toHaveLength(2);
expect(tabs[0]).toHaveTextContent('Charts');
diff --git
a/superset-frontend/src/dashboard/components/BuilderComponentPane/index.tsx
b/superset-frontend/src/dashboard/components/BuilderComponentPane/index.tsx
index e8ff251d81..068fe3a9b8 100644
--- a/superset-frontend/src/dashboard/components/BuilderComponentPane/index.tsx
+++ b/superset-frontend/src/dashboard/components/BuilderComponentPane/index.tsx
@@ -19,9 +19,11 @@
/* eslint-env browser */
import { rgba } from 'emotion-rgba';
import Tabs from '@superset-ui/core/components/Tabs';
-import { t, css, SupersetTheme } from '@superset-ui/core';
+import { t, css, SupersetTheme, useTheme } from '@superset-ui/core';
+import { useSelector } from 'react-redux';
import SliceAdder from 'src/dashboard/containers/SliceAdder';
import dashboardComponents from
'src/visualizations/presets/dashboardComponents';
+import { useMemo } from 'react';
import NewColumn from '../gridComponents/new/NewColumn';
import NewDivider from '../gridComponents/new/NewDivider';
import NewHeader from '../gridComponents/new/NewHeader';
@@ -37,81 +39,97 @@ const TABS_KEYS = {
LAYOUT_ELEMENTS: 'LAYOUT_ELEMENTS',
};
-const BuilderComponentPane = ({ topOffset = 0 }) => (
- <div
- data-test="dashboard-builder-sidepane"
- css={css`
- position: sticky;
- right: 0;
- top: ${topOffset}px;
- height: calc(100vh - ${topOffset}px);
- width: ${BUILDER_PANE_WIDTH}px;
- `}
- >
+const BuilderComponentPane = ({ topOffset = 0 }) => {
+ const theme = useTheme();
+ const nativeFiltersBarOpen = useSelector(
+ (state: any) => state.dashboardState.nativeFiltersBarOpen ?? false,
+ );
+
+ const tabBarStyle = useMemo(
+ () => ({
+ paddingLeft: nativeFiltersBarOpen ? 0 : theme.sizeUnit * 4,
+ }),
+ [nativeFiltersBarOpen, theme.sizeUnit],
+ );
+
+ return (
<div
- css={(theme: SupersetTheme) => css`
- position: absolute;
- height: 100%;
+ data-test="dashboard-builder-sidepane"
+ css={css`
+ position: sticky;
+ right: 0;
+ top: ${topOffset}px;
+ height: calc(100vh - ${topOffset}px);
width: ${BUILDER_PANE_WIDTH}px;
- box-shadow: -4px 0 4px 0 ${rgba(theme.colorBorder, 0.1)};
- background-color: ${theme.colorBgBase};
`}
>
- <Tabs
- data-test="dashboard-builder-component-pane-tabs-navigation"
- id="tabs"
+ <div
css={(theme: SupersetTheme) => css`
- line-height: inherit;
- margin-top: ${theme.sizeUnit * 2}px;
+ position: absolute;
height: 100%;
-
- & .ant-tabs-content-holder {
+ width: ${BUILDER_PANE_WIDTH}px;
+ box-shadow: -4px 0 4px 0 ${rgba(theme.colorBorder, 0.1)};
+ background-color: ${theme.colorBgBase};
+ `}
+ >
+ <Tabs
+ data-test="dashboard-builder-component-pane-tabs-navigation"
+ id="tabs"
+ tabBarStyle={tabBarStyle}
+ css={(theme: SupersetTheme) => css`
+ line-height: inherit;
+ margin-top: ${theme.sizeUnit * 2}px;
height: 100%;
- & .ant-tabs-content {
+
+ & .ant-tabs-content-holder {
height: 100%;
+ & .ant-tabs-content {
+ height: 100%;
+ }
}
- }
- `}
- items={[
- {
- key: TABS_KEYS.CHARTS,
- label: t('Charts'),
- children: (
- <div
- css={css`
- height: calc(100vh - ${topOffset * 2}px);
- `}
- >
- <SliceAdder />
- </div>
- ),
- },
- {
- key: TABS_KEYS.LAYOUT_ELEMENTS,
- label: t('Layout elements'),
- children: (
- <>
- <NewTabs />
- <NewRow />
- <NewColumn />
- <NewHeader />
- <NewMarkdown />
- <NewDivider />
- {dashboardComponents
- .getAll()
- .map(({ key: componentKey, metadata }) => (
- <NewDynamicComponent
- metadata={metadata}
- componentKey={componentKey}
- />
- ))}
- </>
- ),
- },
- ]}
- />
+ `}
+ items={[
+ {
+ key: TABS_KEYS.CHARTS,
+ label: t('Charts'),
+ children: (
+ <div
+ css={css`
+ height: calc(100vh - ${topOffset * 2}px);
+ `}
+ >
+ <SliceAdder />
+ </div>
+ ),
+ },
+ {
+ key: TABS_KEYS.LAYOUT_ELEMENTS,
+ label: t('Layout elements'),
+ children: (
+ <>
+ <NewTabs />
+ <NewRow />
+ <NewColumn />
+ <NewHeader />
+ <NewMarkdown />
+ <NewDivider />
+ {dashboardComponents
+ .getAll()
+ .map(({ key: componentKey, metadata }) => (
+ <NewDynamicComponent
+ key={componentKey}
+ metadata={metadata}
+ componentKey={componentKey}
+ />
+ ))}
+ </>
+ ),
+ },
+ ]}
+ />
+ </div>
</div>
- </div>
-);
+ );
+};
export default BuilderComponentPane;
diff --git
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
index 19215399dd..880d5c7fbe 100644
---
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
+++
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
@@ -80,6 +80,7 @@ import DashboardWrapper from './DashboardWrapper';
// @z-index-above-dashboard-charts + 1 = 11
const FiltersPanel = styled.div<{ width: number; hidden: boolean }>`
+ background-color: ${({ theme }) => theme.colorBgContainer};
grid-column: 1;
grid-row: 1 / span 2;
z-index: 11;
@@ -275,6 +276,7 @@ const StyledDashboardContent = styled.div<{
marginLeft: number;
}>`
${({ theme, editMode, marginLeft }) => css`
+ background-color: ${theme.colorBgLayout};
display: flex;
flex-direction: row;
flex-wrap: nowrap;
@@ -291,9 +293,7 @@ const StyledDashboardContent = styled.div<{
width: 0;
flex: 1;
position: relative;
- margin-top: ${theme.sizeUnit * 4}px;
- margin-right: ${theme.sizeUnit * 8}px;
- margin-bottom: ${theme.sizeUnit * 4}px;
+ margin: ${theme.sizeUnit * 4}px;
margin-left: ${marginLeft}px;
${editMode &&
@@ -557,13 +557,9 @@ const DashboardBuilder = () => {
],
);
- const dashboardContentMarginLeft =
- !dashboardFiltersOpen &&
- !editMode &&
- nativeFiltersEnabled &&
- filterBarOrientation !== FilterBarOrientation.Horizontal
- ? 0
- : theme.sizeUnit * 8;
+ const dashboardContentMarginLeft = !editMode
+ ? theme.sizeUnit * 4
+ : theme.sizeUnit * 8;
const renderChild = useCallback(
adjustedWidth => {
diff --git
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
index 784d6020cd..253c17dc42 100644
---
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
+++
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
@@ -70,13 +70,12 @@ type DashboardContainerProps = {
topLevelTabs?: LayoutItem;
};
-export const renderedChartIdsSelector = createSelector(
- [(state: RootState) => state.charts],
- charts =>
+export const renderedChartIdsSelector: (state: RootState) => number[] =
+ createSelector([(state: RootState) => state.charts], charts =>
Object.values(charts)
.filter(chart => chart.chartStatus === 'rendered')
.map(chart => chart.id),
-);
+ );
const useRenderedChartIds = () => {
const renderedChartIds = useSelector<RootState, number[]>(
@@ -297,6 +296,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({
topLevelTabs }) => {
allowOverflow
onFocus={handleFocus}
items={tabItems}
+ tabBarStyle={{ paddingLeft: 0 }}
/>
);
},
diff --git
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx
index 4d5190e79d..f73fe36e9a 100644
---
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx
+++
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { FC, useEffect, useState } from 'react';
+import { FC, PropsWithChildren, useEffect, useState } from 'react';
import { css, styled } from '@superset-ui/core';
import { Constants } from '@superset-ui/core/components';
@@ -113,9 +113,7 @@ const StyledDiv = styled.div`
`}
`;
-type Props = {};
-
-const DashboardWrapper: FC<Props> = ({ children }) => {
+const DashboardWrapper: FC<PropsWithChildren<{}>> = ({ children }) => {
const editMode = useSelector<RootState, boolean>(
state => state.dashboardState.editMode,
);
diff --git
a/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts
b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts
index a338e21a94..c7b7a56fdf 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { useSelector } from 'react-redux';
+import { useSelector, useDispatch } from 'react-redux';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';
@@ -26,23 +26,26 @@ import {
useFilters,
useNativeFiltersDataMask,
} from '../nativeFilters/FilterBar/state';
+import { toggleNativeFiltersBar } from '../../actions/dashboardState';
-// eslint-disable-next-line import/prefer-default-export
export const useNativeFilters = () => {
+ const dispatch = useDispatch();
+
const [isInitialized, setIsInitialized] = useState(false);
+
const showNativeFilters = useSelector<RootState, boolean>(
() => getUrlParam(URL_PARAMS.showFilters) ?? true,
);
const canEdit = useSelector<RootState, boolean>(
({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
);
+ const dashboardFiltersOpen = useSelector<RootState, boolean>(
+ state => state.dashboardState.nativeFiltersBarOpen ?? false,
+ );
const filters = useFilters();
const filterValues = useMemo(() => Object.values(filters), [filters]);
const expandFilters = getUrlParam(URL_PARAMS.expandFilters);
- const [dashboardFiltersOpen, setDashboardFiltersOpen] = useState(
- expandFilters ?? !!filterValues.length,
- );
const nativeFiltersEnabled =
showNativeFilters && (canEdit || (!canEdit && filterValues.length !== 0));
@@ -66,9 +69,13 @@ export const useNativeFilters = () => {
!nativeFiltersEnabled ||
missingInitialFilters.length === 0;
- const toggleDashboardFiltersOpen = useCallback((visible?: boolean) => {
- setDashboardFiltersOpen(prevState => visible ?? !prevState);
- }, []);
+ const toggleDashboardFiltersOpen = useCallback(
+ (visible?: boolean) => {
+ const newState = visible ?? !dashboardFiltersOpen;
+ dispatch(toggleNativeFiltersBar(newState));
+ },
+ [dispatch, dashboardFiltersOpen],
+ );
useEffect(() => {
if (
@@ -77,11 +84,11 @@ export const useNativeFilters = () => {
expandFilters === false ||
(filterValues.length === 0 && nativeFiltersEnabled)
) {
- toggleDashboardFiltersOpen(false);
+ dispatch(toggleNativeFiltersBar(false));
} else {
- toggleDashboardFiltersOpen(true);
+ dispatch(toggleNativeFiltersBar(true));
}
- }, [filterValues.length]);
+ }, [dispatch, filterValues.length, expandFilters, nativeFiltersEnabled]);
useEffect(() => {
if (showDashboard) {
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx
similarity index 96%
rename from superset-frontend/src/dashboard/components/gridComponents/Chart.jsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx
index ec12d98d7b..d81a6ed0f2 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx
@@ -39,26 +39,26 @@ import { URL_PARAMS } from 'src/constants';
import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme';
import exportPivotExcel from 'src/utils/downloadAsPivotExcel';
-import SliceHeader from '../SliceHeader';
-import MissingChart from '../MissingChart';
+import SliceHeader from '../../SliceHeader';
+import MissingChart from '../../MissingChart';
import {
addDangerToast,
addSuccessToast,
-} from '../../../components/MessageToasts/actions';
+} from '../../../../components/MessageToasts/actions';
import {
setFocusedFilterField,
toggleExpandSlice,
unsetFocusedFilterField,
-} from '../../actions/dashboardState';
-import { changeFilter } from '../../actions/dashboardFilters';
-import { refreshChart } from '../../../components/Chart/chartAction';
-import { logEvent } from '../../../logger/actions';
+} from '../../../actions/dashboardState';
+import { changeFilter } from '../../../actions/dashboardFilters';
+import { refreshChart } from '../../../../components/Chart/chartAction';
+import { logEvent } from '../../../../logger/actions';
import {
getActiveFilters,
getAppliedFilterValues,
-} from '../../util/activeDashboardFilters';
-import getFormDataWithExtraFilters from
'../../util/charts/getFormDataWithExtraFilters';
-import { PLACEHOLDER_DATASOURCE } from '../../constants';
+} from '../../../util/activeDashboardFilters';
+import getFormDataWithExtraFilters from
'../../../util/charts/getFormDataWithExtraFilters';
+import { PLACEHOLDER_DATASOURCE } from '../../../constants';
const propTypes = {
id: PropTypes.number.isRequired,
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Chart.test.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.test.jsx
similarity index 99%
rename from
superset-frontend/src/dashboard/components/gridComponents/Chart.test.jsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.test.jsx
index c5f743a52c..a00b9e88c0 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Chart.test.jsx
+++
b/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.test.jsx
@@ -20,13 +20,13 @@ import { fireEvent, render } from
'spec/helpers/testing-library';
import { FeatureFlag, VizType } from '@superset-ui/core';
import * as redux from 'redux';
-import Chart from 'src/dashboard/components/gridComponents/Chart';
import * as exploreUtils from 'src/explore/exploreUtils';
import { sliceEntitiesForChart as sliceEntities } from
'spec/fixtures/mockSliceEntities';
import mockDatasource from 'spec/fixtures/mockDatasource';
import chartQueries, {
sliceId as queryId,
} from 'spec/fixtures/mockChartQueries';
+import Chart from './Chart';
const props = {
id: queryId,
diff --git
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/Chart/index.js
similarity index 57%
copy from
superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
copy to superset-frontend/src/dashboard/components/gridComponents/Chart/index.js
index 24a6a78d3f..7cda5527b4 100644
---
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Chart/index.js
@@ -16,21 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
+import Chart from './Chart';
-import { render, screen } from 'spec/helpers/testing-library';
-import BuilderComponentPane from '.';
-
-jest.mock('src/dashboard/containers/SliceAdder', () => () => (
- <div data-test="mock-slice-adder" />
-));
-
-test('BuilderComponentPane has correct tabs in correct order', () => {
- render(<BuilderComponentPane topOffset={115} />);
- const tabs = screen.getAllByRole('tab');
- expect(tabs).toHaveLength(2);
- expect(tabs[0]).toHaveTextContent('Charts');
- expect(tabs[1]).toHaveTextContent('Layout elements');
- expect(screen.getByRole('tab', { selected: true })).toHaveTextContent(
- 'Charts',
- );
-});
+export default Chart;
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder/ChartHolder.test.tsx
similarity index 98%
rename from
superset-frontend/src/dashboard/components/gridComponents/ChartHolder.test.tsx
rename to
superset-frontend/src/dashboard/components/gridComponents/ChartHolder/ChartHolder.test.tsx
index 072a087c8f..aac33a802c 100644
---
a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.test.tsx
+++
b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder/ChartHolder.test.tsx
@@ -35,9 +35,13 @@ import { nativeFiltersInfo } from
'src/dashboard/fixtures/mockNativeFilters';
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
import { initialState } from 'src/SqlLab/fixtures';
import { SET_DIRECT_PATH } from 'src/dashboard/actions/dashboardState';
-import { CHART_TYPE, COLUMN_TYPE, ROW_TYPE } from '../../util/componentTypes';
+import {
+ CHART_TYPE,
+ COLUMN_TYPE,
+ ROW_TYPE,
+} from '../../../util/componentTypes';
import ChartHolder, { CHART_MARGIN } from './ChartHolder';
-import { GRID_BASE_UNIT, GRID_GUTTER_SIZE } from '../../util/constants';
+import { GRID_BASE_UNIT, GRID_GUTTER_SIZE } from '../../../util/constants';
const DEFAULT_HEADER_HEIGHT = 22;
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx
b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder/ChartHolder.tsx
similarity index 100%
rename from
superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx
rename to
superset-frontend/src/dashboard/components/gridComponents/ChartHolder/ChartHolder.tsx
diff --git
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder/index.ts
similarity index 57%
copy from
superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
copy to
superset-frontend/src/dashboard/components/gridComponents/ChartHolder/index.ts
index 24a6a78d3f..f5a0b92ff1 100644
---
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
+++
b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder/index.ts
@@ -16,21 +16,4 @@
* specific language governing permissions and limitations
* under the License.
*/
-
-import { render, screen } from 'spec/helpers/testing-library';
-import BuilderComponentPane from '.';
-
-jest.mock('src/dashboard/containers/SliceAdder', () => () => (
- <div data-test="mock-slice-adder" />
-));
-
-test('BuilderComponentPane has correct tabs in correct order', () => {
- render(<BuilderComponentPane topOffset={115} />);
- const tabs = screen.getAllByRole('tab');
- expect(tabs).toHaveLength(2);
- expect(tabs[0]).toHaveTextContent('Charts');
- expect(tabs[1]).toHaveTextContent('Layout elements');
- expect(screen.getByRole('tab', { selected: true })).toHaveTextContent(
- 'Charts',
- );
-});
+export { default } from './ChartHolder';
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Column.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Column/Column.jsx
similarity index 100%
rename from superset-frontend/src/dashboard/components/gridComponents/Column.jsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Column/Column.jsx
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Column.test.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Column/Column.test.jsx
similarity index 99%
rename from
superset-frontend/src/dashboard/components/gridComponents/Column.test.jsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Column/Column.test.jsx
index b70f865dd3..e67be2c20d 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Column.test.jsx
+++
b/superset-frontend/src/dashboard/components/gridComponents/Column/Column.test.jsx
@@ -19,12 +19,12 @@
import { fireEvent, render } from 'spec/helpers/testing-library';
import BackgroundStyleDropdown from
'src/dashboard/components/menu/BackgroundStyleDropdown';
-import Column from 'src/dashboard/components/gridComponents/Column';
import IconButton from 'src/dashboard/components/IconButton';
import { getMockStore } from 'spec/fixtures/mockStore';
import { dashboardLayout as mockLayout } from
'spec/fixtures/mockDashboardLayout';
import { initialState } from 'src/SqlLab/fixtures';
+import Column from './Column';
jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({
Draggable: ({ children }) => (
diff --git
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/Column/index.js
similarity index 57%
copy from
superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
copy to
superset-frontend/src/dashboard/components/gridComponents/Column/index.js
index 24a6a78d3f..9b22df6f6e 100644
---
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Column/index.js
@@ -16,21 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
+import Column from './Column';
-import { render, screen } from 'spec/helpers/testing-library';
-import BuilderComponentPane from '.';
-
-jest.mock('src/dashboard/containers/SliceAdder', () => () => (
- <div data-test="mock-slice-adder" />
-));
-
-test('BuilderComponentPane has correct tabs in correct order', () => {
- render(<BuilderComponentPane topOffset={115} />);
- const tabs = screen.getAllByRole('tab');
- expect(tabs).toHaveLength(2);
- expect(tabs[0]).toHaveTextContent('Charts');
- expect(tabs[1]).toHaveTextContent('Layout elements');
- expect(screen.getByRole('tab', { selected: true })).toHaveTextContent(
- 'Charts',
- );
-});
+export default Column;
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Divider.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.jsx
similarity index 93%
rename from
superset-frontend/src/dashboard/components/gridComponents/Divider.jsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.jsx
index 466b77d0b7..3247cf794d 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Divider.jsx
+++
b/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.jsx
@@ -20,10 +20,10 @@ import { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { css, styled } from '@superset-ui/core';
-import { Draggable } from '../dnd/DragDroppable';
-import HoverMenu from '../menu/HoverMenu';
-import DeleteComponentButton from '../DeleteComponentButton';
-import { componentShape } from '../../util/propShapes';
+import { Draggable } from '../../dnd/DragDroppable';
+import HoverMenu from '../../menu/HoverMenu';
+import DeleteComponentButton from '../../DeleteComponentButton';
+import { componentShape } from '../../../util/propShapes';
const propTypes = {
id: PropTypes.string.isRequired,
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Divider.test.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.test.jsx
similarity index 97%
rename from
superset-frontend/src/dashboard/components/gridComponents/Divider.test.jsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.test.jsx
index 85a30d115d..1cf54aab0d 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Divider.test.jsx
+++
b/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.test.jsx
@@ -18,13 +18,13 @@
*/
import sinon from 'sinon';
-import Divider from 'src/dashboard/components/gridComponents/Divider';
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
import {
DIVIDER_TYPE,
DASHBOARD_GRID_TYPE,
} from 'src/dashboard/util/componentTypes';
import { screen, render, userEvent } from 'spec/helpers/testing-library';
+import Divider from './Divider';
describe('Divider', () => {
const props = {
diff --git
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/Divider/index.js
similarity index 57%
copy from
superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
copy to
superset-frontend/src/dashboard/components/gridComponents/Divider/index.js
index 24a6a78d3f..8a7a0c9781 100644
---
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Divider/index.js
@@ -16,21 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
+import Divider from './Divider';
-import { render, screen } from 'spec/helpers/testing-library';
-import BuilderComponentPane from '.';
-
-jest.mock('src/dashboard/containers/SliceAdder', () => () => (
- <div data-test="mock-slice-adder" />
-));
-
-test('BuilderComponentPane has correct tabs in correct order', () => {
- render(<BuilderComponentPane topOffset={115} />);
- const tabs = screen.getAllByRole('tab');
- expect(tabs).toHaveLength(2);
- expect(tabs[0]).toHaveTextContent('Charts');
- expect(tabs[1]).toHaveTextContent('Layout elements');
- expect(screen.getByRole('tab', { selected: true })).toHaveTextContent(
- 'Charts',
- );
-});
+export default Divider;
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent/DynamicComponent.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent/DynamicComponent.test.tsx
new file mode 100644
index 0000000000..d3d2ade7ca
--- /dev/null
+++
b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent/DynamicComponent.test.tsx
@@ -0,0 +1,329 @@
+/**
+ * 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 { render, screen, fireEvent } from 'spec/helpers/testing-library';
+import { COLUMN_TYPE, ROW_TYPE } from 'src/dashboard/util/componentTypes';
+import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants';
+import DynamicComponent from './DynamicComponent';
+
+// Mock the dashboard components registry
+const mockComponent = () => (
+ <div data-test="mock-dynamic-component">Test Component</div>
+);
+jest.mock('src/visualizations/presets/dashboardComponents', () => ({
+ get: jest.fn(() => ({ Component: mockComponent })),
+}));
+
+// Mock other dependencies
+jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({
+ Draggable: jest.fn(({ children, editMode }) => {
+ const mockElement = { tagName: 'DIV', dataset: {} };
+ const mockDragSourceRef = { current: mockElement };
+ return (
+ <div data-test="mock-draggable">
+ {children({ dragSourceRef: editMode ? mockDragSourceRef : null })}
+ </div>
+ );
+ }),
+}));
+
+jest.mock('src/dashboard/components/menu/WithPopoverMenu', () =>
+ jest.fn(({ children, menuItems, editMode }) => (
+ <div data-test="mock-popover-menu">
+ {editMode &&
+ menuItems &&
+ menuItems.map((item: React.ReactNode, index: number) => (
+ <div key={index} data-test="menu-item">
+ {item}
+ </div>
+ ))}
+ {children}
+ </div>
+ )),
+);
+
+jest.mock('src/dashboard/components/resizable/ResizableContainer', () =>
+ jest.fn(({ children }) => (
+ <div data-test="mock-resizable-container">{children}</div>
+ )),
+);
+
+jest.mock('src/dashboard/components/menu/HoverMenu', () =>
+ jest.fn(({ children }) => <div data-test="mock-hover-menu">{children}</div>),
+);
+
+jest.mock('src/dashboard/components/DeleteComponentButton', () =>
+ jest.fn(({ onDelete }) => (
+ <button type="button" data-test="mock-delete-button" onClick={onDelete}>
+ Delete
+ </button>
+ )),
+);
+
+jest.mock('src/dashboard/components/menu/BackgroundStyleDropdown', () =>
+ jest.fn(({ onChange, value }) => (
+ <select
+ data-test="mock-background-dropdown"
+ value={value}
+ onChange={e => onChange(e.target.value)}
+ >
+ <option value="BACKGROUND_TRANSPARENT">Transparent</option>
+ <option value="BACKGROUND_WHITE">White</option>
+ </select>
+ )),
+);
+
+const createProps = (overrides = {}) => ({
+ component: {
+ id: 'DYNAMIC_COMPONENT_1',
+ meta: {
+ componentKey: 'test-component',
+ width: 6,
+ height: 4,
+ background: BACKGROUND_TRANSPARENT,
+ },
+ componentKey: 'test-component',
+ },
+ parentComponent: {
+ id: 'ROW_1',
+ type: ROW_TYPE,
+ meta: {
+ width: 12,
+ },
+ },
+ index: 0,
+ depth: 1,
+ handleComponentDrop: jest.fn(),
+ editMode: false,
+ columnWidth: 100,
+ availableColumnCount: 12,
+ onResizeStart: jest.fn(),
+ onResizeStop: jest.fn(),
+ onResize: jest.fn(),
+ deleteComponent: jest.fn(),
+ updateComponents: jest.fn(),
+ parentId: 'ROW_1',
+ id: 'DYNAMIC_COMPONENT_1',
+ ...overrides,
+});
+
+const renderWithRedux = (component: React.ReactElement) =>
+ render(component, {
+ useRedux: true,
+ initialState: {
+ nativeFilters: { filters: {} },
+ dataMask: {},
+ },
+ });
+
+describe('DynamicComponent', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('should render the component with basic structure', () => {
+ const props = createProps();
+ renderWithRedux(<DynamicComponent {...props} />);
+
+ expect(screen.getByTestId('mock-draggable')).toBeInTheDocument();
+ expect(screen.getByTestId('mock-popover-menu')).toBeInTheDocument();
+ expect(screen.getByTestId('mock-resizable-container')).toBeInTheDocument();
+ expect(
+ screen.getByTestId('dashboard-component-chart-holder'),
+ ).toBeInTheDocument();
+ expect(screen.getByTestId('mock-dynamic-component')).toBeInTheDocument();
+ });
+
+ test('should render with proper CSS classes and data attributes', () => {
+ const props = createProps();
+ renderWithRedux(<DynamicComponent {...props} />);
+
+ const componentElement = screen.getByTestId('dashboard-test-component');
+ expect(componentElement).toHaveClass('dashboard-component');
+ expect(componentElement).toHaveClass('dashboard-test-component');
+ expect(componentElement).toHaveAttribute('id', 'DYNAMIC_COMPONENT_1');
+ });
+
+ test('should render HoverMenu and DeleteComponentButton in edit mode', () =>
{
+ const props = createProps({ editMode: true });
+ renderWithRedux(<DynamicComponent {...props} />);
+
+ expect(screen.getByTestId('mock-hover-menu')).toBeInTheDocument();
+ expect(screen.getByTestId('mock-delete-button')).toBeInTheDocument();
+ });
+
+ test('should not render HoverMenu and DeleteComponentButton when not in edit
mode', () => {
+ const props = createProps({ editMode: false });
+ renderWithRedux(<DynamicComponent {...props} />);
+
+ expect(screen.queryByTestId('mock-hover-menu')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('mock-delete-button')).not.toBeInTheDocument();
+ });
+
+ test('should call deleteComponent when delete button is clicked', () => {
+ const props = createProps({ editMode: true });
+ renderWithRedux(<DynamicComponent {...props} />);
+
+ fireEvent.click(screen.getByTestId('mock-delete-button'));
+ expect(props.deleteComponent).toHaveBeenCalledWith(
+ 'DYNAMIC_COMPONENT_1',
+ 'ROW_1',
+ );
+ });
+
+ test('should call updateComponents when background is changed', () => {
+ const props = createProps({ editMode: true });
+ renderWithRedux(<DynamicComponent {...props} />);
+
+ const backgroundDropdown = screen.getByTestId('mock-background-dropdown');
+ fireEvent.change(backgroundDropdown, {
+ target: { value: 'BACKGROUND_WHITE' },
+ });
+
+ expect(props.updateComponents).toHaveBeenCalledWith({
+ DYNAMIC_COMPONENT_1: {
+ ...props.component,
+ meta: {
+ ...props.component.meta,
+ background: 'BACKGROUND_WHITE',
+ },
+ },
+ });
+ });
+
+ test('should calculate width multiple from component meta when parent is not
COLUMN_TYPE', () => {
+ const props = createProps({
+ component: {
+ ...createProps().component,
+ meta: { ...createProps().component.meta, width: 8 },
+ },
+ parentComponent: {
+ ...createProps().parentComponent,
+ type: ROW_TYPE,
+ },
+ });
+ renderWithRedux(<DynamicComponent {...props} />);
+
+ // Component should render successfully with width from
component.meta.width
+ expect(screen.getByTestId('mock-resizable-container')).toBeInTheDocument();
+ });
+
+ test('should calculate width multiple from parent meta when parent is
COLUMN_TYPE', () => {
+ const props = createProps({
+ parentComponent: {
+ id: 'COLUMN_1',
+ type: COLUMN_TYPE,
+ meta: {
+ width: 6,
+ },
+ },
+ });
+ renderWithRedux(<DynamicComponent {...props} />);
+
+ // Component should render successfully with width from
parentComponent.meta.width
+ expect(screen.getByTestId('mock-resizable-container')).toBeInTheDocument();
+ });
+
+ test('should use default width when no width is specified', () => {
+ const props = createProps({
+ component: {
+ ...createProps().component,
+ meta: {
+ ...createProps().component.meta,
+ width: undefined,
+ },
+ },
+ parentComponent: {
+ ...createProps().parentComponent,
+ type: ROW_TYPE,
+ meta: {},
+ },
+ });
+ renderWithRedux(<DynamicComponent {...props} />);
+
+ // Component should render successfully with default width
(GRID_MIN_COLUMN_COUNT)
+ expect(screen.getByTestId('mock-resizable-container')).toBeInTheDocument();
+ });
+
+ test('should render background style correctly', () => {
+ const props = createProps({
+ editMode: true, // Need edit mode for menu items to render
+ component: {
+ ...createProps().component,
+ meta: {
+ ...createProps().component.meta,
+ background: 'BACKGROUND_WHITE',
+ },
+ },
+ });
+ renderWithRedux(<DynamicComponent {...props} />);
+
+ // Background dropdown should have the correct value
+ const backgroundDropdown = screen.getByTestId('mock-background-dropdown');
+ expect(backgroundDropdown).toHaveValue('BACKGROUND_WHITE');
+ });
+
+ test('should pass dashboard data from Redux store to dynamic component', ()
=> {
+ const props = createProps();
+ const initialState = {
+ nativeFilters: { filters: { filter1: {} } },
+ dataMask: { mask1: {} },
+ };
+
+ render(<DynamicComponent {...props} />, {
+ useRedux: true,
+ initialState,
+ });
+
+ // Component should render - either the mock component or loading state
+ const container = screen.getByTestId('dashboard-component-chart-holder');
+ expect(container).toBeInTheDocument();
+ // Check that either the component loaded or is loading
+ expect(
+ screen.queryByTestId('mock-dynamic-component') ||
+ screen.queryByText('Loading...'),
+ ).toBeTruthy();
+ });
+
+ test('should handle resize callbacks', () => {
+ const props = createProps();
+ renderWithRedux(<DynamicComponent {...props} />);
+
+ // Resize callbacks should be passed to ResizableContainer
+ expect(screen.getByTestId('mock-resizable-container')).toBeInTheDocument();
+ });
+
+ test('should render with proper data-test attribute based on componentKey',
() => {
+ const props = createProps({
+ component: {
+ ...createProps().component,
+ meta: {
+ ...createProps().component.meta,
+ componentKey: 'custom-component',
+ },
+ componentKey: 'custom-component',
+ },
+ });
+ renderWithRedux(<DynamicComponent {...props} />);
+
+ expect(
+ screen.getByTestId('dashboard-custom-component'),
+ ).toBeInTheDocument();
+ });
+});
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx
b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent/DynamicComponent.tsx
similarity index 84%
rename from
superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx
rename to
superset-frontend/src/dashboard/components/gridComponents/DynamicComponent/DynamicComponent.tsx
index 66db0fd898..eb06833fba 100644
---
a/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx
+++
b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent/DynamicComponent.tsx
@@ -22,40 +22,40 @@ import backgroundStyleOptions from
'src/dashboard/util/backgroundStyleOptions';
import cx from 'classnames';
import { shallowEqual, useSelector } from 'react-redux';
import { ResizeCallback, ResizeStartCallback } from 're-resizable';
-import { Draggable } from '../dnd/DragDroppable';
-import { COLUMN_TYPE, ROW_TYPE } from '../../util/componentTypes';
-import WithPopoverMenu from '../menu/WithPopoverMenu';
-import ResizableContainer from '../resizable/ResizableContainer';
+import { Draggable } from '../../dnd/DragDroppable';
+import { COLUMN_TYPE, ROW_TYPE } from '../../../util/componentTypes';
+import WithPopoverMenu from '../../menu/WithPopoverMenu';
+import ResizableContainer from '../../resizable/ResizableContainer';
import {
BACKGROUND_TRANSPARENT,
GRID_BASE_UNIT,
GRID_MIN_COLUMN_COUNT,
-} from '../../util/constants';
-import HoverMenu from '../menu/HoverMenu';
-import DeleteComponentButton from '../DeleteComponentButton';
-import BackgroundStyleDropdown from '../menu/BackgroundStyleDropdown';
-import dashboardComponents from
'../../../visualizations/presets/dashboardComponents';
-import { RootState } from '../../types';
+} from '../../../util/constants';
+import HoverMenu from '../../menu/HoverMenu';
+import DeleteComponentButton from '../../DeleteComponentButton';
+import BackgroundStyleDropdown from '../../menu/BackgroundStyleDropdown';
+import dashboardComponents from
'../../../../visualizations/presets/dashboardComponents';
+import { RootState } from '../../../types';
-type FilterSummaryType = {
+type DynamicComponentProps = {
component: JsonObject;
parentComponent: JsonObject;
index: number;
depth: number;
- handleComponentDrop: (...args: any[]) => any;
+ handleComponentDrop: (dropResult: unknown) => void;
editMode: boolean;
columnWidth: number;
availableColumnCount: number;
onResizeStart: ResizeStartCallback;
onResizeStop: ResizeCallback;
onResize: ResizeCallback;
- deleteComponent: Function;
- updateComponents: Function;
- parentId: number;
- id: number;
+ deleteComponent: (id: string, parentId: string) => void;
+ updateComponents: (updates: Record<string, JsonObject>) => void;
+ parentId: string;
+ id: string;
};
-const DynamicComponent: FC<FilterSummaryType> = ({
+const DynamicComponent: FC<DynamicComponentProps> = ({
component,
parentComponent,
index,
diff --git
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent/index.ts
similarity index 57%
copy from
superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
copy to
superset-frontend/src/dashboard/components/gridComponents/DynamicComponent/index.ts
index 24a6a78d3f..482eedb7f0 100644
---
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
+++
b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent/index.ts
@@ -16,21 +16,4 @@
* specific language governing permissions and limitations
* under the License.
*/
-
-import { render, screen } from 'spec/helpers/testing-library';
-import BuilderComponentPane from '.';
-
-jest.mock('src/dashboard/containers/SliceAdder', () => () => (
- <div data-test="mock-slice-adder" />
-));
-
-test('BuilderComponentPane has correct tabs in correct order', () => {
- render(<BuilderComponentPane topOffset={115} />);
- const tabs = screen.getAllByRole('tab');
- expect(tabs).toHaveLength(2);
- expect(tabs[0]).toHaveTextContent('Charts');
- expect(tabs[1]).toHaveTextContent('Layout elements');
- expect(screen.getByRole('tab', { selected: true })).toHaveTextContent(
- 'Charts',
- );
-});
+export { default } from './DynamicComponent';
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Header.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Header/Header.jsx
similarity index 100%
rename from superset-frontend/src/dashboard/components/gridComponents/Header.jsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Header/Header.jsx
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Header.test.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Header/Header.test.jsx
similarity index 98%
rename from
superset-frontend/src/dashboard/components/gridComponents/Header.test.jsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Header/Header.test.jsx
index 48c969bb9b..1f204406a2 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Header.test.jsx
+++
b/superset-frontend/src/dashboard/components/gridComponents/Header/Header.test.jsx
@@ -22,7 +22,6 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import sinon from 'sinon';
import { render, screen, fireEvent } from 'spec/helpers/testing-library';
-import Header from 'src/dashboard/components/gridComponents/Header';
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
import {
HEADER_TYPE,
@@ -30,6 +29,7 @@ import {
} from 'src/dashboard/util/componentTypes';
import { mockStoreWithTabs } from 'spec/fixtures/mockStore';
+import Header from './Header';
describe('Header', () => {
const props = {
diff --git
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/Header/index.js
similarity index 57%
copy from
superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
copy to
superset-frontend/src/dashboard/components/gridComponents/Header/index.js
index 24a6a78d3f..87090f2574 100644
---
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Header/index.js
@@ -16,21 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
+import Header from './Header';
-import { render, screen } from 'spec/helpers/testing-library';
-import BuilderComponentPane from '.';
-
-jest.mock('src/dashboard/containers/SliceAdder', () => () => (
- <div data-test="mock-slice-adder" />
-));
-
-test('BuilderComponentPane has correct tabs in correct order', () => {
- render(<BuilderComponentPane topOffset={115} />);
- const tabs = screen.getAllByRole('tab');
- expect(tabs).toHaveLength(2);
- expect(tabs[0]).toHaveTextContent('Charts');
- expect(tabs[1]).toHaveTextContent('Layout elements');
- expect(screen.getByRole('tab', { selected: true })).toHaveTextContent(
- 'Charts',
- );
-});
+export default Header;
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.jsx
similarity index 100%
rename from
superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.jsx
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Markdown.test.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.test.jsx
similarity index 99%
rename from
superset-frontend/src/dashboard/components/gridComponents/Markdown.test.jsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.test.jsx
index 0d968b1b7a..c10d10b776 100644
---
a/superset-frontend/src/dashboard/components/gridComponents/Markdown.test.jsx
+++
b/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.test.jsx
@@ -18,9 +18,9 @@
*/
import { Provider } from 'react-redux';
import { act, render, screen, fireEvent } from 'spec/helpers/testing-library';
-import MarkdownConnected from
'src/dashboard/components/gridComponents/Markdown';
import { mockStore } from 'spec/fixtures/mockStore';
import { dashboardLayout as mockLayout } from
'spec/fixtures/mockDashboardLayout';
+import MarkdownConnected from './Markdown';
describe('Markdown', () => {
const props = {
diff --git
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/Markdown/index.js
similarity index 57%
copy from
superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
copy to
superset-frontend/src/dashboard/components/gridComponents/Markdown/index.js
index 24a6a78d3f..af0149d068 100644
---
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
+++
b/superset-frontend/src/dashboard/components/gridComponents/Markdown/index.js
@@ -16,21 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
+import Markdown from './Markdown';
-import { render, screen } from 'spec/helpers/testing-library';
-import BuilderComponentPane from '.';
-
-jest.mock('src/dashboard/containers/SliceAdder', () => () => (
- <div data-test="mock-slice-adder" />
-));
-
-test('BuilderComponentPane has correct tabs in correct order', () => {
- render(<BuilderComponentPane topOffset={115} />);
- const tabs = screen.getAllByRole('tab');
- expect(tabs).toHaveLength(2);
- expect(tabs[0]).toHaveTextContent('Charts');
- expect(tabs[1]).toHaveTextContent('Layout elements');
- expect(screen.getByRole('tab', { selected: true })).toHaveTextContent(
- 'Charts',
- );
-});
+export default Markdown;
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Row.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.jsx
similarity index 99%
rename from superset-frontend/src/dashboard/components/gridComponents/Row.jsx
rename to superset-frontend/src/dashboard/components/gridComponents/Row/Row.jsx
index 61cd5ff69e..917cdfc965 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Row.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.jsx
@@ -53,7 +53,7 @@ import { BACKGROUND_TRANSPARENT } from
'src/dashboard/util/constants';
import { isEmbedded } from 'src/dashboard/util/isEmbedded';
import { EMPTY_CONTAINER_Z_INDEX } from 'src/dashboard/constants';
import { isCurrentUserBot } from 'src/utils/isBot';
-import { useDebouncedEffect } from '../../../explore/exploreUtils';
+import { useDebouncedEffect } from '../../../../explore/exploreUtils';
const propTypes = {
id: PropTypes.string.isRequired,
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Row.test.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.jsx
similarity index 99%
rename from
superset-frontend/src/dashboard/components/gridComponents/Row.test.jsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.jsx
index e15d116f06..2f8499a6fe 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Row.test.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.jsx
@@ -20,12 +20,12 @@ import { fireEvent, render } from
'spec/helpers/testing-library';
import BackgroundStyleDropdown from
'src/dashboard/components/menu/BackgroundStyleDropdown';
import IconButton from 'src/dashboard/components/IconButton';
-import Row from 'src/dashboard/components/gridComponents/Row';
import { DASHBOARD_GRID_ID } from 'src/dashboard/util/constants';
import { getMockStore } from 'spec/fixtures/mockStore';
import { dashboardLayout as mockLayout } from
'spec/fixtures/mockDashboardLayout';
import { initialState } from 'src/SqlLab/fixtures';
+import Row from './Row';
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
diff --git
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/Row/index.js
similarity index 57%
copy from
superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
copy to superset-frontend/src/dashboard/components/gridComponents/Row/index.js
index 24a6a78d3f..2b78be10dc 100644
---
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Row/index.js
@@ -16,21 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
+import Row from './Row';
-import { render, screen } from 'spec/helpers/testing-library';
-import BuilderComponentPane from '.';
-
-jest.mock('src/dashboard/containers/SliceAdder', () => () => (
- <div data-test="mock-slice-adder" />
-));
-
-test('BuilderComponentPane has correct tabs in correct order', () => {
- render(<BuilderComponentPane topOffset={115} />);
- const tabs = screen.getAllByRole('tab');
- expect(tabs).toHaveLength(2);
- expect(tabs[0]).toHaveTextContent('Charts');
- expect(tabs[1]).toHaveTextContent('Layout elements');
- expect(screen.getByRole('tab', { selected: true })).toHaveTextContent(
- 'Charts',
- );
-});
+export default Row;
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Tab.test.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Tab.test.jsx
deleted file mode 100644
index 74c5f4813a..0000000000
--- a/superset-frontend/src/dashboard/components/gridComponents/Tab.test.jsx
+++ /dev/null
@@ -1,141 +0,0 @@
-/**
- * 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 { render, screen, fireEvent } from 'spec/helpers/testing-library';
-
-import { Provider } from 'react-redux';
-import { DndProvider } from 'react-dnd';
-import { HTML5Backend } from 'react-dnd-html5-backend';
-import Tab, { RENDER_TAB } from 'src/dashboard/components/gridComponents/Tab';
-import { dashboardLayoutWithTabs } from 'spec/fixtures/mockDashboardLayout';
-import { getMockStore } from 'spec/fixtures/mockStore';
-
-// TODO: rewrite to RTL
-describe('Tabs', () => {
- const props = {
- id: 'TAB_ID',
- parentId: 'TABS_ID',
- component: dashboardLayoutWithTabs.present.TAB_ID,
- parentComponent: dashboardLayoutWithTabs.present.TABS_ID,
- index: 0,
- depth: 1,
- editMode: false,
- renderType: RENDER_TAB,
- filters: {},
- dashboardId: 123,
- setDirectPathToChild: jest.fn(),
- onDropOnTab() {},
- onDeleteTab() {},
- availableColumnCount: 12,
- columnWidth: 50,
- onResizeStart() {},
- onResize() {},
- onResizeStop() {},
- createComponent() {},
- handleComponentDrop() {},
- onChangeTab() {},
- deleteComponent() {},
- updateComponents() {},
- dropToChild: false,
- maxChildrenHeight: 100,
- shouldDropToChild: () => false, // Add this prop
- };
-
- function setup(overrideProps = {}) {
- return render(
- <Provider
- store={getMockStore({
- dashboardLayout: dashboardLayoutWithTabs,
- })}
- >
- <DndProvider backend={HTML5Backend}>
- <Tab {...props} {...overrideProps} />
- </DndProvider>
- </Provider>,
- );
- }
-
- describe('renderType=RENDER_TAB', () => {
- it('should render a DragDroppable', () => {
- setup();
- expect(screen.getByTestId('dragdroppable-object')).toBeInTheDocument();
- });
-
- it('should render an EditableTitle with meta.text', () => {
- setup();
- const titleElement = screen.getByTestId('editable-title');
- expect(titleElement).toBeInTheDocument();
- expect(titleElement).toHaveTextContent(
- props.component.meta.defaultText || '',
- );
- });
-
- it('should call updateComponents when EditableTitle changes', async () => {
- const updateComponents = jest.fn();
- setup({
- editMode: true,
- updateComponents,
- component: {
- ...dashboardLayoutWithTabs.present.TAB_ID,
- meta: {
- text: 'Original Title',
- defaultText: 'Original Title', // Add defaultText to match
component
- },
- },
- isFocused: true,
- });
-
- const titleElement = screen.getByTestId('editable-title');
- fireEvent.click(titleElement);
-
- const titleInput = await screen.findByTestId(
- 'textarea-editable-title-input',
- );
- fireEvent.change(titleInput, { target: { value: 'New title' } });
- fireEvent.blur(titleInput);
-
- expect(updateComponents).toHaveBeenCalledWith({
- TAB_ID: {
- ...dashboardLayoutWithTabs.present.TAB_ID,
- meta: {
- ...dashboardLayoutWithTabs.present.TAB_ID.meta,
- text: 'New title',
- defaultText: 'Original Title', // Keep the original defaultText
- },
- },
- });
- });
- });
-
- describe('renderType=RENDER_TAB_CONTENT', () => {
- it('should render DashboardComponents', () => {
- setup({
- renderType: 'RENDER_TAB_CONTENT',
- component: {
- ...dashboardLayoutWithTabs.present.TAB_ID,
- children: ['ROW_ID'],
- },
- });
-
- expect(
- screen.getByTestId('dashboard-component-chart-holder'),
- ).toBeInTheDocument();
- });
- });
-});
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.jsx
similarity index 100%
rename from superset-frontend/src/dashboard/components/gridComponents/Tab.jsx
rename to superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.jsx
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Tab.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.test.tsx
similarity index 99%
rename from
superset-frontend/src/dashboard/components/gridComponents/Tab.test.tsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.test.tsx
index 05ceb7e0cc..3ff5f28797 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tab.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.test.tsx
@@ -29,7 +29,7 @@ import { EditableTitle } from '@superset-ui/core/components';
import { setEditMode } from 'src/dashboard/actions/dashboardState';
import Tab from './Tab';
-import Markdown from './Markdown';
+import Markdown from '../Markdown';
jest.mock('src/dashboard/containers/DashboardComponent', () =>
jest.fn(() => <div data-test="DashboardComponent" />),
diff --git
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/Tab/index.js
similarity index 57%
copy from
superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
copy to superset-frontend/src/dashboard/components/gridComponents/Tab/index.js
index 24a6a78d3f..f1c4c41d48 100644
---
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tab/index.js
@@ -16,21 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
+import Tab from './Tab';
-import { render, screen } from 'spec/helpers/testing-library';
-import BuilderComponentPane from '.';
-
-jest.mock('src/dashboard/containers/SliceAdder', () => () => (
- <div data-test="mock-slice-adder" />
-));
-
-test('BuilderComponentPane has correct tabs in correct order', () => {
- render(<BuilderComponentPane topOffset={115} />);
- const tabs = screen.getAllByRole('tab');
- expect(tabs).toHaveLength(2);
- expect(tabs[0]).toHaveTextContent('Charts');
- expect(tabs[1]).toHaveTextContent('Layout elements');
- expect(screen.getByRole('tab', { selected: true })).toHaveTextContent(
- 'Charts',
- );
-});
+export default Tab;
+export { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx
deleted file mode 100644
index d3c606fc2b..0000000000
--- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx
+++ /dev/null
@@ -1,203 +0,0 @@
-/**
- * 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 { fireEvent, render } from 'spec/helpers/testing-library';
-import fetchMock from 'fetch-mock';
-import Tabs from 'src/dashboard/components/gridComponents/Tabs';
-import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
-import emptyDashboardLayout from 'src/dashboard/fixtures/emptyDashboardLayout';
-import { dashboardLayoutWithTabs } from 'spec/fixtures/mockDashboardLayout';
-import { nativeFilters } from 'spec/fixtures/mockNativeFilters';
-import { initialState } from 'src/SqlLab/fixtures';
-
-jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({
- Draggable: ({ children }) => (
- <div data-test="mock-draggable">{children({})}</div>
- ),
- Droppable: ({ children }) => (
- <div data-test="mock-droppable">{children({})}</div>
- ),
-}));
-jest.mock('src/dashboard/containers/DashboardComponent', () => ({ id }) => (
- <div data-test="mock-dashboard-component">{id}</div>
-));
-
-jest.mock(
- 'src/dashboard/components/DeleteComponentButton',
- () =>
- ({ onDelete }) => (
- <button
- type="button"
- data-test="mock-delete-component-button"
- onClick={onDelete}
- >
- Delete
- </button>
- ),
-);
-
-fetchMock.post('glob:*/r/shortener/', {});
-
-const props = {
- id: 'TABS_ID',
- parentId: DASHBOARD_ROOT_ID,
- component: dashboardLayoutWithTabs.present.TABS_ID,
- parentComponent: dashboardLayoutWithTabs.present[DASHBOARD_ROOT_ID],
- index: 0,
- depth: 1,
- renderTabContent: true,
- editMode: false,
- availableColumnCount: 12,
- columnWidth: 50,
- dashboardId: 1,
- onResizeStart() {},
- onResize() {},
- onResizeStop() {},
- createComponent() {},
- handleComponentDrop() {},
- onChangeTab() {},
- deleteComponent() {},
- updateComponents() {},
- logEvent() {},
- dashboardLayout: emptyDashboardLayout,
- nativeFilters: nativeFilters.filters,
-};
-
-function setup(overrideProps, overrideState = {}) {
- return render(<Tabs {...props} {...overrideProps} />, {
- useDnd: true,
- useRouter: true,
- useRedux: true,
- initialState: {
- ...initialState,
- dashboardLayout: dashboardLayoutWithTabs,
- dashboardFilters: {},
- ...overrideState,
- },
- });
-}
-
-test('should render a Draggable', () => {
- // test just Tabs with no children Draggable
- const { getByTestId } = setup({
- component: { ...props.component, children: [] },
- });
- expect(getByTestId('mock-draggable')).toBeInTheDocument();
-});
-
-test('should render non-editable tabs', () => {
- const { getAllByRole, container } = setup();
- expect(getAllByRole('tab')[0]).toBeInTheDocument();
- expect(container.querySelector('.ant-tabs-nav-add')).not.toBeInTheDocument();
-});
-
-test('should render a tab pane for each child', () => {
- const { getAllByRole } = setup();
- expect(getAllByRole('tab')).toHaveLength(props.component.children.length);
-});
-
-test('should render editable tabs in editMode', () => {
- const { getAllByRole, container } = setup({ editMode: true });
- expect(getAllByRole('tab')[0]).toBeInTheDocument();
- expect(container.querySelector('.ant-tabs-nav-add')).toBeInTheDocument();
-});
-
-test('should render a DashboardComponent for each child', () => {
- // note: this does not test Tab content
- const { getAllByTestId } = setup({ renderTabContent: false });
- expect(getAllByTestId('mock-dashboard-component')).toHaveLength(
- props.component.children.length,
- );
-});
-
-test('should call createComponent if the (+) tab is clicked', () => {
- const createComponent = jest.fn();
- const { getAllByRole } = setup({ editMode: true, createComponent });
- const addButtons = getAllByRole('button', { name: 'Add tab' });
- fireEvent.click(addButtons[0]);
- expect(createComponent).toHaveBeenCalledTimes(1);
-});
-
-test('should call onChangeTab when a tab is clicked', () => {
- const onChangeTab = jest.fn();
- const { getByRole } = setup({ editMode: true, onChangeTab });
- const newTab = getByRole('tab', { selected: false });
- fireEvent.click(newTab);
- expect(onChangeTab).toHaveBeenCalledTimes(1);
-});
-
-test('should not call onChangeTab when anchor link is clicked', () => {
- const onChangeTab = jest.fn();
- const { getByRole } = setup({ editMode: true, onChangeTab });
- const currentTab = getByRole('tab', { selected: true });
- fireEvent.click(currentTab);
-
- expect(onChangeTab).toHaveBeenCalledTimes(0);
-});
-
-test('should render a HoverMenu in editMode', () => {
- const { container } = setup({ editMode: true });
- expect(container.querySelector('.hover-menu')).toBeInTheDocument();
-});
-
-test('should render a DeleteComponentButton in editMode', () => {
- const { getByTestId } = setup({ editMode: true });
- expect(getByTestId('mock-delete-component-button')).toBeInTheDocument();
-});
-
-test('should call deleteComponent when deleted', () => {
- const deleteComponent = jest.fn();
- const { getByTestId } = setup({ editMode: true, deleteComponent });
- fireEvent.click(getByTestId('mock-delete-component-button'));
- expect(deleteComponent).toHaveBeenCalledTimes(1);
-});
-
-test('should direct display direct-link tab', () => {
- // display child in directPathToChild list
- const directPathToChild =
- dashboardLayoutWithTabs.present.ROW_ID2.parents.slice();
- const { getByRole } = setup({}, { dashboardState: { directPathToChild } });
- expect(getByRole('tab', { selected: true })).toHaveTextContent('TAB_ID2');
-});
-
-test('should render Modal when clicked remove tab button', () => {
- const deleteComponent = jest.fn();
- const { container, getByText, queryByText } = setup({
- editMode: true,
- deleteComponent,
- });
-
- // Initially no modal should be visible
- expect(queryByText('Delete dashboard tab?')).not.toBeInTheDocument();
-
- // Click the remove tab button
- fireEvent.click(container.querySelector('.ant-tabs-tab-remove'));
-
- // Modal should now be visible
- expect(getByText('Delete dashboard tab?')).toBeInTheDocument();
- expect(deleteComponent).toHaveBeenCalledTimes(0);
-});
-
-test('should set new tab key if dashboardId was changed', () => {
- const { getByRole } = setup({
- ...props,
- dashboardId: 2,
- component: dashboardLayoutWithTabs.present.TAB_ID,
- });
- expect(getByRole('tab', { selected: true })).toHaveTextContent('ROW_ID');
-});
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx
b/superset-frontend/src/dashboard/components/gridComponents/Tabs/Tabs.jsx
similarity index 84%
rename from superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Tabs/Tabs.jsx
index 68abbfcee6..c59b38609c 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs/Tabs.jsx
@@ -18,25 +18,22 @@
*/
import { useCallback, useEffect, useMemo, useState, memo } from 'react';
import PropTypes from 'prop-types';
-import { styled, t, usePrevious, css } from '@superset-ui/core';
+import { t, usePrevious, useTheme, styled } from '@superset-ui/core';
import { useSelector } from 'react-redux';
-import { LineEditableTabs } from '@superset-ui/core/components/Tabs';
import { Icons } from '@superset-ui/core/components/Icons';
import { LOG_ACTIONS_SELECT_DASHBOARD_TAB } from 'src/logger/LogUtils';
import { Modal } from '@superset-ui/core/components';
import { DROP_LEFT, DROP_RIGHT } from 'src/dashboard/util/getDropPosition';
-import { Draggable } from '../dnd/DragDroppable';
-import DragHandle from '../dnd/DragHandle';
-import DashboardComponent from '../../containers/DashboardComponent';
-import DeleteComponentButton from '../DeleteComponentButton';
-import HoverMenu from '../menu/HoverMenu';
-import findTabIndexByComponentId from '../../util/findTabIndexByComponentId';
-import getDirectPathToTabIndex from '../../util/getDirectPathToTabIndex';
-import getLeafComponentIdFromPath from '../../util/getLeafComponentIdFromPath';
-import { componentShape } from '../../util/propShapes';
-import { NEW_TAB_ID } from '../../util/constants';
-import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
-import { TABS_TYPE, TAB_TYPE } from '../../util/componentTypes';
+import { Draggable } from '../../dnd/DragDroppable';
+import DashboardComponent from '../../../containers/DashboardComponent';
+import findTabIndexByComponentId from
'../../../util/findTabIndexByComponentId';
+import getDirectPathToTabIndex from '../../../util/getDirectPathToTabIndex';
+import getLeafComponentIdFromPath from
'../../../util/getLeafComponentIdFromPath';
+import { componentShape } from '../../../util/propShapes';
+import { NEW_TAB_ID } from '../../../util/constants';
+import { RENDER_TAB, RENDER_TAB_CONTENT } from '../Tab';
+import { TABS_TYPE, TAB_TYPE } from '../../../util/componentTypes';
+import TabsRenderer from '../TabsRenderer';
const propTypes = {
id: PropTypes.string.isRequired,
@@ -76,34 +73,6 @@ const defaultProps = {
onResizeStop() {},
};
-const StyledTabsContainer = styled.div`
- ${({ theme }) => css`
- width: 100%;
- background-color: ${theme.colorBgBase};
-
- .dashboard-component-tabs-content {
- min-height: ${theme.sizeUnit * 12}px;
- margin-top: ${theme.sizeUnit / 4}px;
- position: relative;
- }
-
- .ant-tabs {
- overflow: visible;
-
- .ant-tabs-nav-wrap {
- min-height: ${theme.sizeUnit * 12.5}px;
- }
-
- .ant-tabs-content-holder {
- overflow: visible;
- }
- }
-
- div .ant-tabs-tab-btn {
- text-transform: none;
- }
- `}
-`;
const DropIndicator = styled.div`
border: 2px solid ${({ theme }) => theme.colorPrimary};
width: 5px;
@@ -124,11 +93,16 @@ const CloseIconWithDropIndicator = props => (
);
const Tabs = props => {
+ const theme = useTheme();
+
const nativeFilters = useSelector(state => state.nativeFilters);
const activeTabs = useSelector(state => state.dashboardState.activeTabs);
const directPathToChild = useSelector(
state => state.dashboardState.directPathToChild,
);
+ const nativeFiltersBarOpen = useSelector(
+ state => state.dashboardState.nativeFiltersBarOpen ?? false,
+ );
const { tabIndex: initTabIndex, activeKey: initActiveKey } = useMemo(() => {
let tabIndex = Math.max(
@@ -378,6 +352,13 @@ const Tabs = props => {
const { children: tabIds } = tabsComponent;
+ const tabBarPaddingLeft =
+ renderTabContent === false
+ ? nativeFiltersBarOpen
+ ? 0
+ : theme.sizeUnit * 4
+ : 0;
+
const showDropIndicators = useCallback(
currentDropTabIndex =>
currentDropTabIndex === dragOverTabIndex && {
@@ -392,16 +373,21 @@ const Tabs = props => {
[draggingTabId],
);
- let tabsToHighlight;
- const highlightedFilterId =
- nativeFilters?.focusedFilterId || nativeFilters?.hoveredFilterId;
- if (highlightedFilterId) {
- tabsToHighlight = nativeFilters.filters[highlightedFilterId]?.tabsInScope;
- }
-
- const renderChild = useCallback(
- ({ dragSourceRef: tabsDragSourceRef }) => {
- const tabItems = tabIds.map((tabId, tabIndex) => ({
+ // Extract tab highlighting logic into a hook
+ const useTabHighlighting = useCallback(() => {
+ const highlightedFilterId =
+ nativeFilters?.focusedFilterId || nativeFilters?.hoveredFilterId;
+ return highlightedFilterId
+ ? nativeFilters.filters[highlightedFilterId]?.tabsInScope
+ : undefined;
+ }, [nativeFilters]);
+
+ const tabsToHighlight = useTabHighlighting();
+
+ // Extract tab items creation logic into a memoized value (not a hook inside
hook)
+ const tabItems = useMemo(
+ () =>
+ tabIds.map((tabId, tabIndex) => ({
key: tabId,
label: removeDraggedTab(tabId) ? (
<></>
@@ -456,51 +442,20 @@ const Tabs = props => {
}
/>
),
- }));
-
- return (
- <StyledTabsContainer
- className="dashboard-component dashboard-component-tabs"
- data-test="dashboard-component-tabs"
- >
- {editMode && renderHoverMenu && (
- <HoverMenu innerRef={tabsDragSourceRef} position="left">
- <DragHandle position="left" />
- <DeleteComponentButton onDelete={handleDeleteComponent} />
- </HoverMenu>
- )}
-
- <LineEditableTabs
- id={tabsComponent.id}
- activeKey={activeKey}
- onChange={key => {
- handleClickTab(tabIds.indexOf(key));
- }}
- onEdit={handleEdit}
- data-test="nav-list"
- type={editMode ? 'editable-card' : 'card'}
- items={tabItems} // Pass the dynamically generated items array
- />
- </StyledTabsContainer>
- );
- },
+ })),
[
- editMode,
- renderHoverMenu,
- handleDeleteComponent,
- tabsComponent.id,
- activeKey,
- handleEdit,
tabIds,
- handleClickTab,
removeDraggedTab,
showDropIndicators,
+ tabsComponent.id,
depth,
availableColumnCount,
columnWidth,
handleDropOnTab,
handleGetDropPosition,
handleDragggingTab,
+ handleClickTab,
+ activeKey,
tabsToHighlight,
renderTabContent,
onResizeStart,
@@ -511,6 +466,36 @@ const Tabs = props => {
],
);
+ const renderChild = useCallback(
+ ({ dragSourceRef: tabsDragSourceRef }) => (
+ <TabsRenderer
+ tabItems={tabItems}
+ editMode={editMode}
+ renderHoverMenu={renderHoverMenu}
+ tabsDragSourceRef={tabsDragSourceRef}
+ handleDeleteComponent={handleDeleteComponent}
+ tabsComponent={tabsComponent}
+ activeKey={activeKey}
+ tabIds={tabIds}
+ handleClickTab={handleClickTab}
+ handleEdit={handleEdit}
+ tabBarPaddingLeft={tabBarPaddingLeft}
+ />
+ ),
+ [
+ tabItems,
+ editMode,
+ renderHoverMenu,
+ handleDeleteComponent,
+ tabsComponent,
+ activeKey,
+ tabIds,
+ handleClickTab,
+ handleEdit,
+ tabBarPaddingLeft,
+ ],
+ );
+
return (
<>
<Draggable
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/Tabs/Tabs.test.tsx
similarity index 87%
rename from
superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx
rename to
superset-frontend/src/dashboard/components/gridComponents/Tabs/Tabs.test.tsx
index 9fdf935d2a..573fa268f6 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx
+++
b/superset-frontend/src/dashboard/components/gridComponents/Tabs/Tabs.test.tsx
@@ -59,9 +59,10 @@ jest.mock('src/dashboard/util/getLeafComponentIdFromPath',
() => jest.fn());
jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({
Draggable: jest.fn(props => {
+ const mockElement = { tagName: 'DIV', dataset: {} };
const childProps = props.editMode
? {
- dragSourceRef: props.dragSourceRef,
+ dragSourceRef: { current: mockElement },
dropIndicatorProps: props.dropIndicatorProps,
}
: {};
@@ -135,6 +136,36 @@ test('Should render editMode:true', () => {
expect(DeleteComponentButton).toHaveBeenCalledTimes(1);
});
+test('Should render HoverMenu in editMode', () => {
+ const props = createProps();
+ const { container } = render(<Tabs {...props} />, {
+ useRedux: true,
+ useDnd: true,
+ });
+ // HoverMenu is rendered inside TabsRenderer when editMode is true
+ expect(container.querySelector('.hover-menu')).toBeInTheDocument();
+});
+
+test('Should not render HoverMenu when not in editMode', () => {
+ const props = createProps();
+ props.editMode = false;
+ const { container } = render(<Tabs {...props} />, {
+ useRedux: true,
+ useDnd: true,
+ });
+ expect(container.querySelector('.hover-menu')).not.toBeInTheDocument();
+});
+
+test('Should not render HoverMenu when renderHoverMenu is false', () => {
+ const props = createProps();
+ props.renderHoverMenu = false;
+ const { container } = render(<Tabs {...props} />, {
+ useRedux: true,
+ useDnd: true,
+ });
+ expect(container.querySelector('.hover-menu')).not.toBeInTheDocument();
+});
+
test('Should render editMode:false', () => {
const props = createProps();
props.editMode = false;
diff --git
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/Tabs/index.js
similarity index 57%
copy from
superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
copy to superset-frontend/src/dashboard/components/gridComponents/Tabs/index.js
index 24a6a78d3f..30383821d3 100644
---
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs/index.js
@@ -16,21 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
+import Tabs from './Tabs';
-import { render, screen } from 'spec/helpers/testing-library';
-import BuilderComponentPane from '.';
-
-jest.mock('src/dashboard/containers/SliceAdder', () => () => (
- <div data-test="mock-slice-adder" />
-));
-
-test('BuilderComponentPane has correct tabs in correct order', () => {
- render(<BuilderComponentPane topOffset={115} />);
- const tabs = screen.getAllByRole('tab');
- expect(tabs).toHaveLength(2);
- expect(tabs[0]).toHaveTextContent('Charts');
- expect(tabs[1]).toHaveTextContent('Layout elements');
- expect(screen.getByRole('tab', { selected: true })).toHaveTextContent(
- 'Charts',
- );
-});
+export default Tabs;
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.test.tsx
new file mode 100644
index 0000000000..bbba9f01eb
--- /dev/null
+++
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.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 { fireEvent, render, screen } from 'spec/helpers/testing-library';
+import TabsRenderer, { TabItem, TabsRendererProps } from './TabsRenderer';
+
+const mockTabItems: TabItem[] = [
+ {
+ key: 'tab-1',
+ label: <div>Tab 1</div>,
+ closeIcon: <div>×</div>,
+ children: <div>Tab 1 Content</div>,
+ },
+ {
+ key: 'tab-2',
+ label: <div>Tab 2</div>,
+ closeIcon: <div>×</div>,
+ children: <div>Tab 2 Content</div>,
+ },
+];
+
+const mockProps: TabsRendererProps = {
+ tabItems: mockTabItems,
+ editMode: false,
+ renderHoverMenu: true,
+ tabsDragSourceRef: undefined,
+ handleDeleteComponent: jest.fn(),
+ tabsComponent: { id: 'test-tabs-id' },
+ activeKey: 'tab-1',
+ tabIds: ['tab-1', 'tab-2'],
+ handleClickTab: jest.fn(),
+ handleEdit: jest.fn(),
+ tabBarPaddingLeft: 16,
+};
+
+describe('TabsRenderer', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('renders tabs container with correct test attributes', () => {
+ render(<TabsRenderer {...mockProps} />);
+
+ const tabsContainer = screen.getByTestId('dashboard-component-tabs');
+
+ expect(tabsContainer).toBeInTheDocument();
+ expect(tabsContainer).toHaveClass('dashboard-component-tabs');
+ });
+
+ test('renders LineEditableTabs with correct props', () => {
+ render(<TabsRenderer {...mockProps} />);
+
+ const editableTabs = screen.getByTestId('nav-list');
+ expect(editableTabs).toBeInTheDocument();
+ });
+
+ test('applies correct tab bar padding', () => {
+ const { rerender } = render(<TabsRenderer {...mockProps} />);
+
+ let editableTabs = screen.getByTestId('nav-list');
+ expect(editableTabs).toBeInTheDocument();
+
+ rerender(<TabsRenderer {...mockProps} tabBarPaddingLeft={0} />);
+ editableTabs = screen.getByTestId('nav-list');
+
+ expect(editableTabs).toBeInTheDocument();
+ });
+
+ test('calls handleClickTab when tab is clicked', () => {
+ const handleClickTabMock = jest.fn();
+ const propsWithTab2Active = {
+ ...mockProps,
+ activeKey: 'tab-2',
+ handleClickTab: handleClickTabMock,
+ };
+ render(<TabsRenderer {...propsWithTab2Active} />);
+
+ const tabElement = screen.getByText('Tab 1').closest('[role="tab"]');
+ expect(tabElement).not.toBeNull();
+
+ fireEvent.click(tabElement!);
+
+ expect(handleClickTabMock).toHaveBeenCalledWith(0);
+ expect(handleClickTabMock).toHaveBeenCalledTimes(1);
+ });
+
+ test('shows hover menu in edit mode', () => {
+ const mockRef = { current: null };
+ const editModeProps: TabsRendererProps = {
+ ...mockProps,
+ editMode: true,
+ renderHoverMenu: true,
+ tabsDragSourceRef: mockRef,
+ };
+
+ render(<TabsRenderer {...editModeProps} />);
+
+ const hoverMenu = document.querySelector('.hover-menu');
+
+ expect(hoverMenu).toBeInTheDocument();
+ });
+
+ test('hides hover menu when not in edit mode', () => {
+ const viewModeProps: TabsRendererProps = {
+ ...mockProps,
+ editMode: false,
+ renderHoverMenu: true,
+ };
+
+ render(<TabsRenderer {...viewModeProps} />);
+
+ const hoverMenu = document.querySelector('.hover-menu');
+
+ expect(hoverMenu).not.toBeInTheDocument();
+ });
+
+ test('hides hover menu when renderHoverMenu is false', () => {
+ const mockRef = { current: null };
+ const noHoverMenuProps: TabsRendererProps = {
+ ...mockProps,
+ editMode: true,
+ renderHoverMenu: false,
+ tabsDragSourceRef: mockRef,
+ };
+
+ render(<TabsRenderer {...noHoverMenuProps} />);
+
+ const hoverMenu = document.querySelector('.hover-menu');
+
+ expect(hoverMenu).not.toBeInTheDocument();
+ });
+
+ test('renders with correct tab type based on edit mode', () => {
+ const { rerender } = render(
+ <TabsRenderer {...mockProps} editMode={false} />,
+ );
+
+ let editableTabs = screen.getByTestId('nav-list');
+ expect(editableTabs).toBeInTheDocument();
+
+ rerender(<TabsRenderer {...mockProps} editMode />);
+
+ editableTabs = screen.getByTestId('nav-list');
+
+ expect(editableTabs).toBeInTheDocument();
+ });
+
+ test('handles default props correctly', () => {
+ const minimalProps: TabsRendererProps = {
+ tabItems: mockProps.tabItems,
+ editMode: false,
+ handleDeleteComponent: mockProps.handleDeleteComponent,
+ tabsComponent: mockProps.tabsComponent,
+ activeKey: mockProps.activeKey,
+ tabIds: mockProps.tabIds,
+ handleClickTab: mockProps.handleClickTab,
+ handleEdit: mockProps.handleEdit,
+ };
+
+ render(<TabsRenderer {...minimalProps} />);
+
+ const tabsContainer = screen.getByTestId('dashboard-component-tabs');
+
+ expect(tabsContainer).toBeInTheDocument();
+ });
+
+ test('calls onEdit when edit action is triggered', () => {
+ const handleEditMock = jest.fn();
+ const editableProps = {
+ ...mockProps,
+ editMode: true,
+ handleEdit: handleEditMock,
+ };
+
+ render(<TabsRenderer {...editableProps} />);
+
+ expect(screen.getByTestId('nav-list')).toBeInTheDocument();
+ });
+
+ test('renders tab content correctly', () => {
+ render(<TabsRenderer {...mockProps} />);
+
+ expect(screen.getByText('Tab 1 Content')).toBeInTheDocument();
+ expect(screen.queryByText('Tab 2 Content')).not.toBeInTheDocument(); //
Not active
+ });
+});
diff --git
a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
new file mode 100644
index 0000000000..52e34638f7
--- /dev/null
+++
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
@@ -0,0 +1,121 @@
+/**
+ * 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 { memo, ReactElement, RefObject } from 'react';
+import { styled } from '@superset-ui/core';
+import {
+ LineEditableTabs,
+ TabsProps as AntdTabsProps,
+} from '@superset-ui/core/components/Tabs';
+import HoverMenu from '../../menu/HoverMenu';
+import DragHandle from '../../dnd/DragHandle';
+import DeleteComponentButton from '../../DeleteComponentButton';
+
+const StyledTabsContainer = styled.div`
+ width: 100%;
+ background-color: ${({ theme }) => theme.colorBgContainer};
+
+ & .dashboard-component-tabs-content {
+ height: 100%;
+ }
+
+ & > .hover-menu:hover {
+ opacity: 1;
+ }
+
+ &.dragdroppable-row .dashboard-component-tabs-content {
+ height: calc(100% - 47px);
+ }
+`;
+
+export interface TabItem {
+ key: string;
+ label: ReactElement;
+ closeIcon: ReactElement;
+ children: ReactElement;
+}
+
+export interface TabsComponent {
+ id: string;
+}
+
+export interface TabsRendererProps {
+ tabItems: TabItem[];
+ editMode: boolean;
+ renderHoverMenu?: boolean;
+ tabsDragSourceRef?: RefObject<HTMLDivElement>;
+ handleDeleteComponent: () => void;
+ tabsComponent: TabsComponent;
+ activeKey: string;
+ tabIds: string[];
+ handleClickTab: (index: number) => void;
+ handleEdit: AntdTabsProps['onEdit'];
+ tabBarPaddingLeft?: number;
+}
+
+/**
+ * TabsRenderer component handles the rendering of dashboard tabs
+ * Extracted from the main Tabs component for better separation of concerns
+ */
+const TabsRenderer = memo<TabsRendererProps>(
+ ({
+ tabItems,
+ editMode,
+ renderHoverMenu = true,
+ tabsDragSourceRef,
+ handleDeleteComponent,
+ tabsComponent,
+ activeKey,
+ tabIds,
+ handleClickTab,
+ handleEdit,
+ tabBarPaddingLeft = 0,
+ }) => (
+ <StyledTabsContainer
+ className="dashboard-component dashboard-component-tabs"
+ data-test="dashboard-component-tabs"
+ >
+ {editMode && renderHoverMenu && tabsDragSourceRef && (
+ <HoverMenu innerRef={tabsDragSourceRef} position="left">
+ <DragHandle position="left" />
+ <DeleteComponentButton onDelete={handleDeleteComponent} />
+ </HoverMenu>
+ )}
+
+ <LineEditableTabs
+ id={tabsComponent.id}
+ activeKey={activeKey}
+ onChange={key => {
+ if (typeof key === 'string') {
+ const tabIndex = tabIds.indexOf(key);
+ if (tabIndex !== -1) handleClickTab(tabIndex);
+ }
+ }}
+ onEdit={handleEdit}
+ data-test="nav-list"
+ type={editMode ? 'editable-card' : 'card'}
+ items={tabItems}
+ tabBarStyle={{ paddingLeft: tabBarPaddingLeft }}
+ />
+ </StyledTabsContainer>
+ ),
+);
+
+TabsRenderer.displayName = 'TabsRenderer';
+
+export default TabsRenderer;
diff --git
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/index.ts
similarity index 57%
copy from
superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
copy to
superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/index.ts
index 24a6a78d3f..320e8ebb02 100644
---
a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx
+++
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/index.ts
@@ -16,21 +16,5 @@
* specific language governing permissions and limitations
* under the License.
*/
-
-import { render, screen } from 'spec/helpers/testing-library';
-import BuilderComponentPane from '.';
-
-jest.mock('src/dashboard/containers/SliceAdder', () => () => (
- <div data-test="mock-slice-adder" />
-));
-
-test('BuilderComponentPane has correct tabs in correct order', () => {
- render(<BuilderComponentPane topOffset={115} />);
- const tabs = screen.getAllByRole('tab');
- expect(tabs).toHaveLength(2);
- expect(tabs[0]).toHaveTextContent('Charts');
- expect(tabs[1]).toHaveTextContent('Layout elements');
- expect(screen.getByRole('tab', { selected: true })).toHaveTextContent(
- 'Charts',
- );
-});
+export { default } from './TabsRenderer';
+export type { TabsRendererProps, TabItem, TabsComponent } from
'./TabsRenderer';
diff --git a/superset-frontend/src/dashboard/components/gridComponents/index.js
b/superset-frontend/src/dashboard/components/gridComponents/index.js
index 38f3558864..8d3078c9eb 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/index.js
+++ b/superset-frontend/src/dashboard/components/gridComponents/index.js
@@ -38,16 +38,6 @@ import Tab from './Tab';
import Tabs from './Tabs';
import DynamicComponent from './DynamicComponent';
-export { default as ChartHolder } from './ChartHolder';
-export { default as Markdown } from './Markdown';
-export { default as Column } from './Column';
-export { default as Divider } from './Divider';
-export { default as Header } from './Header';
-export { default as Row } from './Row';
-export { default as Tab } from './Tab';
-export { default as Tabs } from './Tabs';
-export { default as DynamicComponent } from './DynamicComponent';
-
export const componentLookup = {
[CHART_TYPE]: ChartHolder,
[MARKDOWN_TYPE]: Markdown,
diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js
b/superset-frontend/src/dashboard/reducers/dashboardState.js
index 3c7c65c601..7897584b23 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardState.js
+++ b/superset-frontend/src/dashboard/reducers/dashboardState.js
@@ -50,6 +50,7 @@ import {
SET_DASHBOARD_LABELS_COLORMAP_SYNCED,
SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCABLE,
SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED,
+ TOGGLE_NATIVE_FILTERS_BAR,
} from '../actions/dashboardState';
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
@@ -271,6 +272,12 @@ export default function dashboardStateReducer(state = {},
action) {
datasetsStatus: action.status,
};
},
+ [TOGGLE_NATIVE_FILTERS_BAR]() {
+ return {
+ ...state,
+ nativeFiltersBarOpen: action.isOpen,
+ };
+ },
};
if (action.type in actionHandlers) {
diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.test.js
b/superset-frontend/src/dashboard/reducers/dashboardState.test.js
index 39798ecf13..803e159657 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardState.test.js
+++ b/superset-frontend/src/dashboard/reducers/dashboardState.test.js
@@ -27,6 +27,7 @@ import {
SET_UNSAVED_CHANGES,
TOGGLE_EXPAND_SLICE,
TOGGLE_FAVE_STAR,
+ TOGGLE_NATIVE_FILTERS_BAR,
UNSET_FOCUSED_FILTER_FIELD,
} from 'src/dashboard/actions/dashboardState';
@@ -197,4 +198,20 @@ describe('dashboardState reducer', () => {
column: 'column_2',
});
});
+
+ it('should toggle native filters bar', () => {
+ expect(
+ dashboardStateReducer(
+ { nativeFiltersBarOpen: false },
+ { type: TOGGLE_NATIVE_FILTERS_BAR, isOpen: true },
+ ),
+ ).toEqual({ nativeFiltersBarOpen: true });
+
+ expect(
+ dashboardStateReducer(
+ { nativeFiltersBarOpen: true },
+ { type: TOGGLE_NATIVE_FILTERS_BAR, isOpen: false },
+ ),
+ ).toEqual({ nativeFiltersBarOpen: false });
+ });
});
diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.test.ts
b/superset-frontend/src/dashboard/reducers/dashboardState.test.ts
index 5e77b41022..1594c1b2a6 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardState.test.ts
+++ b/superset-frontend/src/dashboard/reducers/dashboardState.test.ts
@@ -20,10 +20,39 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import dashboardStateReducer from './dashboardState';
import { setActiveTab, setActiveTabs } from '../actions/dashboardState';
+import { DashboardState } from '../types';
+
+// Type the reducer function properly since it's imported from JS
+type DashboardStateReducer = (
+ state: Partial<DashboardState> | undefined,
+ action: any,
+) => Partial<DashboardState>;
+const typedDashboardStateReducer =
+ dashboardStateReducer as DashboardStateReducer;
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
+// Helper function to create mock dashboard state with proper types
+const createMockDashboardState = (
+ overrides: Partial<DashboardState> = {},
+): DashboardState => ({
+ editMode: false,
+ isPublished: false,
+ directPathToChild: [],
+ activeTabs: [],
+ fullSizeChartId: null,
+ isRefreshing: false,
+ isFiltersRefreshing: false,
+ hasUnsavedChanges: false,
+ dashboardIsSaving: false,
+ colorScheme: '',
+ sliceIds: [],
+ directPathLastUpdated: 0,
+ nativeFiltersBarOpen: false,
+ ...overrides,
+});
+
describe('DashboardState reducer', () => {
describe('SET_ACTIVE_TAB', () => {
it('switches a single tab', () => {
@@ -34,16 +63,28 @@ describe('DashboardState reducer', () => {
const request = setActiveTab('tab1');
const thunkAction = request(store.dispatch, store.getState);
- expect(dashboardStateReducer({ activeTabs: [] }, thunkAction)).toEqual({
- activeTabs: ['tab1'],
- inactiveTabs: [],
- });
+ expect(
+ typedDashboardStateReducer(
+ createMockDashboardState({ activeTabs: [] }),
+ thunkAction,
+ ),
+ ).toEqual(
+ expect.objectContaining({
+ activeTabs: ['tab1'],
+ inactiveTabs: [],
+ }),
+ );
const request2 = setActiveTab('tab2', 'tab1');
const thunkAction2 = request2(store.dispatch, store.getState);
expect(
- dashboardStateReducer({ activeTabs: ['tab1'] }, thunkAction2),
- ).toEqual({ activeTabs: ['tab2'], inactiveTabs: [] });
+ typedDashboardStateReducer(
+ createMockDashboardState({ activeTabs: ['tab1'] }),
+ thunkAction2,
+ ),
+ ).toEqual(
+ expect.objectContaining({ activeTabs: ['tab2'], inactiveTabs: [] }),
+ );
});
it('switches a multi-depth tab', () => {
@@ -63,75 +104,90 @@ describe('DashboardState reducer', () => {
});
let request = setActiveTab('TAB-B', 'TAB-A');
let thunkAction = request(store.dispatch, store.getState);
- let result = dashboardStateReducer(
- { activeTabs: ['TAB-1', 'TAB-A', 'TAB-__a'] },
+ let result = typedDashboardStateReducer(
+ createMockDashboardState({ activeTabs: ['TAB-1', 'TAB-A', 'TAB-__a']
}),
thunkAction,
);
- expect(result).toEqual({
- activeTabs: expect.arrayContaining(['TAB-1', 'TAB-B']),
- inactiveTabs: ['TAB-__a'],
- });
+ expect(result).toEqual(
+ expect.objectContaining({
+ activeTabs: expect.arrayContaining(['TAB-1', 'TAB-B']),
+ inactiveTabs: ['TAB-__a'],
+ }),
+ );
request = setActiveTab('TAB-2', 'TAB-1');
thunkAction = request(store.dispatch, () => ({
...(store.getState() ?? {}),
dashboardState: result,
}));
- result = dashboardStateReducer(result, thunkAction);
- expect(result).toEqual({
- activeTabs: ['TAB-2'],
- inactiveTabs: expect.arrayContaining(['TAB-B', 'TAB-__a']),
- });
+ result = typedDashboardStateReducer(result, thunkAction);
+ expect(result).toEqual(
+ expect.objectContaining({
+ activeTabs: ['TAB-2'],
+ inactiveTabs: expect.arrayContaining(['TAB-B', 'TAB-__a']),
+ }),
+ );
request = setActiveTab('TAB-1', 'TAB-2');
thunkAction = request(store.dispatch, () => ({
...(store.getState() ?? {}),
dashboardState: result,
}));
- result = dashboardStateReducer(result, thunkAction);
- expect(result).toEqual({
- activeTabs: expect.arrayContaining(['TAB-1', 'TAB-B']),
- inactiveTabs: ['TAB-__a'],
- });
+ result = typedDashboardStateReducer(result, thunkAction);
+ expect(result).toEqual(
+ expect.objectContaining({
+ activeTabs: expect.arrayContaining(['TAB-1', 'TAB-B']),
+ inactiveTabs: ['TAB-__a'],
+ }),
+ );
request = setActiveTab('TAB-A', 'TAB-B');
thunkAction = request(store.dispatch, () => ({
...(store.getState() ?? {}),
dashboardState: result,
}));
- result = dashboardStateReducer(result, thunkAction);
- expect(result).toEqual({
- activeTabs: expect.arrayContaining(['TAB-1', 'TAB-A', 'TAB-__a']),
- inactiveTabs: [],
- });
+ result = typedDashboardStateReducer(result, thunkAction);
+ expect(result).toEqual(
+ expect.objectContaining({
+ activeTabs: expect.arrayContaining(['TAB-1', 'TAB-A', 'TAB-__a']),
+ inactiveTabs: [],
+ }),
+ );
request = setActiveTab('TAB-2', 'TAB-1');
thunkAction = request(store.dispatch, () => ({
...(store.getState() ?? {}),
dashboardState: result,
}));
- result = dashboardStateReducer(result, thunkAction);
- expect(result).toEqual({
- activeTabs: expect.arrayContaining(['TAB-2']),
- inactiveTabs: ['TAB-A', 'TAB-__a'],
- });
+ result = typedDashboardStateReducer(result, thunkAction);
+ expect(result).toEqual(
+ expect.objectContaining({
+ activeTabs: expect.arrayContaining(['TAB-2']),
+ inactiveTabs: ['TAB-A', 'TAB-__a'],
+ }),
+ );
request = setActiveTab('TAB-1', 'TAB-2');
thunkAction = request(store.dispatch, () => ({
...(store.getState() ?? {}),
dashboardState: result,
}));
- result = dashboardStateReducer(result, thunkAction);
- expect(result).toEqual({
- activeTabs: expect.arrayContaining(['TAB-1', 'TAB-A', 'TAB-__a']),
- inactiveTabs: [],
- });
+ result = typedDashboardStateReducer(result, thunkAction);
+ expect(result).toEqual(
+ expect.objectContaining({
+ activeTabs: expect.arrayContaining(['TAB-1', 'TAB-A', 'TAB-__a']),
+ inactiveTabs: [],
+ }),
+ );
});
});
it('SET_ACTIVE_TABS', () => {
expect(
- dashboardStateReducer({ activeTabs: [] }, setActiveTabs(['tab1'])),
- ).toEqual({ activeTabs: ['tab1'] });
+ typedDashboardStateReducer(
+ createMockDashboardState({ activeTabs: [] }),
+ setActiveTabs(['tab1']),
+ ),
+ ).toEqual(expect.objectContaining({ activeTabs: ['tab1'] }));
expect(
- dashboardStateReducer(
- { activeTabs: ['tab1', 'tab2'] },
+ typedDashboardStateReducer(
+ createMockDashboardState({ activeTabs: ['tab1', 'tab2'] }),
setActiveTabs(['tab3', 'tab4']),
),
- ).toEqual({ activeTabs: ['tab3', 'tab4'] });
+ ).toEqual(expect.objectContaining({ activeTabs: ['tab3', 'tab4'] }));
});
});
diff --git a/superset-frontend/src/dashboard/types.ts
b/superset-frontend/src/dashboard/types.ts
index c7bf2c097a..15b9095932 100644
--- a/superset-frontend/src/dashboard/types.ts
+++ b/superset-frontend/src/dashboard/types.ts
@@ -107,6 +107,7 @@ export type DashboardState = {
colorScheme: string;
sliceIds: number[];
directPathLastUpdated: number;
+ nativeFiltersBarOpen?: boolean;
css?: string;
focusedFilterField?: {
chartId: number;