refactor(Alert): Migrate Alert component to Ant Design V5 (#31168)

Co-authored-by: Diego Pucci <diegopucci.me@gmail.com>
This commit is contained in:
Levis Mbote 2024-12-06 22:26:04 +03:00 committed by GitHub
parent 079e7327a2
commit 79aff6827c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 127 additions and 182 deletions

View File

@ -62,7 +62,7 @@ describe('Visualization > Line', () => {
'not.exist', 'not.exist',
); );
cy.get('.ant-alert-warning').should('not.exist'); cy.get('.antd5-alert-warning').should('not.exist');
}); });
it('should allow negative values in Y bounds', () => { it('should allow negative values in Y bounds', () => {
@ -71,7 +71,7 @@ describe('Visualization > Line', () => {
cy.get('#controlSections-tab-display').click(); cy.get('#controlSections-tab-display').click();
cy.get('span').contains('Y Axis Bounds').scrollIntoView(); cy.get('span').contains('Y Axis Bounds').scrollIntoView();
cy.get('input[placeholder="Min"]').type('-0.1', { delay: 100 }); cy.get('input[placeholder="Min"]').type('-0.1', { delay: 100 });
cy.get('.ant-alert-warning').should('not.exist'); cy.get('.antd5-alert-warning').should('not.exist');
}); });
it('should allow type to search color schemes and apply the scheme', () => { it('should allow type to search color schemes and apply the scheme', () => {

View File

@ -94,7 +94,7 @@ export const databasesPage = {
dbDropdown: '[class="ant-select-selection-search-input"]', dbDropdown: '[class="ant-select-selection-search-input"]',
dbDropdownMenu: '.rc-virtual-list-holder-inner', dbDropdownMenu: '.rc-virtual-list-holder-inner',
dbDropdownMenuItem: '[class="ant-select-item-option-content"]', dbDropdownMenuItem: '[class="ant-select-item-option-content"]',
infoAlert: '.ant-alert', infoAlert: '.antd5-alert',
serviceAccountInput: '[name="credentials_info"]', serviceAccountInput: '[name="credentials_info"]',
connectionStep: { connectionStep: {
modal: '.ant-modal-content', modal: '.ant-modal-content',
@ -103,7 +103,7 @@ export const databasesPage = {
helperBottom: '.helper-bottom', helperBottom: '.helper-bottom',
postgresDatabase: '[name="database"]', postgresDatabase: '[name="database"]',
dbInput: '[name="database_name"]', dbInput: '[name="database_name"]',
alertMessage: '.ant-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="ant-btn input-upload-btn"]',
@ -166,7 +166,7 @@ export const sqlLabView = {
renderedTableHeader: '.ReactVirtualized__Table__headerRow', renderedTableHeader: '.ReactVirtualized__Table__headerRow',
renderedTableRow: '.ReactVirtualized__Table__row', renderedTableRow: '.ReactVirtualized__Table__row',
errorBody: '.error-body', errorBody: '.error-body',
alertMessage: '.ant-alert-message', alertMessage: '.antd5-alert-message',
historyTable: { historyTable: {
header: '[role=columnheader]', header: '[role=columnheader]',
table: '.QueryTable', table: '.QueryTable',
@ -325,7 +325,7 @@ export const nativeFilters = {
confirmCancelButton: dataTestLocator( confirmCancelButton: dataTestLocator(
'native-filter-modal-confirm-cancel-button', 'native-filter-modal-confirm-cancel-button',
), ),
alertXUnsavedFilters: '.ant-alert-message', alertXUnsavedFilters: '.antd5-alert-message',
tabsList: { tabsList: {
filterItemsContainer: dataTestLocator('filter-title-container'), filterItemsContainer: dataTestLocator('filter-title-container'),
tabsContainer: '[class="ant-tabs-nav-list"]', tabsContainer: '[class="ant-tabs-nav-list"]',

View File

@ -18,13 +18,12 @@
*/ */
import Alert, { AlertProps } from './index'; import Alert, { AlertProps } from './index';
type AlertType = Pick<AlertProps, 'type'>; type AlertType = Required<Pick<AlertProps, 'type'>>;
type AlertTypeValue = AlertType[keyof AlertType]; type AlertTypeValue = AlertType['type'];
const types: AlertTypeValue[] = ['info', 'error', 'warning', 'success']; const types: AlertTypeValue[] = ['info', 'error', 'warning', 'success'];
const smallText = 'Lorem ipsum dolor sit amet'; const smallText = 'Lorem ipsum dolor sit amet';
const bigText = const bigText =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' +
'Nam id porta neque, a vehicula orci. Maecenas rhoncus elit sit amet ' + 'Nam id porta neque, a vehicula orci. Maecenas rhoncus elit sit amet ' +
@ -38,40 +37,46 @@ export default {
export const AlertGallery = () => ( export const AlertGallery = () => (
<> <>
{types.map(type => ( {types.map(type => (
<div key={type} style={{ marginBottom: 40, width: 600 }}> <div key={type} style={{ marginBottom: '40px', width: '600px' }}>
<h4>{type}</h4> <h4 style={{ textTransform: 'capitalize' }}>{type} Alerts</h4>
<Alert <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
type={type} <Alert
showIcon type={type}
closable showIcon
message={bigText} message={smallText}
style={{ marginBottom: 20 }} description={bigText}
/> closable
<Alert closeIcon={
type={type} <span
showIcon aria-label="close icon"
message={smallText} style={{
description={bigText} fontSize: '12px',
closable fontWeight: 'bold',
/> }}
>
x
</span>
}
/>
</div>
</div> </div>
))} ))}
</> </>
); );
AlertGallery.parameters = {
actions: {
disable: true,
},
controls: {
disable: true,
},
};
export const InteractiveAlert = (args: AlertProps) => ( export const InteractiveAlert = (args: AlertProps) => (
<> <>
<Alert {...args} /> <Alert {...args} />
Some content to test the `roomBelow` prop <div
style={{
marginTop: args.roomBelow ? '40px' : '0px',
border: '1px dashed gray',
padding: '10px',
textAlign: 'center',
}}
>
Content below the Alert to test the `roomBelow` property
</div>
</> </>
); );
@ -79,8 +84,8 @@ InteractiveAlert.args = {
closable: true, closable: true,
roomBelow: false, roomBelow: false,
type: 'info', type: 'info',
message: smallText, message: 'This is a sample alert message.',
description: bigText, description: 'Sample description for additional context.',
showIcon: true, showIcon: true,
}; };
@ -89,5 +94,18 @@ InteractiveAlert.argTypes = {
type: { type: {
control: { type: 'select' }, control: { type: 'select' },
options: types, options: types,
description: 'Type of the alert (e.g., info, error, warning, success).',
},
closable: {
control: { type: 'boolean' },
description: 'Whether the Alert can be closed with a close button.',
},
showIcon: {
control: { type: 'boolean' },
description: 'Whether to display an icon in the Alert.',
},
roomBelow: {
control: { type: 'boolean' },
description: 'Adds margin below the Alert for layout spacing.',
}, },
}; };

View File

@ -27,45 +27,41 @@ test('renders with default props', async () => {
render(<Alert message="Message" />); render(<Alert message="Message" />);
expect(screen.getByRole('alert')).toHaveTextContent('Message'); expect(screen.getByRole('alert')).toHaveTextContent('Message');
expect(await screen.findByLabelText(`info icon`)).toBeInTheDocument(); expect(await screen.findByLabelText('info icon')).toBeInTheDocument();
expect(await screen.findByLabelText('close icon')).toBeInTheDocument(); expect(await screen.findByLabelText('close icon')).toBeInTheDocument();
}); });
test('renders each type', async () => { test('renders each type', async () => {
const types: AlertTypeValue[] = ['info', 'error', 'warning', 'success']; const types: AlertTypeValue[] = ['info', 'error', 'warning', 'success'];
for (let i = 0; i < types.length; i += 1) {
const type = types[i]; await Promise.all(
render(<Alert type={type} message="Message" />); types.map(async type => {
// eslint-disable-next-line no-await-in-loop render(<Alert type={type} message="Message" />);
expect(await screen.findByLabelText(`${type} icon`)).toBeInTheDocument(); expect(await screen.findByLabelText(`${type} icon`)).toBeInTheDocument();
} }),
);
}); });
test('renders without close button', async () => { test('renders without close button', async () => {
render(<Alert message="Message" closable={false} />); render(<Alert message="Message" closable={false} />);
await waitFor(() => { await waitFor(() => {
expect(screen.queryByLabelText('close icon')).not.toBeInTheDocument(); expect(screen.queryByLabelText('close icon')).not.toBeInTheDocument();
}); });
}); });
test('disappear when closed', () => { test('disappear when closed', async () => {
render(<Alert message="Message" />); render(<Alert message="Message" />);
userEvent.click(screen.queryByLabelText('close icon')!); userEvent.click(screen.getByLabelText('close icon'));
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
test('renders without icon', async () => {
const type = 'info';
render(<Alert type={type} message="Message" showIcon={false} />);
await waitFor(() => { await waitFor(() => {
expect(screen.queryByLabelText(`${type} icon`)).not.toBeInTheDocument(); expect(screen.queryByRole('alert')).not.toBeInTheDocument();
}); });
}); });
test('renders message', async () => { test('renders without icon', async () => {
render(<Alert message="Message" />); render(<Alert type="info" message="Message" showIcon={false} />);
expect(await screen.findByRole('alert')).toHaveTextContent('Message'); await waitFor(() => {
expect(screen.queryByLabelText('info icon')).not.toBeInTheDocument();
});
}); });
test('renders message and description', async () => { test('renders message and description', async () => {
@ -74,3 +70,10 @@ test('renders message and description', async () => {
expect(alert).toHaveTextContent('Message'); expect(alert).toHaveTextContent('Message');
expect(alert).toHaveTextContent('Description'); expect(alert).toHaveTextContent('Description');
}); });
test('calls onClose callback when closed', () => {
const onCloseMock = jest.fn();
render(<Alert message="Message" onClose={onCloseMock} />);
userEvent.click(screen.getByLabelText('close icon'));
expect(onCloseMock).toHaveBeenCalledTimes(1);
});

View File

@ -17,12 +17,13 @@
* under the License. * under the License.
*/ */
import { PropsWithChildren } from 'react'; import { PropsWithChildren } from 'react';
import AntdAlert, { AlertProps as AntdAlertProps } from 'antd/lib/alert'; import { Alert as AntdAlert } from 'antd-v5';
import { useTheme } from '@superset-ui/core'; import { AlertProps as AntdAlertProps } from 'antd-v5/lib/alert';
import { css, useTheme } from '@superset-ui/core';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
export type AlertProps = PropsWithChildren< export type AlertProps = PropsWithChildren<
AntdAlertProps & { roomBelow?: boolean } Omit<AntdAlertProps, 'children'> & { roomBelow?: boolean }
>; >;
export default function Alert(props: AlertProps) { export default function Alert(props: AlertProps) {
@ -36,8 +37,8 @@ export default function Alert(props: AlertProps) {
} = props; } = props;
const theme = useTheme(); const theme = useTheme();
const { colors, typography, gridUnit } = theme; const { colors } = theme;
const { alert, error, info, success } = colors; const { alert: alertColor, error, info, success } = colors;
let baseColor = info; let baseColor = info;
let AlertIcon = Icons.InfoSolid; let AlertIcon = Icons.InfoSolid;
@ -45,7 +46,7 @@ export default function Alert(props: AlertProps) {
baseColor = error; baseColor = error;
AlertIcon = Icons.ErrorSolid; AlertIcon = Icons.ErrorSolid;
} else if (type === 'warning') { } else if (type === 'warning') {
baseColor = alert; baseColor = alertColor;
AlertIcon = Icons.AlertSolid; AlertIcon = Icons.AlertSolid;
} else if (type === 'success') { } else if (type === 'success') {
baseColor = success; baseColor = success;
@ -55,33 +56,36 @@ export default function Alert(props: AlertProps) {
return ( return (
<AntdAlert <AntdAlert
role="alert" role="alert"
aria-live={type === 'error' ? 'assertive' : 'polite'}
showIcon={showIcon} showIcon={showIcon}
icon={<AlertIcon aria-label={`${type} icon`} />} icon={
closeText={closable && <Icons.XSmall aria-label="close icon" />} showIcon && (
css={{ <span
marginBottom: roomBelow ? gridUnit * 4 : 0, role="img"
padding: `${gridUnit * 2}px ${gridUnit * 3}px`, aria-label={`${type} icon`}
alignItems: 'flex-start', style={{
border: 0, color: baseColor.base,
backgroundColor: baseColor.light2, }}
'& .ant-alert-icon': { >
marginRight: gridUnit * 2, <AlertIcon />
}, </span>
'& .ant-alert-message': { )
color: baseColor.dark2, }
fontSize: typography.sizes.m, closeIcon={closable && <Icons.XSmall aria-label="close icon" />}
fontWeight: description message={children || 'Default message'}
? typography.weights.bold description={description}
: typography.weights.normal, css={css`
}, margin-bottom: ${roomBelow ? theme.gridUnit * 4 : 0}px;
'& .ant-alert-description': { a {
color: baseColor.dark2, text-decoration: underline;
fontSize: typography.sizes.m, }
}, .antd5-alert-message {
}} font-weight: ${description
? theme.typography.weights.bold
: 'inherit'};
}
`}
{...props} {...props}
> />
{children}
</AntdAlert>
); );
} }

View File

@ -20,24 +20,9 @@
import { css, SupersetTheme } from '@superset-ui/core'; import { css, SupersetTheme } from '@superset-ui/core';
export const antdWarningAlertStyles = (theme: SupersetTheme) => css` export const antdWarningAlertStyles = (theme: SupersetTheme) => css`
border: 1px solid ${theme.colors.warning.light1};
padding: ${theme.gridUnit * 4}px;
margin: ${theme.gridUnit * 4}px 0; margin: ${theme.gridUnit * 4}px 0;
color: ${theme.colors.warning.dark2};
.ant-alert-message { .antd5-alert-message {
margin: 0; margin: 0;
} }
.ant-alert-description {
font-size: ${theme.typography.sizes.s + 1}px;
line-height: ${theme.gridUnit * 4}px;
.ant-alert-icon {
margin-right: ${theme.gridUnit * 2.5}px;
font-size: ${theme.typography.sizes.l + 1}px;
position: relative;
top: ${theme.gridUnit / 4}px;
}
}
`; `;

View File

@ -43,7 +43,7 @@ export function CancelConfirmationAlert({
css={{ css={{
textAlign: 'left', textAlign: 'left',
flex: 1, flex: 1,
'& .ant-alert-action': { alignSelf: 'center' }, '& .antd5-alert-action': { alignSelf: 'center' },
}} }}
description={children} description={children}
action={ action={

View File

@ -166,82 +166,25 @@ export const antDModalStyles = (theme: SupersetTheme) => css`
`; `;
export const antDAlertStyles = (theme: SupersetTheme) => css` export const antDAlertStyles = (theme: SupersetTheme) => css`
border: 1px solid ${theme.colors.info.base};
padding: ${theme.gridUnit * 4}px;
margin: ${theme.gridUnit * 4}px 0; margin: ${theme.gridUnit * 4}px 0;
.ant-alert-message {
color: ${theme.colors.info.dark2};
font-size: ${theme.typography.sizes.m}px;
font-weight: ${theme.typography.weights.bold};
}
.ant-alert-description {
color: ${theme.colors.info.dark2};
font-size: ${theme.typography.sizes.m}px;
line-height: ${theme.gridUnit * 5}px;
a {
text-decoration: underline;
}
.ant-alert-icon {
margin-right: ${theme.gridUnit * 2.5}px;
font-size: ${theme.typography.sizes.l}px;
position: relative;
top: ${theme.gridUnit / 4}px;
}
}
`; `;
export const StyledAlertMargin = styled.div` export const StyledAlertMargin = styled.div`
${({ theme }) => css` ${({ theme }) => css`
margin: 0 ${theme.gridUnit * 4}px -${theme.gridUnit * 4}px; margin: 0 ${theme.gridUnit * 4}px ${theme.gridUnit * 4}px;
`} `}
`; `;
export const antDErrorAlertStyles = (theme: SupersetTheme) => css` export const antDErrorAlertStyles = (theme: SupersetTheme) => css`
border: ${theme.colors.error.base} 1px solid;
padding: ${theme.gridUnit * 4}px;
margin: ${theme.gridUnit * 8}px ${theme.gridUnit * 4}px; margin: ${theme.gridUnit * 8}px ${theme.gridUnit * 4}px;
color: ${theme.colors.error.dark2};
.ant-alert-message {
font-size: ${theme.typography.sizes.m}px;
font-weight: ${theme.typography.weights.bold};
}
.ant-alert-description {
font-size: ${theme.typography.sizes.m}px;
line-height: ${theme.gridUnit * 5}px;
.ant-alert-icon {
margin-right: ${theme.gridUnit * 2.5}px;
font-size: ${theme.typography.sizes.l}px;
position: relative;
top: ${theme.gridUnit / 4}px;
}
}
`; `;
export const antdWarningAlertStyles = (theme: SupersetTheme) => css` export const antdWarningAlertStyles = (theme: SupersetTheme) => css`
border: 1px solid ${theme.colors.warning.light1};
padding: ${theme.gridUnit * 4}px;
margin: ${theme.gridUnit * 4}px 0; margin: ${theme.gridUnit * 4}px 0;
color: ${theme.colors.warning.dark2};
.ant-alert-message { .antd5-alert-message {
margin: 0; margin: 0;
} }
.ant-alert-description {
font-size: ${theme.typography.sizes.s + 1}px;
line-height: ${theme.gridUnit * 4}px;
.ant-alert-icon {
margin-right: ${theme.gridUnit * 2.5}px;
font-size: ${theme.typography.sizes.l + 1}px;
position: relative;
top: ${theme.gridUnit / 4}px;
}
}
`; `;
export const formHelperStyles = (theme: SupersetTheme) => css` export const formHelperStyles = (theme: SupersetTheme) => css`

View File

@ -113,23 +113,6 @@ export const StyledRadioGroup = styled(Radio.Group)`
`; `;
export const antDErrorAlertStyles = (theme: SupersetTheme) => css` export const antDErrorAlertStyles = (theme: SupersetTheme) => css`
border: ${theme.colors.error.base} 1px solid;
padding: ${theme.gridUnit * 4}px;
margin: ${theme.gridUnit * 4}px; margin: ${theme.gridUnit * 4}px;
margin-top: 0; margin-top: 0;
color: ${theme.colors.error.dark2};
.ant-alert-message {
font-size: ${theme.typography.sizes.m}px;
font-weight: bold;
}
.ant-alert-description {
font-size: ${theme.typography.sizes.m}px;
line-height: ${theme.gridUnit * 4}px;
.ant-alert-icon {
margin-right: ${theme.gridUnit * 2.5}px;
font-size: ${theme.typography.sizes.l}px;
position: relative;
top: ${theme.gridUnit / 4}px;
}
}
`; `;

View File

@ -55,6 +55,15 @@ const baseConfig: ThemeConfig = {
zIndexPopupBase: supersetTheme.zIndex.max, zIndexPopupBase: supersetTheme.zIndex.max,
}, },
components: { components: {
Alert: {
borderRadius: supersetTheme.borderRadius,
colorBgContainer: supersetTheme.colors.grayscale.light5,
colorBorder: supersetTheme.colors.grayscale.light3,
fontSize: supersetTheme.typography.sizes.m,
fontSizeLG: supersetTheme.typography.sizes.m,
fontSizeIcon: supersetTheme.typography.sizes.l,
colorText: supersetTheme.colors.grayscale.dark1,
},
Avatar: { Avatar: {
containerSize: 32, containerSize: 32,
fontSize: supersetTheme.typography.sizes.s, fontSize: supersetTheme.typography.sizes.s,