chore: refactor Alert-related components (#31858)

This commit is contained in:
Maxime Beauchemin
2025-01-24 08:39:09 -08:00
committed by GitHub
parent 5fe6ef268e
commit 6eb87e04c0
51 changed files with 590 additions and 753 deletions

View File

@@ -16,250 +16,129 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useState, ReactNode } from 'react';
import {
ErrorLevel,
ErrorSource,
styled,
useTheme,
t,
} from '@superset-ui/core';
import { noOp } from 'src/utils/common';
import { useState } from 'react';
import { Tooltip } from 'src/components/Tooltip';
import Modal from 'src/components/Modal';
import Button from 'src/components/Button';
import { isCurrentUserBot } from 'src/utils/isBot';
import { ExclamationCircleOutlined, WarningOutlined } from '@ant-design/icons';
import Alert from 'src/components/Alert';
import { t, useTheme } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import CopyToClipboard from '../CopyToClipboard';
const ErrorAlertDiv = styled.div<{ level: ErrorLevel }>`
align-items: center;
background-color: ${({ level, theme }) => theme.colors[level].light2};
border-radius: ${({ theme }) => theme.borderRadius}px;
border: 1px solid ${({ level, theme }) => theme.colors[level].base};
color: ${({ level, theme }) => theme.colors[level].dark2};
padding: ${({ theme }) => 2 * theme.gridUnit}px;
width: 100%;
.top-row {
display: flex;
justify-content: space-between;
}
.error-body {
padding-top: ${({ theme }) => theme.gridUnit}px;
padding-left: ${({ theme }) => 8 * theme.gridUnit}px;
}
.icon {
margin-right: ${({ theme }) => 2 * theme.gridUnit}px;
}
.link {
color: ${({ level, theme }) => theme.colors[level].dark2};
text-decoration: underline;
&:focus-visible {
border: 1px solid ${({ theme }) => theme.colors.primary.base};
padding: ${({ theme }) => theme.gridUnit / 2}px;
margin: -${({ theme }) => theme.gridUnit / 2 + 1}px;
border-radius: ${({ theme }) => theme.borderRadius}px;
}
`;
const ErrorModal = styled(Modal)<{ level: ErrorLevel }>`
color: ${({ level, theme }) => theme.colors[level].dark2};
overflow-wrap: break-word;
.antd5-modal-header {
background-color: ${({ level, theme }) => theme.colors[level].light2};
padding: ${({ theme }) => 4 * theme.gridUnit}px;
}
.icon {
margin-right: ${({ theme }) => 2 * theme.gridUnit}px;
}
.header {
display: flex;
align-items: center;
font-size: ${({ theme }) => theme.typography.sizes.l}px;
}
`;
const LeftSideContent = styled.div`
align-items: center;
display: flex;
`;
interface ErrorAlertProps {
body: ReactNode;
copyText?: string;
level: ErrorLevel;
source?: ErrorSource;
subtitle: ReactNode;
title: ReactNode;
description?: string;
export interface ErrorAlertProps {
errorType?: string; // Strong text on the first line
message: React.ReactNode | string; // Text shown on the first line
type?: 'warning' | 'error' | 'info'; // Allows only 'warning' or 'error'
description?: React.ReactNode; // Text shown under the first line, not collapsible
descriptionDetails?: React.ReactNode | string; // Text shown under the first line, collapsible
descriptionDetailsCollapsed?: boolean; // Hides the collapsible section unless "Show more" is clicked, default true
descriptionPre?: boolean; // Uses pre-style to break lines, default true
compact?: boolean; // Shows the error icon with tooltip and modal, default false
children?: React.ReactNode; // Additional content to show in the modal
closable?: boolean; // Show close button, default true
showIcon?: boolean; // Show icon, default true
}
export default function ErrorAlert({
body,
copyText,
level = 'error',
source = 'dashboard',
subtitle,
title,
const ErrorAlert: React.FC<ErrorAlertProps> = ({
errorType = t('Error'),
message,
type = 'error',
description,
}: ErrorAlertProps) {
descriptionDetails,
descriptionDetailsCollapsed = true,
descriptionPre = true,
compact = false,
children,
closable = true,
showIcon = true,
}) => {
const [isDescriptionVisible, setIsDescriptionVisible] = useState(
!descriptionDetailsCollapsed,
);
const [showModal, setShowModal] = useState(false);
const toggleDescription = () => {
setIsDescriptionVisible(!isDescriptionVisible);
};
const theme = useTheme();
const [isModalOpen, setIsModalOpen] = useState(false);
const [isBodyExpanded, setIsBodyExpanded] = useState(isCurrentUserBot());
const isExpandable =
isCurrentUserBot() || ['explore', 'sqllab'].includes(source);
const iconColor = theme.colors[level].base;
return (
<ErrorAlertDiv level={level} role="alert">
<div className="top-row">
<LeftSideContent>
{level === 'error' ? (
<Icons.ErrorSolid className="icon" iconColor={iconColor} />
) : (
<Icons.WarningSolid className="icon" iconColor={iconColor} />
const renderTrigger = () => {
const icon =
type === 'warning' ? <WarningOutlined /> : <ExclamationCircleOutlined />;
const color =
type === 'warning' ? theme.colors.warning.base : theme.colors.error.base;
return (
<div style={{ cursor: 'pointer' }}>
<span style={{ color }}>{icon} </span>
{errorType}
</div>
);
};
const preStyle = {
whiteSpace: 'pre-wrap',
fontFamily: theme.typography.families.sansSerif,
};
const renderDescription = () => (
<div>
{description && (
<p style={descriptionPre ? preStyle : {}} data-testid="description">
{description}
</p>
)}
{descriptionDetails && (
<div>
{isDescriptionVisible && (
<p style={descriptionPre ? preStyle : {}}>{descriptionDetails}</p>
)}
<strong>{title}</strong>
</LeftSideContent>
{!isExpandable && !description && (
<span
role="button"
tabIndex={0}
className="link"
onClick={() => setIsModalOpen(true)}
onKeyDown={event => {
if (event.key === 'Enter') {
setIsModalOpen(true);
}
}}
onClick={toggleDescription}
style={{ textDecoration: 'underline', cursor: 'pointer' }}
>
{t('See more')}
{isDescriptionVisible ? t('See less') : t('See more')}
</span>
)}
</div>
{description && (
<div className="error-body">
<p>{description}</p>
{!isExpandable && (
<span
role="button"
tabIndex={0}
className="link"
onClick={() => setIsModalOpen(true)}
onKeyDown={event => {
if (event.key === 'Enter') {
setIsModalOpen(true);
}
}}
>
{t('See more')}
</span>
)}
</div>
)}
{isExpandable ? (
<div className="error-body">
<p>{subtitle}</p>
{body && (
<>
{!isBodyExpanded && (
<span
role="button"
tabIndex={0}
className="link"
onClick={() => setIsBodyExpanded(true)}
onKeyDown={event => {
if (event.key === 'Enter') {
setIsBodyExpanded(true);
}
}}
>
{t('See more')}
</span>
)}
{isBodyExpanded && (
<>
<br />
{body}
<span
role="button"
tabIndex={0}
className="link"
onClick={() => setIsBodyExpanded(false)}
onKeyDown={event => {
if (event.key === 'Enter') {
setIsBodyExpanded(false);
}
}}
>
{t('See less')}
</span>
</>
)}
</>
)}
</div>
) : (
<ErrorModal
level={level}
show={isModalOpen}
onHide={() => setIsModalOpen(false)}
destroyOnClose
title={
<div className="header">
{level === 'error' ? (
<Icons.ErrorSolid className="icon" iconColor={iconColor} />
) : (
<Icons.WarningSolid className="icon" iconColor={iconColor} />
)}
<div className="title">{title}</div>
</div>
}
footer={
<>
{copyText && (
<CopyToClipboard
text={copyText}
shouldShowText={false}
wrapped={false}
copyNode={<Button onClick={noOp}>{t('Copy message')}</Button>}
/>
)}
<Button
cta
buttonStyle="primary"
onClick={() => setIsModalOpen(false)}
tabIndex={0}
onKeyDown={event => {
if (event.key === 'Enter') {
setIsModalOpen(false);
}
}}
>
{t('Close')}
</Button>
</>
}
>
<>
<p>{subtitle}</p>
{/* This break was in the original design of the modal but
the spacing looks really off if there is only
subtitle or a body */}
{subtitle && body && <br />}
{body}
</>
</ErrorModal>
)}
</ErrorAlertDiv>
</div>
);
}
const renderAlert = (closable: boolean) => (
<Alert
description={renderDescription()}
type={type}
showIcon
closable={closable}
>
<strong>{errorType}</strong>
{message && (
<>
: <span>{message}</span>
</>
)}
</Alert>
);
if (compact) {
return (
<>
<Tooltip title={`${errorType}: ${message}`}>
<span role="button" onClick={() => setShowModal(true)} tabIndex={0}>
{renderTrigger()}
</span>
</Tooltip>
<Modal
title={errorType}
show={showModal}
onHide={() => setShowModal(false)}
footer={null}
>
{renderAlert(false)}
{children}
</Modal>
</>
);
}
return renderAlert(closable);
};
export default ErrorAlert;