refactor(Button): Upgrade Button component to Antd5 (#31623)

This commit is contained in:
alexandrusoare 2025-01-09 12:04:59 +02:00 committed by GitHub
parent 840a920aba
commit 9cd3a8d5b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 114 additions and 56 deletions

View File

@ -106,7 +106,7 @@ export const databasesPage = {
alertMessage: '.antd5-alert-message', alertMessage: '.antd5-alert-message',
errorField: '[role="alert"]', errorField: '[role="alert"]',
uploadJson: '[title="Upload JSON file"]', uploadJson: '[title="Upload JSON file"]',
chooseFile: '[class="ant-btn input-upload-btn"]', chooseFile: '[class="antd5-btn input-upload-btn"]',
additionalParameters: '[name="query_input"]', additionalParameters: '[name="query_input"]',
sqlAlchemyUriInput: dataTestLocator('sqlalchemy-uri-input'), sqlAlchemyUriInput: dataTestLocator('sqlalchemy-uri-input'),
advancedTab: '#rc-tabs-0-tab-2', advancedTab: '#rc-tabs-0-tab-2',
@ -148,7 +148,7 @@ export const sqlLabView = {
examplesMenuItem: '[title="examples"]', examplesMenuItem: '[title="examples"]',
tableInput: ':nth-child(4) > .select > :nth-child(1)', tableInput: ':nth-child(4) > .select > :nth-child(1)',
sqlEditor: '#brace-editor textarea', sqlEditor: '#brace-editor textarea',
saveAsButton: '.SaveQuery > .ant-btn', saveAsButton: '.SaveQuery > .antd5-btn',
saveAsModal: { saveAsModal: {
footer: '.antd5-modal-footer', footer: '.antd5-modal-footer',
queryNameInput: 'input[class^="ant-input"]', queryNameInput: 'input[class^="ant-input"]',
@ -195,7 +195,7 @@ export const savedQuery = {
export const annotationLayersView = { export const annotationLayersView = {
emptyDescription: { emptyDescription: {
description: '.ant-empty-description', description: '.ant-empty-description',
addAnnotationLayerButton: '.ant-empty-footer > .ant-btn', addAnnotationLayerButton: '.ant-empty-footer > .antd5-btn',
}, },
modal: { modal: {
content: { content: {
@ -434,7 +434,7 @@ export const dashboardListView = {
newDashboardButton: '.css-yff34v', newDashboardButton: '.css-yff34v',
}, },
importModal: { importModal: {
selectFileButton: '.ant-upload > .ant-btn > span', selectFileButton: '.ant-upload > .antd5-btn > span',
importButton: dataTestLocator('modal-confirm-button'), importButton: dataTestLocator('modal-confirm-button'),
}, },
header: { header: {
@ -588,7 +588,7 @@ export const exploreView = {
rowsContainer: dataTestLocator('table-content-rows'), rowsContainer: dataTestLocator('table-content-rows'),
}, },
confirmModal: { confirmModal: {
okButton: '.antd5-modal-confirm-btns .ant-btn-primary', okButton: '.antd5-modal-confirm-btns .antd5-btn-primary',
}, },
}, },
visualizationTypeModal: { visualizationTypeModal: {

View File

@ -37,7 +37,7 @@ const SaveDatasetActionButton = ({
const StyledDropdownButton = styled( const StyledDropdownButton = styled(
DropdownButton as FC<DropdownButtonProps>, DropdownButton as FC<DropdownButtonProps>,
)` )`
&.ant-dropdown-button button.ant-btn.ant-btn-default { &.ant-dropdown-button button.antd5-btn.antd5-btn-default {
font-weight: ${theme.gridUnit * 150}; font-weight: ${theme.gridUnit * 150};
background-color: ${theme.colors.primary.light4}; background-color: ${theme.colors.primary.light4};
color: ${theme.colors.primary.dark1}; color: ${theme.colors.primary.dark1};

View File

@ -26,10 +26,10 @@ import {
import { mix } from 'polished'; import { mix } from 'polished';
import cx from 'classnames'; import cx from 'classnames';
import { Button as AntdButton } from 'antd'; import { Button as AntdButton } from 'antd-v5';
import { useTheme } from '@superset-ui/core'; import { useTheme } from '@superset-ui/core';
import { Tooltip, TooltipProps } from 'src/components/Tooltip'; import { Tooltip, TooltipProps } from 'src/components/Tooltip';
import { ButtonProps as AntdButtonProps } from 'antd/lib/button'; import { ButtonProps as AntdButtonProps } from 'antd-v5/lib/button';
export type OnClickHandler = MouseEventHandler<HTMLElement>; export type OnClickHandler = MouseEventHandler<HTMLElement>;
@ -56,6 +56,25 @@ export type ButtonProps = Omit<AntdButtonProps, 'css'> &
showMarginRight?: boolean; showMarginRight?: boolean;
}; };
const decideType = (buttonStyle: ButtonStyle) => {
const typeMap: Record<
ButtonStyle,
'primary' | 'default' | 'dashed' | 'link'
> = {
primary: 'primary',
danger: 'primary',
warning: 'primary',
success: 'primary',
secondary: 'default',
default: 'default',
tertiary: 'dashed',
dashed: 'dashed',
link: 'link',
};
return typeMap[buttonStyle];
};
export default function Button(props: ButtonProps) { export default function Button(props: ButtonProps) {
const { const {
tooltip, tooltip,
@ -73,7 +92,7 @@ export default function Button(props: ButtonProps) {
const theme = useTheme(); const theme = useTheme();
const { colors, transitionTiming, borderRadius, typography } = theme; const { colors, transitionTiming, borderRadius, typography } = theme;
const { primary, grayscale, success, warning, error } = colors; const { primary, grayscale, success, warning } = colors;
let height = 32; let height = 32;
let padding = 18; let padding = 18;
@ -85,25 +104,19 @@ export default function Button(props: ButtonProps) {
padding = 10; padding = 10;
} }
let backgroundColor = primary.light4; let backgroundColor;
let backgroundColorHover = mix(0.1, primary.base, primary.light4); let backgroundColorHover;
let backgroundColorActive = mix(0.25, primary.base, primary.light4); let backgroundColorActive;
let backgroundColorDisabled = grayscale.light2; let backgroundColorDisabled = grayscale.light2;
let color = primary.dark1; let color;
let colorHover = color; let colorHover;
let borderWidth = 0; let borderWidth = 0;
let borderStyle = 'none'; let borderStyle = 'none';
let borderColor = 'transparent'; let borderColor;
let borderColorHover = 'transparent'; let borderColorHover;
let borderColorDisabled = 'transparent'; let borderColorDisabled = 'transparent';
if (buttonStyle === 'primary') { if (buttonStyle === 'tertiary' || buttonStyle === 'dashed') {
backgroundColor = primary.base;
backgroundColorHover = primary.dark1;
backgroundColorActive = mix(0.2, grayscale.dark2, primary.dark1);
color = grayscale.light5;
colorHover = color;
} else if (buttonStyle === 'tertiary' || buttonStyle === 'dashed') {
backgroundColor = grayscale.light5; backgroundColor = grayscale.light5;
backgroundColorHover = grayscale.light5; backgroundColorHover = grayscale.light5;
backgroundColorActive = grayscale.light5; backgroundColorActive = grayscale.light5;
@ -114,10 +127,6 @@ export default function Button(props: ButtonProps) {
borderColorHover = primary.light1; borderColorHover = primary.light1;
borderColorDisabled = grayscale.light2; borderColorDisabled = grayscale.light2;
} else if (buttonStyle === 'danger') { } else if (buttonStyle === 'danger') {
backgroundColor = error.base;
backgroundColorHover = mix(0.1, grayscale.light5, error.base);
backgroundColorActive = mix(0.2, grayscale.dark2, error.base);
color = grayscale.light5;
colorHover = color; colorHover = color;
} else if (buttonStyle === 'warning') { } else if (buttonStyle === 'warning') {
backgroundColor = warning.base; backgroundColor = warning.base;
@ -135,7 +144,7 @@ export default function Button(props: ButtonProps) {
backgroundColor = 'transparent'; backgroundColor = 'transparent';
backgroundColorHover = 'transparent'; backgroundColorHover = 'transparent';
backgroundColorActive = 'transparent'; backgroundColorActive = 'transparent';
colorHover = primary.base; color = primary.dark1;
} }
const element = children as ReactElement; const element = children as ReactElement;
@ -149,10 +158,14 @@ export default function Button(props: ButtonProps) {
const firstChildMargin = const firstChildMargin =
showMarginRight && renderedChildren.length > 1 ? theme.gridUnit * 2 : 0; showMarginRight && renderedChildren.length > 1 ? theme.gridUnit * 2 : 0;
const effectiveButtonStyle: ButtonStyle = buttonStyle ?? 'default';
const button = ( const button = (
<AntdButton <AntdButton
href={disabled ? undefined : href} href={disabled ? undefined : href}
disabled={disabled} disabled={disabled}
type={decideType(effectiveButtonStyle)}
danger={effectiveButtonStyle === 'danger'}
className={cx( className={cx(
className, className,
'superset-button', 'superset-button',
@ -179,12 +192,18 @@ export default function Button(props: ButtonProps) {
borderColor, borderColor,
borderRadius, borderRadius,
color, color,
backgroundColor, background: backgroundColor,
'&:hover': { ...(colorHover || backgroundColorHover || borderColorHover
color: colorHover, ? {
backgroundColor: backgroundColorHover, [`&.superset-button.superset-button-${buttonStyle}:hover`]: {
borderColor: borderColorHover, ...(colorHover && { color: colorHover }),
}, ...(backgroundColorHover && {
background: backgroundColorHover,
}),
...(borderColorHover && { borderColor: borderColorHover }),
},
}
: {}),
'&:active': { '&:active': {
color, color,
backgroundColor: backgroundColorActive, backgroundColor: backgroundColorActive,
@ -206,7 +225,7 @@ export default function Button(props: ButtonProps) {
'& + .superset-button': { '& + .superset-button': {
marginLeft: theme.gridUnit * 2, marginLeft: theme.gridUnit * 2,
}, },
'& > :first-of-type': { '& > span > :first-of-type': {
marginRight: firstChildMargin, marginRight: firstChildMargin,
}, },
}} }}

View File

@ -23,8 +23,8 @@ import { styled } from '@superset-ui/core';
import { kebabCase } from 'lodash'; import { kebabCase } from 'lodash';
const StyledDropdownButton = styled.div` const StyledDropdownButton = styled.div`
.ant-btn-group { .antd5-btn-group {
button.ant-btn { button.antd5-btn {
background-color: ${({ theme }) => theme.colors.primary.dark1}; background-color: ${({ theme }) => theme.colors.primary.dark1};
border-color: transparent; border-color: transparent;
color: ${({ theme }) => theme.colors.grayscale.light5}; color: ${({ theme }) => theme.colors.grayscale.light5};

View File

@ -41,10 +41,10 @@ export interface DropDownSelectableProps extends Pick<MenuProps, 'onSelect'> {
} }
const StyledDropdownButton = styled(DropdownButton as FC<DropdownButtonProps>)` const StyledDropdownButton = styled(DropdownButton as FC<DropdownButtonProps>)`
button.ant-btn:first-of-type { button.antd5-btn:first-of-type {
display: none; display: none;
} }
> button.ant-btn:nth-of-type(2) { > button.antd5-btn:nth-of-type(2) {
display: inline-flex; display: inline-flex;
background-color: transparent !important; background-color: transparent !important;
height: unset; height: unset;

View File

@ -17,8 +17,7 @@
* under the License. * under the License.
*/ */
import { styled } from '@superset-ui/core'; import { styled } from '@superset-ui/core';
import Button from 'src/components/Button'; import Button, { ButtonProps as AntdButtonProps } from 'src/components/Button';
import { ButtonProps as AntdButtonProps } from 'antd/lib/button';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
import LinesEllipsis from 'react-lines-ellipsis'; import LinesEllipsis from 'react-lines-ellipsis';

View File

@ -35,7 +35,7 @@ export const menuTriggerStyles = (theme: SupersetTheme) => css`
padding: 0; padding: 0;
border: 1px solid ${theme.colors.primary.dark2}; border: 1px solid ${theme.colors.primary.dark2};
&.ant-btn > span.anticon { &.antd5-btn > span.anticon {
line-height: 0; line-height: 0;
transition: inherit; transition: inherit;
} }

View File

@ -45,7 +45,7 @@ const AddButtonContainer = styled.div`
padding-bottom: 1px; padding-bottom: 1px;
} }
.ant-btn > .anticon + span { .antd5-btn > .anticon + span {
margin-left: 0; margin-left: 0;
} }
`} `}

View File

@ -97,7 +97,7 @@ export const MenuTrigger = styled(Button)`
padding: 0; padding: 0;
border: 1px solid ${theme.colors.primary.dark2}; border: 1px solid ${theme.colors.primary.dark2};
&.ant-btn > span.anticon { &.antd5-btn > span.anticon {
line-height: 0; line-height: 0;
transition: inherit; transition: inherit;
} }

View File

@ -624,7 +624,7 @@ describe('DatabaseModal', () => {
]; ];
visibleComponents.forEach(component => { visibleComponents.forEach(component => {
expect(component).toBeVisible(); expect(component).toBeInTheDocument();
}); });
}); });
@ -781,7 +781,7 @@ describe('DatabaseModal', () => {
enableRowExpansionCheckbox, enableRowExpansionCheckbox,
]; ];
visibleComponents.forEach(component => { visibleComponents.forEach(component => {
expect(component).toBeVisible(); expect(component).toBeInTheDocument();
}); });
invisibleComponents.forEach(component => { invisibleComponents.forEach(component => {
expect(component).not.toBeVisible(); expect(component).not.toBeVisible();
@ -849,7 +849,7 @@ describe('DatabaseModal', () => {
]; ];
visibleComponents.forEach(component => { visibleComponents.forEach(component => {
expect(component).toBeVisible(); expect(component).toBeInTheDocument();
}); });
}); });
@ -929,7 +929,7 @@ describe('DatabaseModal', () => {
// ---------- Assertions ---------- // ---------- Assertions ----------
visibleComponents.forEach(component => { visibleComponents.forEach(component => {
expect(component).toBeVisible(); expect(component).toBeInTheDocument();
}); });
invisibleComponents.forEach(component => { invisibleComponents.forEach(component => {
expect(component).not.toBeVisible(); expect(component).not.toBeVisible();
@ -1137,7 +1137,7 @@ describe('DatabaseModal', () => {
expect(await screen.findByText(/step 2 of 2/i)).toBeInTheDocument(); expect(await screen.findByText(/step 2 of 2/i)).toBeInTheDocument();
const sqlAlchemyFormStepText = screen.getByText(/step 2 of 2/i); const sqlAlchemyFormStepText = screen.getByText(/step 2 of 2/i);
expect(sqlAlchemyFormStepText).toBeVisible(); expect(sqlAlchemyFormStepText).toBeInTheDocument();
}); });
describe('SQL Alchemy form flow', () => { describe('SQL Alchemy form flow', () => {
@ -1293,7 +1293,7 @@ describe('DatabaseModal', () => {
expect(await screen.findByText(/step 2 of 2/i)).toBeInTheDocument(); expect(await screen.findByText(/step 2 of 2/i)).toBeInTheDocument();
const SSHTunnelingToggle = screen.getByTestId('ssh-tunnel-switch'); const SSHTunnelingToggle = screen.getByTestId('ssh-tunnel-switch');
expect(SSHTunnelingToggle).toBeVisible(); expect(SSHTunnelingToggle).toBeInTheDocument();
const SSHTunnelServerAddressInput = screen.queryByTestId( const SSHTunnelServerAddressInput = screen.queryByTestId(
'ssh-tunnel-server_address-input', 'ssh-tunnel-server_address-input',
); );
@ -1325,26 +1325,26 @@ describe('DatabaseModal', () => {
const SSHTunnelUsePasswordInput = screen.getByTestId( const SSHTunnelUsePasswordInput = screen.getByTestId(
'ssh-tunnel-use_password-radio', 'ssh-tunnel-use_password-radio',
); );
expect(SSHTunnelUsePasswordInput).toBeVisible(); expect(SSHTunnelUsePasswordInput).toBeInTheDocument();
const SSHTunnelUsePrivateKeyInput = screen.getByTestId( const SSHTunnelUsePrivateKeyInput = screen.getByTestId(
'ssh-tunnel-use_private_key-radio', 'ssh-tunnel-use_private_key-radio',
); );
expect(SSHTunnelUsePrivateKeyInput).toBeVisible(); expect(SSHTunnelUsePrivateKeyInput).toBeInTheDocument();
const SSHTunnelPasswordInput = screen.getByTestId( const SSHTunnelPasswordInput = screen.getByTestId(
'ssh-tunnel-password-input', 'ssh-tunnel-password-input',
); );
// By default, we use Password as login method // By default, we use Password as login method
expect(SSHTunnelPasswordInput).toBeVisible(); expect(SSHTunnelPasswordInput).toBeInTheDocument();
// Change the login method to use private key // Change the login method to use private key
userEvent.click(SSHTunnelUsePrivateKeyInput); userEvent.click(SSHTunnelUsePrivateKeyInput);
const SSHTunnelPrivateKeyInput = screen.getByTestId( const SSHTunnelPrivateKeyInput = screen.getByTestId(
'ssh-tunnel-private_key-input', 'ssh-tunnel-private_key-input',
); );
expect(SSHTunnelPrivateKeyInput).toBeVisible(); expect(SSHTunnelPrivateKeyInput).toBeInTheDocument();
const SSHTunnelPrivateKeyPasswordInput = screen.getByTestId( const SSHTunnelPrivateKeyPasswordInput = screen.getByTestId(
'ssh-tunnel-private_key_password-input', 'ssh-tunnel-private_key_password-input',
); );
expect(SSHTunnelPrivateKeyPasswordInput).toBeVisible(); expect(SSHTunnelPrivateKeyPasswordInput).toBeInTheDocument();
}); });
}); });
}); });

View File

@ -118,7 +118,7 @@ export const StyledLayoutFooter = styled.div`
`; `;
export const HeaderComponentStyles = styled.div` export const HeaderComponentStyles = styled.div`
.ant-btn { .antd5-btn {
span { span {
margin-right: 0; margin-right: 0;
} }

View File

@ -19,6 +19,7 @@
import { type ThemeConfig } from 'antd-v5'; import { type ThemeConfig } from 'antd-v5';
import { theme as supersetTheme } from 'src/preamble'; import { theme as supersetTheme } from 'src/preamble';
import { mix } from 'polished';
import { lightAlgorithm } from './light'; import { lightAlgorithm } from './light';
export enum ThemeType { export enum ThemeType {
@ -72,6 +73,45 @@ const baseConfig: ThemeConfig = {
Badge: { Badge: {
paddingXS: supersetTheme.gridUnit * 2, paddingXS: supersetTheme.gridUnit * 2,
}, },
Button: {
defaultBg: supersetTheme.colors.primary.light4,
defaultHoverBg: mix(
0.1,
supersetTheme.colors.primary.base,
supersetTheme.colors.primary.light4,
),
defaultActiveBg: mix(
0.25,
supersetTheme.colors.primary.base,
supersetTheme.colors.primary.light4,
),
defaultColor: supersetTheme.colors.primary.dark1,
defaultHoverColor: supersetTheme.colors.primary.dark1,
defaultBorderColor: 'transparent',
defaultHoverBorderColor: 'transparent',
colorPrimaryHover: supersetTheme.colors.primary.dark1,
colorPrimaryActive: mix(
0.2,
supersetTheme.colors.grayscale.dark2,
supersetTheme.colors.primary.dark1,
),
primaryColor: supersetTheme.colors.grayscale.light5,
colorPrimaryTextHover: supersetTheme.colors.grayscale.light5,
colorError: supersetTheme.colors.error.base,
colorErrorHover: mix(
0.1,
supersetTheme.colors.grayscale.light5,
supersetTheme.colors.error.base,
),
colorErrorBg: mix(
0.2,
supersetTheme.colors.grayscale.dark2,
supersetTheme.colors.error.base,
),
dangerColor: supersetTheme.colors.grayscale.light5,
colorLinkHover: supersetTheme.colors.primary.base,
linkHoverBg: 'transparent',
},
Card: { Card: {
paddingLG: supersetTheme.gridUnit * 6, paddingLG: supersetTheme.gridUnit * 6,
fontWeightStrong: supersetTheme.typography.weights.medium, fontWeightStrong: supersetTheme.typography.weights.medium,