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',
);
cy.get('.ant-alert-warning').should('not.exist');
cy.get('.antd5-alert-warning').should('not.exist');
});
it('should allow negative values in Y bounds', () => {
@ -71,7 +71,7 @@ describe('Visualization > Line', () => {
cy.get('#controlSections-tab-display').click();
cy.get('span').contains('Y Axis Bounds').scrollIntoView();
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', () => {

View File

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

View File

@ -18,13 +18,12 @@
*/
import Alert, { AlertProps } from './index';
type AlertType = Pick<AlertProps, 'type'>;
type AlertTypeValue = AlertType[keyof AlertType];
type AlertType = Required<Pick<AlertProps, 'type'>>;
type AlertTypeValue = AlertType['type'];
const types: AlertTypeValue[] = ['info', 'error', 'warning', 'success'];
const smallText = 'Lorem ipsum dolor sit amet';
const bigText =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' +
'Nam id porta neque, a vehicula orci. Maecenas rhoncus elit sit amet ' +
@ -38,40 +37,46 @@ export default {
export const AlertGallery = () => (
<>
{types.map(type => (
<div key={type} style={{ marginBottom: 40, width: 600 }}>
<h4>{type}</h4>
<Alert
type={type}
showIcon
closable
message={bigText}
style={{ marginBottom: 20 }}
/>
<Alert
type={type}
showIcon
message={smallText}
description={bigText}
closable
/>
<div key={type} style={{ marginBottom: '40px', width: '600px' }}>
<h4 style={{ textTransform: 'capitalize' }}>{type} Alerts</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<Alert
type={type}
showIcon
message={smallText}
description={bigText}
closable
closeIcon={
<span
aria-label="close icon"
style={{
fontSize: '12px',
fontWeight: 'bold',
}}
>
x
</span>
}
/>
</div>
</div>
))}
</>
);
AlertGallery.parameters = {
actions: {
disable: true,
},
controls: {
disable: true,
},
};
export const InteractiveAlert = (args: AlertProps) => (
<>
<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,
roomBelow: false,
type: 'info',
message: smallText,
description: bigText,
message: 'This is a sample alert message.',
description: 'Sample description for additional context.',
showIcon: true,
};
@ -89,5 +94,18 @@ InteractiveAlert.argTypes = {
type: {
control: { type: 'select' },
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" />);
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();
});
test('renders each type', async () => {
const types: AlertTypeValue[] = ['info', 'error', 'warning', 'success'];
for (let i = 0; i < types.length; i += 1) {
const type = types[i];
render(<Alert type={type} message="Message" />);
// eslint-disable-next-line no-await-in-loop
expect(await screen.findByLabelText(`${type} icon`)).toBeInTheDocument();
}
await Promise.all(
types.map(async type => {
render(<Alert type={type} message="Message" />);
expect(await screen.findByLabelText(`${type} icon`)).toBeInTheDocument();
}),
);
});
test('renders without close button', async () => {
render(<Alert message="Message" closable={false} />);
await waitFor(() => {
expect(screen.queryByLabelText('close icon')).not.toBeInTheDocument();
});
});
test('disappear when closed', () => {
test('disappear when closed', async () => {
render(<Alert message="Message" />);
userEvent.click(screen.queryByLabelText('close icon')!);
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
test('renders without icon', async () => {
const type = 'info';
render(<Alert type={type} message="Message" showIcon={false} />);
userEvent.click(screen.getByLabelText('close icon'));
await waitFor(() => {
expect(screen.queryByLabelText(`${type} icon`)).not.toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
});
test('renders message', async () => {
render(<Alert message="Message" />);
expect(await screen.findByRole('alert')).toHaveTextContent('Message');
test('renders without icon', async () => {
render(<Alert type="info" message="Message" showIcon={false} />);
await waitFor(() => {
expect(screen.queryByLabelText('info icon')).not.toBeInTheDocument();
});
});
test('renders message and description', async () => {
@ -74,3 +70,10 @@ test('renders message and description', async () => {
expect(alert).toHaveTextContent('Message');
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.
*/
import { PropsWithChildren } from 'react';
import AntdAlert, { AlertProps as AntdAlertProps } from 'antd/lib/alert';
import { useTheme } from '@superset-ui/core';
import { Alert as AntdAlert } from 'antd-v5';
import { AlertProps as AntdAlertProps } from 'antd-v5/lib/alert';
import { css, useTheme } from '@superset-ui/core';
import Icons from 'src/components/Icons';
export type AlertProps = PropsWithChildren<
AntdAlertProps & { roomBelow?: boolean }
Omit<AntdAlertProps, 'children'> & { roomBelow?: boolean }
>;
export default function Alert(props: AlertProps) {
@ -36,8 +37,8 @@ export default function Alert(props: AlertProps) {
} = props;
const theme = useTheme();
const { colors, typography, gridUnit } = theme;
const { alert, error, info, success } = colors;
const { colors } = theme;
const { alert: alertColor, error, info, success } = colors;
let baseColor = info;
let AlertIcon = Icons.InfoSolid;
@ -45,7 +46,7 @@ export default function Alert(props: AlertProps) {
baseColor = error;
AlertIcon = Icons.ErrorSolid;
} else if (type === 'warning') {
baseColor = alert;
baseColor = alertColor;
AlertIcon = Icons.AlertSolid;
} else if (type === 'success') {
baseColor = success;
@ -55,33 +56,36 @@ export default function Alert(props: AlertProps) {
return (
<AntdAlert
role="alert"
aria-live={type === 'error' ? 'assertive' : 'polite'}
showIcon={showIcon}
icon={<AlertIcon aria-label={`${type} icon`} />}
closeText={closable && <Icons.XSmall aria-label="close icon" />}
css={{
marginBottom: roomBelow ? gridUnit * 4 : 0,
padding: `${gridUnit * 2}px ${gridUnit * 3}px`,
alignItems: 'flex-start',
border: 0,
backgroundColor: baseColor.light2,
'& .ant-alert-icon': {
marginRight: gridUnit * 2,
},
'& .ant-alert-message': {
color: baseColor.dark2,
fontSize: typography.sizes.m,
fontWeight: description
? typography.weights.bold
: typography.weights.normal,
},
'& .ant-alert-description': {
color: baseColor.dark2,
fontSize: typography.sizes.m,
},
}}
icon={
showIcon && (
<span
role="img"
aria-label={`${type} icon`}
style={{
color: baseColor.base,
}}
>
<AlertIcon />
</span>
)
}
closeIcon={closable && <Icons.XSmall aria-label="close icon" />}
message={children || 'Default message'}
description={description}
css={css`
margin-bottom: ${roomBelow ? theme.gridUnit * 4 : 0}px;
a {
text-decoration: underline;
}
.antd5-alert-message {
font-weight: ${description
? theme.typography.weights.bold
: 'inherit'};
}
`}
{...props}
>
{children}
</AntdAlert>
/>
);
}

View File

@ -20,24 +20,9 @@
import { css, SupersetTheme } from '@superset-ui/core';
export const antdWarningAlertStyles = (theme: SupersetTheme) => css`
border: 1px solid ${theme.colors.warning.light1};
padding: ${theme.gridUnit * 4}px;
margin: ${theme.gridUnit * 4}px 0;
color: ${theme.colors.warning.dark2};
.ant-alert-message {
.antd5-alert-message {
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={{
textAlign: 'left',
flex: 1,
'& .ant-alert-action': { alignSelf: 'center' },
'& .antd5-alert-action': { alignSelf: 'center' },
}}
description={children}
action={

View File

@ -166,82 +166,25 @@ export const antDModalStyles = (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;
.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`
${({ 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`
border: ${theme.colors.error.base} 1px solid;
padding: ${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`
border: 1px solid ${theme.colors.warning.light1};
padding: ${theme.gridUnit * 4}px;
margin: ${theme.gridUnit * 4}px 0;
color: ${theme.colors.warning.dark2};
.ant-alert-message {
.antd5-alert-message {
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`

View File

@ -113,23 +113,6 @@ export const StyledRadioGroup = styled(Radio.Group)`
`;
export const antDErrorAlertStyles = (theme: SupersetTheme) => css`
border: ${theme.colors.error.base} 1px solid;
padding: ${theme.gridUnit * 4}px;
margin: ${theme.gridUnit * 4}px;
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,
},
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: {
containerSize: 32,
fontSize: supersetTheme.typography.sizes.s,