alert.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import {useState} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import classNames from 'classnames';
  5. import {IconChevron} from 'sentry/icons';
  6. import space from 'sentry/styles/space';
  7. import {Theme} from 'sentry/utils/theme';
  8. type Props = {
  9. expand?: React.ReactNode[];
  10. expandIcon?: React.ReactNode;
  11. icon?: React.ReactNode;
  12. onExpandIconClick?: () => void;
  13. opaque?: boolean;
  14. system?: boolean;
  15. type?: keyof Theme['alert'];
  16. };
  17. type AlertProps = Omit<React.HTMLProps<HTMLDivElement>, keyof Props> & Props;
  18. const DEFAULT_TYPE = 'info';
  19. const IconWrapper = styled('span')`
  20. display: flex;
  21. margin-right: ${space(1)};
  22. /* Give the wrapper an explicit height so icons are line height with the
  23. * (common) line height. */
  24. height: 22px;
  25. align-items: center;
  26. `;
  27. const alertStyles = ({
  28. theme,
  29. type = DEFAULT_TYPE,
  30. system,
  31. opaque,
  32. }: Props & {theme: Theme}) => {
  33. const alertColors = theme.alert[type] ?? theme.alert[DEFAULT_TYPE];
  34. return css`
  35. display: flex;
  36. flex-direction: column;
  37. margin: 0 0 ${space(2)};
  38. padding: ${space(1.5)} ${space(2)};
  39. font-size: ${theme.fontSizeLarge};
  40. box-shadow: ${theme.dropShadowLight};
  41. border-radius: ${theme.borderRadius};
  42. border: 1px solid ${alertColors.border};
  43. background: ${opaque
  44. ? `linear-gradient(${alertColors.backgroundLight}, ${alertColors.backgroundLight}), linear-gradient(${theme.background}, ${theme.background})`
  45. : `${alertColors.backgroundLight}`};
  46. a:not([role='button']) {
  47. color: ${theme.textColor};
  48. border-bottom: 1px dotted ${theme.textColor};
  49. }
  50. ${system &&
  51. `
  52. border-width: 0 0 1px 0;
  53. border-radius: 0;
  54. `}
  55. ${IconWrapper} {
  56. color: ${alertColors.iconColor};
  57. }
  58. `;
  59. };
  60. const StyledTextBlock = styled('span')`
  61. line-height: 1.5;
  62. position: relative;
  63. flex: 1;
  64. `;
  65. const MessageContainer = styled('div')`
  66. display: flex;
  67. width: 100%;
  68. `;
  69. const ExpandContainer = styled('div')`
  70. display: grid;
  71. grid-template-columns: minmax(${space(4)}, 1fr) 30fr 1fr;
  72. grid-template-areas: '. details details';
  73. padding: ${space(1.5)} 0;
  74. `;
  75. const DetailsContainer = styled('div')`
  76. grid-area: details;
  77. `;
  78. const ExpandIcon = styled(props => (
  79. <IconWrapper {...props}>{<IconChevron size="md" />}</IconWrapper>
  80. ))`
  81. transform: ${props => (props.isExpanded ? 'rotate(0deg)' : 'rotate(180deg)')};
  82. cursor: pointer;
  83. justify-self: flex-end;
  84. `;
  85. const Alert = styled(
  86. ({
  87. type,
  88. icon,
  89. children,
  90. className,
  91. expand,
  92. expandIcon,
  93. onExpandIconClick,
  94. opaque: _opaque, // don't forward to `div`
  95. system: _system, // don't forward to `div`
  96. ...props
  97. }: AlertProps) => {
  98. const [isExpanded, setIsExpanded] = useState(false);
  99. const showExpand = expand && expand.length;
  100. const showExpandItems = showExpand && isExpanded;
  101. const handleOnExpandIconClick = onExpandIconClick ? onExpandIconClick : setIsExpanded;
  102. return (
  103. <div className={classNames(type ? `ref-${type}` : '', className)} {...props}>
  104. <MessageContainer>
  105. {icon && <IconWrapper>{icon}</IconWrapper>}
  106. <StyledTextBlock>{children}</StyledTextBlock>
  107. {showExpand && (
  108. <div onClick={() => handleOnExpandIconClick(!isExpanded)}>
  109. {expandIcon || <ExpandIcon isExpanded={isExpanded} />}
  110. </div>
  111. )}
  112. </MessageContainer>
  113. {showExpandItems && (
  114. <ExpandContainer>
  115. <DetailsContainer>{(expand || []).map(item => item)}</DetailsContainer>
  116. </ExpandContainer>
  117. )}
  118. </div>
  119. );
  120. }
  121. )<AlertProps>`
  122. ${alertStyles}
  123. `;
  124. Alert.defaultProps = {
  125. type: DEFAULT_TYPE,
  126. };
  127. export {alertStyles};
  128. export default Alert;