mirror of
https://github.com/apache/superset.git
synced 2024-09-17 11:09:47 -04:00
refactor: Boostrap to AntD - Tabs (#14048)
This commit is contained in:
parent
4410fd047e
commit
1d6a746a09
@ -24,7 +24,7 @@ import fetchMock from 'fetch-mock';
|
|||||||
import { ParentSize } from '@vx/responsive';
|
import { ParentSize } from '@vx/responsive';
|
||||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||||
import { Sticky, StickyContainer } from 'react-sticky';
|
import { Sticky, StickyContainer } from 'react-sticky';
|
||||||
import { TabContainer, TabContent, TabPane } from 'react-bootstrap';
|
import Tabs from 'src/components/Tabs';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane';
|
import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane';
|
||||||
@ -39,7 +39,10 @@ import {
|
|||||||
} from 'spec/fixtures/mockDashboardLayout';
|
} from 'spec/fixtures/mockDashboardLayout';
|
||||||
import { mockStoreWithTabs, storeWithState } from 'spec/fixtures/mockStore';
|
import { mockStoreWithTabs, storeWithState } from 'spec/fixtures/mockStore';
|
||||||
import mockState from 'spec/fixtures/mockState';
|
import mockState from 'spec/fixtures/mockState';
|
||||||
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
|
import {
|
||||||
|
DASHBOARD_ROOT_ID,
|
||||||
|
DASHBOARD_GRID_ID,
|
||||||
|
} from 'src/dashboard/util/constants';
|
||||||
|
|
||||||
fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
|
fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
|
||||||
|
|
||||||
@ -118,19 +121,17 @@ describe('DashboardBuilder', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a TabContainer and TabContent', () => {
|
it('should render one Tabs and two TabPane', () => {
|
||||||
const wrapper = setup({ dashboardLayout: undoableDashboardLayoutWithTabs });
|
const wrapper = setup({ dashboardLayout: undoableDashboardLayoutWithTabs });
|
||||||
const parentSize = wrapper.find(ParentSize);
|
const parentSize = wrapper.find(ParentSize);
|
||||||
expect(parentSize.find(TabContainer)).toHaveLength(1);
|
expect(parentSize.find(Tabs)).toHaveLength(1);
|
||||||
expect(parentSize.find(TabContent)).toHaveLength(1);
|
expect(parentSize.find(Tabs.TabPane)).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set animation=true, mountOnEnter=true, and unmounOnExit=false on TabContainer for perf', () => {
|
it('should set animated=true on Tabs for perf', () => {
|
||||||
const wrapper = setup({ dashboardLayout: undoableDashboardLayoutWithTabs });
|
const wrapper = setup({ dashboardLayout: undoableDashboardLayoutWithTabs });
|
||||||
const tabProps = wrapper.find(ParentSize).find(TabContainer).props();
|
const tabProps = wrapper.find(ParentSize).find(Tabs).props();
|
||||||
expect(tabProps.animation).toBe(true);
|
expect(tabProps.animated).toBe(true);
|
||||||
expect(tabProps.mountOnEnter).toBe(true);
|
|
||||||
expect(tabProps.unmountOnExit).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a TabPane and DashboardGrid for first Tab', () => {
|
it('should render a TabPane and DashboardGrid for first Tab', () => {
|
||||||
@ -138,10 +139,10 @@ describe('DashboardBuilder', () => {
|
|||||||
const parentSize = wrapper.find(ParentSize);
|
const parentSize = wrapper.find(ParentSize);
|
||||||
const expectedCount =
|
const expectedCount =
|
||||||
undoableDashboardLayoutWithTabs.present.TABS_ID.children.length;
|
undoableDashboardLayoutWithTabs.present.TABS_ID.children.length;
|
||||||
expect(parentSize.find(TabPane)).toHaveLength(expectedCount);
|
expect(parentSize.find(Tabs.TabPane)).toHaveLength(expectedCount);
|
||||||
expect(parentSize.find(TabPane).first().find(DashboardGrid)).toHaveLength(
|
expect(
|
||||||
1,
|
parentSize.find(Tabs.TabPane).first().find(DashboardGrid),
|
||||||
);
|
).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a TabPane and DashboardGrid for second Tab', () => {
|
it('should render a TabPane and DashboardGrid for second Tab', () => {
|
||||||
@ -155,8 +156,10 @@ describe('DashboardBuilder', () => {
|
|||||||
const parentSize = wrapper.find(ParentSize);
|
const parentSize = wrapper.find(ParentSize);
|
||||||
const expectedCount =
|
const expectedCount =
|
||||||
undoableDashboardLayoutWithTabs.present.TABS_ID.children.length;
|
undoableDashboardLayoutWithTabs.present.TABS_ID.children.length;
|
||||||
expect(parentSize.find(TabPane)).toHaveLength(expectedCount);
|
expect(parentSize.find(Tabs.TabPane)).toHaveLength(expectedCount);
|
||||||
expect(parentSize.find(TabPane).at(1).find(DashboardGrid)).toHaveLength(1);
|
expect(
|
||||||
|
parentSize.find(Tabs.TabPane).at(1).find(DashboardGrid),
|
||||||
|
).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a BuilderComponentPane if editMode=false and user selects "Insert Components" pane', () => {
|
it('should render a BuilderComponentPane if editMode=false and user selects "Insert Components" pane', () => {
|
||||||
@ -179,7 +182,7 @@ describe('DashboardBuilder', () => {
|
|||||||
dashboardLayout: undoableDashboardLayoutWithTabs,
|
dashboardLayout: undoableDashboardLayoutWithTabs,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.find(TabContainer).prop('activeKey')).toBe(0);
|
expect(wrapper.find(Tabs).prop('activeKey')).toBe(DASHBOARD_GRID_ID);
|
||||||
|
|
||||||
wrapper
|
wrapper
|
||||||
.find('.dashboard-component-tabs .ant-tabs .ant-tabs-tab')
|
.find('.dashboard-component-tabs .ant-tabs .ant-tabs-tab')
|
||||||
|
@ -18,13 +18,11 @@
|
|||||||
*/
|
*/
|
||||||
import React, { useState, useRef, useCallback } from 'react';
|
import React, { useState, useRef, useCallback } from 'react';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { withKnobs, boolean } from '@storybook/addon-knobs';
|
import { withKnobs } from '@storybook/addon-knobs';
|
||||||
import { CronPicker, CronError } from 'src/components/CronPicker';
|
import { CronPicker, CronError } from 'src/components/CronPicker';
|
||||||
import Modal from 'src/components/Modal';
|
import Modal from 'src/components/Modal';
|
||||||
import InfoTooltip from 'src/components/InfoTooltip';
|
import InfoTooltip from 'src/components/InfoTooltip';
|
||||||
import { Dropdown } from 'src/components/Dropdown';
|
import { Input, Divider } from '.';
|
||||||
import Tabs, { EditableTabs } from 'src/components/Tabs';
|
|
||||||
import { Menu, Input, Divider } from '.';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Common components',
|
title: 'Common components',
|
||||||
@ -45,80 +43,6 @@ export const StyledModal = () => (
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const StyledTabs = () => (
|
|
||||||
<Tabs
|
|
||||||
defaultActiveKey="1"
|
|
||||||
centered={boolean('Center tabs', false)}
|
|
||||||
fullWidth={boolean('Full width', true)}
|
|
||||||
>
|
|
||||||
<Tabs.TabPane
|
|
||||||
tab="Tab 1"
|
|
||||||
key="1"
|
|
||||||
disabled={boolean('Tab 1 disabled', false)}
|
|
||||||
>
|
|
||||||
Tab 1 Content!
|
|
||||||
</Tabs.TabPane>
|
|
||||||
<Tabs.TabPane
|
|
||||||
tab="Tab 2"
|
|
||||||
key="2"
|
|
||||||
disabled={boolean('Tab 2 disabled', false)}
|
|
||||||
>
|
|
||||||
Tab 2 Content!
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const StyledEditableTabs = () => (
|
|
||||||
<EditableTabs
|
|
||||||
defaultActiveKey="1"
|
|
||||||
centered={boolean('Center tabs', false)}
|
|
||||||
fullWidth={boolean('Full width', true)}
|
|
||||||
>
|
|
||||||
<Tabs.TabPane
|
|
||||||
tab="Tab 1"
|
|
||||||
key="1"
|
|
||||||
disabled={boolean('Tab 1 disabled', false)}
|
|
||||||
>
|
|
||||||
Tab 1 Content!
|
|
||||||
</Tabs.TabPane>
|
|
||||||
<Tabs.TabPane
|
|
||||||
tab="Tab 2"
|
|
||||||
key="2"
|
|
||||||
disabled={boolean('Tab 2 disabled', false)}
|
|
||||||
>
|
|
||||||
Tab 2 Content!
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</EditableTabs>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const TabsWithDropdownMenu = () => (
|
|
||||||
<EditableTabs
|
|
||||||
defaultActiveKey="1"
|
|
||||||
centered={boolean('Center tabs', false)}
|
|
||||||
fullWidth={boolean('Full width', true)}
|
|
||||||
>
|
|
||||||
<Tabs.TabPane
|
|
||||||
tab={
|
|
||||||
<>
|
|
||||||
<Dropdown
|
|
||||||
overlay={
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item key="1">Item 1</Menu.Item>
|
|
||||||
<Menu.Item key="2">Item 2</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Tab with dropdown menu
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
key="1"
|
|
||||||
disabled={boolean('Tab 1 disabled', false)}
|
|
||||||
>
|
|
||||||
Tab 1 Content!
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</EditableTabs>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const StyledInfoTooltip = (args: any) => {
|
export const StyledInfoTooltip = (args: any) => {
|
||||||
const styles = {
|
const styles = {
|
||||||
padding: '100px 0 0 200px',
|
padding: '100px 0 0 200px',
|
||||||
|
68
superset-frontend/src/components/Tabs/Tabs.stories.tsx
Normal file
68
superset-frontend/src/components/Tabs/Tabs.stories.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* 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 React from 'react';
|
||||||
|
import Tabs, { TabsProps } from '.';
|
||||||
|
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Tabs',
|
||||||
|
component: Tabs,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InteractiveTabs = (args: TabsProps) => (
|
||||||
|
<Tabs {...args}>
|
||||||
|
<TabPane tab="Tab 1" key="1">
|
||||||
|
Content of Tab Pane 1
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tab="Tab 2" key="2">
|
||||||
|
Content of Tab Pane 2
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tab="Tab 3" key="3">
|
||||||
|
Content of Tab Pane 3
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
|
||||||
|
InteractiveTabs.args = {
|
||||||
|
defaultActiveKey: '1',
|
||||||
|
animated: true,
|
||||||
|
centered: false,
|
||||||
|
fullWidth: false,
|
||||||
|
allowOverflow: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
InteractiveTabs.argTypes = {
|
||||||
|
onChange: { action: 'onChange' },
|
||||||
|
type: {
|
||||||
|
defaultValue: 'line',
|
||||||
|
control: {
|
||||||
|
type: 'inline-radio',
|
||||||
|
options: ['line', 'card', 'editable-card'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
InteractiveTabs.story = {
|
||||||
|
parameters: {
|
||||||
|
knobs: {
|
||||||
|
disable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -18,17 +18,17 @@
|
|||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { css, styled } from '@superset-ui/core';
|
import { css, styled } from '@superset-ui/core';
|
||||||
import { Tabs as AntdTabs } from 'src/common/components';
|
import AntDTabs, { TabsProps as AntDTabsProps } from 'antd/lib/tabs';
|
||||||
import Icon from 'src/components/Icon';
|
import Icon from 'src/components/Icon';
|
||||||
|
|
||||||
interface TabsProps {
|
export interface TabsProps extends AntDTabsProps {
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
allowOverflow?: boolean;
|
allowOverflow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const notForwardedProps = ['fullWidth', 'allowOverflow'];
|
const notForwardedProps = ['fullWidth', 'allowOverflow'];
|
||||||
|
|
||||||
const StyledTabs = styled(AntdTabs, {
|
const StyledTabs = styled(AntDTabs, {
|
||||||
shouldForwardProp: prop => !notForwardedProps.includes(prop),
|
shouldForwardProp: prop => !notForwardedProps.includes(prop),
|
||||||
})<TabsProps>`
|
})<TabsProps>`
|
||||||
overflow: ${({ allowOverflow }) => (allowOverflow ? 'visible' : 'hidden')};
|
overflow: ${({ allowOverflow }) => (allowOverflow ? 'visible' : 'hidden')};
|
||||||
@ -96,7 +96,7 @@ const StyledTabs = styled(AntdTabs, {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTabPane = styled(AntdTabs.TabPane)``;
|
const StyledTabPane = styled(AntDTabs.TabPane)``;
|
||||||
|
|
||||||
const Tabs = Object.assign(StyledTabs, {
|
const Tabs = Object.assign(StyledTabs, {
|
||||||
TabPane: StyledTabPane,
|
TabPane: StyledTabPane,
|
||||||
@ -104,6 +104,7 @@ const Tabs = Object.assign(StyledTabs, {
|
|||||||
|
|
||||||
Tabs.defaultProps = {
|
Tabs.defaultProps = {
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
|
animated: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledEditableTabs = styled(StyledTabs)`
|
const StyledEditableTabs = styled(StyledTabs)`
|
||||||
|
@ -244,10 +244,7 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
|
|||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</StickyVerticalBar>
|
</StickyVerticalBar>
|
||||||
)}
|
)}
|
||||||
<DashboardContainer
|
<DashboardContainer topLevelTabs={topLevelTabs} />
|
||||||
topLevelTabs={topLevelTabs}
|
|
||||||
handleChangeTab={handleChangeTab}
|
|
||||||
/>
|
|
||||||
{editMode && <BuilderComponentPane topOffset={barTopOffset} />}
|
{editMode && <BuilderComponentPane topOffset={barTopOffset} />}
|
||||||
</StyledDashboardContent>
|
</StyledDashboardContent>
|
||||||
<ToastPresenter />
|
<ToastPresenter />
|
||||||
|
@ -19,8 +19,8 @@
|
|||||||
// ParentSize uses resize observer so the dashboard will update size
|
// ParentSize uses resize observer so the dashboard will update size
|
||||||
// when its container size changes, due to e.g., builder side panel opening
|
// when its container size changes, due to e.g., builder side panel opening
|
||||||
import { ParentSize } from '@vx/responsive';
|
import { ParentSize } from '@vx/responsive';
|
||||||
import { TabContainer, TabContent, TabPane } from 'react-bootstrap';
|
import Tabs from 'src/components/Tabs';
|
||||||
import React, { FC, SyntheticEvent, useEffect, useState } from 'react';
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import DashboardGrid from 'src/dashboard/containers/DashboardGrid';
|
import DashboardGrid from 'src/dashboard/containers/DashboardGrid';
|
||||||
import getLeafComponentIdFromPath from 'src/dashboard/util/getLeafComponentIdFromPath';
|
import getLeafComponentIdFromPath from 'src/dashboard/util/getLeafComponentIdFromPath';
|
||||||
@ -33,13 +33,9 @@ import { getRootLevelTabIndex } from './utils';
|
|||||||
|
|
||||||
type DashboardContainerProps = {
|
type DashboardContainerProps = {
|
||||||
topLevelTabs?: LayoutItem;
|
topLevelTabs?: LayoutItem;
|
||||||
handleChangeTab: (event: SyntheticEvent<TabContainer, Event>) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DashboardContainer: FC<DashboardContainerProps> = ({
|
const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||||
topLevelTabs,
|
|
||||||
handleChangeTab,
|
|
||||||
}) => {
|
|
||||||
const dashboardLayout = useSelector<RootState, DashboardLayout>(
|
const dashboardLayout = useSelector<RootState, DashboardLayout>(
|
||||||
state => state.dashboardLayout.present,
|
state => state.dashboardLayout.present,
|
||||||
);
|
);
|
||||||
@ -58,6 +54,9 @@ const DashboardContainer: FC<DashboardContainerProps> = ({
|
|||||||
? topLevelTabs.children
|
? topLevelTabs.children
|
||||||
: [DASHBOARD_GRID_ID];
|
: [DASHBOARD_GRID_ID];
|
||||||
|
|
||||||
|
const min = Math.min(tabIndex, childIds.length - 1);
|
||||||
|
const activeKey = min === 0 ? DASHBOARD_GRID_ID : min.toString();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid-container" data-test="grid-container">
|
<div className="grid-container" data-test="grid-container">
|
||||||
<ParentSize>
|
<ParentSize>
|
||||||
@ -68,23 +67,18 @@ const DashboardContainer: FC<DashboardContainerProps> = ({
|
|||||||
the entire dashboard upon adding/removing top-level tabs, which would otherwise
|
the entire dashboard upon adding/removing top-level tabs, which would otherwise
|
||||||
happen because of React's diffing algorithm
|
happen because of React's diffing algorithm
|
||||||
*/
|
*/
|
||||||
<TabContainer
|
<Tabs
|
||||||
id={DASHBOARD_GRID_ID}
|
id={DASHBOARD_GRID_ID}
|
||||||
activeKey={Math.min(tabIndex, childIds.length - 1)}
|
activeKey={activeKey}
|
||||||
onSelect={handleChangeTab}
|
renderTabBar={() => <></>}
|
||||||
// @ts-ignore
|
fullWidth={false}
|
||||||
animation
|
|
||||||
mountOnEnter
|
|
||||||
unmountOnExit={false}
|
|
||||||
>
|
>
|
||||||
<TabContent>
|
|
||||||
{childIds.map((id, index) => (
|
{childIds.map((id, index) => (
|
||||||
// Matching the key of the first TabPane irrespective of topLevelTabs
|
// Matching the key of the first TabPane irrespective of topLevelTabs
|
||||||
// lets us keep the same React component tree when !!topLevelTabs changes.
|
// lets us keep the same React component tree when !!topLevelTabs changes.
|
||||||
// This avoids expensive mounts/unmounts of the entire dashboard.
|
// This avoids expensive mounts/unmounts of the entire dashboard.
|
||||||
<TabPane
|
<Tabs.TabPane
|
||||||
key={index === 0 ? DASHBOARD_GRID_ID : id}
|
key={index === 0 ? DASHBOARD_GRID_ID : index.toString()}
|
||||||
eventKey={index}
|
|
||||||
>
|
>
|
||||||
<DashboardGrid
|
<DashboardGrid
|
||||||
gridComponent={dashboardLayout[id]}
|
gridComponent={dashboardLayout[id]}
|
||||||
@ -93,10 +87,9 @@ const DashboardContainer: FC<DashboardContainerProps> = ({
|
|||||||
width={width}
|
width={width}
|
||||||
isComponentVisible={index === tabIndex}
|
isComponentVisible={index === tabIndex}
|
||||||
/>
|
/>
|
||||||
</TabPane>
|
</Tabs.TabPane>
|
||||||
))}
|
))}
|
||||||
</TabContent>
|
</Tabs>
|
||||||
</TabContainer>
|
|
||||||
)}
|
)}
|
||||||
</ParentSize>
|
</ParentSize>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user