123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- import {useRef, useState} from 'react';
- import {css} from '@emotion/react';
- import styled from '@emotion/styled';
- import {useHover} from '@react-aria/interactions';
- import classNames from 'classnames';
- import {IconCheckmark, IconChevron, IconInfo, IconNot, IconWarning} from 'sentry/icons';
- import space from 'sentry/styles/space';
- import {defined} from 'sentry/utils';
- import {Theme} from 'sentry/utils/theme';
- export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
- expand?: React.ReactNode;
- icon?: React.ReactNode;
- opaque?: boolean;
- showIcon?: boolean;
- system?: boolean;
- trailingItems?: React.ReactNode;
- type?: keyof Theme['alert'];
- }
- const DEFAULT_TYPE = 'info';
- function Alert({
- type = DEFAULT_TYPE,
- showIcon = false,
- icon,
- opaque,
- system,
- expand,
- trailingItems,
- className,
- children,
- ...props
- }: AlertProps) {
- const [isExpanded, setIsExpanded] = useState(false);
- const showExpand = defined(expand);
- const showTrailingItems = defined(trailingItems);
- // Show the hover state (with darker borders) only when hovering over the
- // IconWrapper or MessageContainer.
- const {hoverProps, isHovered} = useHover({
- isDisabled: !showExpand,
- });
- const {hoverProps: expandHoverProps, isHovered: expandIsHovered} = useHover({
- isDisabled: !showExpand,
- });
- function getIcon() {
- switch (type) {
- case 'warning':
- return <IconWarning />;
- case 'success':
- return <IconCheckmark />;
- case 'error':
- return <IconNot />;
- case 'info':
- default:
- return <IconInfo />;
- }
- }
- const expandRef = useRef<HTMLDivElement>(null);
- function handleClick(e: React.MouseEvent<HTMLDivElement>) {
- if (
- // Only close the alert when the click event originated from outside the expanded
- // content.
- e.target === expandRef.current ||
- expandRef.current?.contains(e.target as HTMLDivElement)
- ) {
- return;
- }
- showExpand && setIsExpanded(!isExpanded);
- }
- return (
- <Wrap
- type={type}
- system={system}
- opaque={opaque}
- expand={expand}
- trailingItems={trailingItems}
- showIcon={showIcon}
- onClick={handleClick}
- hovered={isHovered && !expandIsHovered}
- className={classNames(type ? `ref-${type}` : '', className)}
- {...hoverProps}
- {...props}
- >
- {showIcon && <IconWrapper onClick={handleClick}>{icon ?? getIcon()}</IconWrapper>}
- <Message>{children}</Message>
- {showTrailingItems && (
- <TrailingItems showIcon={showIcon} onClick={e => e.stopPropagation()}>
- {trailingItems}
- </TrailingItems>
- )}
- {showExpand && (
- <ExpandIconWrap>
- <IconChevron direction={isExpanded ? 'up' : 'down'} />
- </ExpandIconWrap>
- )}
- {isExpanded && (
- <ExpandContainer
- ref={expandRef}
- showIcon={showIcon}
- showTrailingItems={showTrailingItems}
- {...expandHoverProps}
- >
- {Array.isArray(expand) ? expand.map(item => item) : expand}
- </ExpandContainer>
- )}
- </Wrap>
- );
- }
- const alertStyles = ({
- type = DEFAULT_TYPE,
- system,
- opaque,
- expand,
- showIcon,
- trailingItems,
- hovered,
- theme,
- }: AlertProps & {theme: Theme; hovered?: boolean}) => {
- const alertColors = theme.alert[type];
- const showExpand = defined(expand);
- const showTrailingItems = defined(trailingItems);
- return css`
- display: grid;
- grid-template-columns:
- ${showIcon && `minmax(0, max-content)`}
- minmax(0, 1fr)
- ${showTrailingItems && 'max-content'}
- ${showExpand && 'max-content'};
- gap: ${space(1)};
- margin: 0 0 ${space(2)};
- font-size: ${theme.fontSizeMedium};
- border-radius: ${theme.borderRadius};
- border: 1px solid ${alertColors.border};
- background: ${opaque
- ? `linear-gradient(
- ${alertColors.backgroundLight},
- ${alertColors.backgroundLight}),
- linear-gradient(${theme.background}, ${theme.background}
- )`
- : `${alertColors.backgroundLight}`};
- a:not([role='button']) {
- color: ${theme.textColor};
- text-decoration-color: ${theme.translucentBorder};
- text-decoration-style: solid;
- text-decoration-line: underline;
- text-decoration-thickness: 0.08em;
- text-underline-offset: 0.06em;
- }
- a:not([role='button']):hover {
- text-decoration-color: ${theme.subText};
- text-decoration-style: solid;
- }
- pre {
- background: ${alertColors.backgroundLight};
- margin: ${space(0.5)} 0 0;
- }
- ${IconWrapper}, ${ExpandIconWrap} {
- color: ${alertColors.iconColor};
- }
- ${hovered &&
- `
- border-color: ${alertColors.borderHover};
- ${IconWrapper}, ${IconChevron} {
- color: ${alertColors.iconHoverColor};
- }
- `}
- ${showExpand &&
- `cursor: pointer;
- ${TrailingItems} {
- cursor: auto;
- }
- `}
- ${system &&
- `
- border-width: 0 0 1px 0;
- border-radius: 0;
- `}
- `;
- };
- const Wrap = styled('div')<AlertProps & {hovered: boolean}>`
- ${alertStyles}
- padding: ${space(1.5)} ${space(2)};
- `;
- const IconWrapper = styled('div')`
- display: flex;
- align-items: center;
- height: calc(${p => p.theme.fontSizeMedium} * ${p => p.theme.text.lineHeightBody});
- `;
- const Message = styled('span')`
- position: relative;
- line-height: ${p => p.theme.text.lineHeightBody};
- `;
- const TrailingItems = styled('div')<{showIcon: boolean}>`
- height: calc(${p => p.theme.fontSizeMedium} * ${p => p.theme.text.lineHeightBody});
- display: grid;
- grid-auto-flow: column;
- grid-template-rows: 100%;
- align-items: center;
- gap: ${space(1)};
- @media (max-width: ${p => p.theme.breakpoints.small}) {
- /* In mobile, TrailingItems should wrap to a second row and be vertically aligned
- with Message. When there is a leading icon, Message is in the second grid column.
- Otherwise it's in the first grid column. */
- grid-row: 2;
- grid-column: ${p => (p.showIcon ? 2 : 1)} / -1;
- justify-items: start;
- margin: ${space(0.5)} 0;
- }
- `;
- const ExpandIconWrap = styled(IconWrapper)`
- margin-left: ${space(0.5)};
- `;
- const ExpandContainer = styled('div')<{showIcon: boolean; showTrailingItems: boolean}>`
- grid-row: 2;
- /* ExpandContainer should be vertically aligned with Message. When there is a leading icon,
- Message is in the second grid column. Otherwise it's in the first column. */
- grid-column: ${p => (p.showIcon ? 2 : 1)} / -1;
- cursor: auto;
- @media (max-width: ${p => p.theme.breakpoints.small}) {
- grid-row: ${p => (p.showTrailingItems ? 3 : 2)};
- }
- `;
- export {alertStyles};
- export default Alert;
|