alert.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import {useState} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {useHover} from '@react-aria/interactions';
  5. import classNames from 'classnames';
  6. import {IconCheckmark, IconChevron, IconInfo, IconNot, IconWarning} from 'sentry/icons';
  7. import space from 'sentry/styles/space';
  8. import {defined} from 'sentry/utils';
  9. import {Theme} from 'sentry/utils/theme';
  10. export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
  11. expand?: React.ReactNode;
  12. icon?: React.ReactNode;
  13. opaque?: boolean;
  14. showIcon?: boolean;
  15. system?: boolean;
  16. trailingItems?: React.ReactNode;
  17. type?: keyof Theme['alert'];
  18. }
  19. const DEFAULT_TYPE = 'info';
  20. function Alert({
  21. type = DEFAULT_TYPE,
  22. showIcon = false,
  23. icon,
  24. opaque,
  25. system,
  26. expand,
  27. trailingItems,
  28. className,
  29. children,
  30. ...props
  31. }: AlertProps) {
  32. const [isExpanded, setIsExpanded] = useState(false);
  33. const showExpand = defined(expand);
  34. const showTrailingItems = defined(trailingItems);
  35. // Show the hover state (with darker borders) only when hovering over the
  36. // IconWrapper or MessageContainer.
  37. const {hoverProps: iconHoverProps, isHovered: iconIsHovered} = useHover({
  38. isDisabled: !showExpand,
  39. });
  40. const {hoverProps: messageHoverProps, isHovered: messageIsHovered} = useHover({
  41. isDisabled: !showExpand,
  42. });
  43. function getIcon() {
  44. switch (type) {
  45. case 'warning':
  46. return <IconWarning />;
  47. case 'success':
  48. return <IconCheckmark />;
  49. case 'error':
  50. return <IconNot />;
  51. case 'info':
  52. default:
  53. return <IconInfo />;
  54. }
  55. }
  56. function handleClick() {
  57. showExpand && setIsExpanded(!isExpanded);
  58. }
  59. return (
  60. <Wrap
  61. type={type}
  62. system={system}
  63. opaque={opaque}
  64. expand={expand}
  65. hovered={iconIsHovered || messageIsHovered}
  66. className={classNames(type ? `ref-${type}` : '', className)}
  67. {...props}
  68. >
  69. {showIcon && (
  70. <IconWrapper onClick={handleClick} {...iconHoverProps}>
  71. {icon ?? getIcon()}
  72. </IconWrapper>
  73. )}
  74. <ContentWrapper>
  75. <MessageContainer
  76. onClick={handleClick}
  77. showIcon={showIcon}
  78. showTrailingItems={showTrailingItems}
  79. {...messageHoverProps}
  80. >
  81. <Message>{children}</Message>
  82. {(showExpand || showTrailingItems) && (
  83. <TrailingItemsWrap>
  84. <TrailingItems onClick={e => e.stopPropagation()}>
  85. {trailingItems}
  86. </TrailingItems>
  87. {showExpand && (
  88. <ExpandIconWrap>
  89. <IconChevron direction={isExpanded ? 'up' : 'down'} />
  90. </ExpandIconWrap>
  91. )}
  92. </TrailingItemsWrap>
  93. )}
  94. </MessageContainer>
  95. {isExpanded && (
  96. <ExpandContainer>
  97. {Array.isArray(expand) ? expand.map(item => item) : expand}
  98. </ExpandContainer>
  99. )}
  100. </ContentWrapper>
  101. </Wrap>
  102. );
  103. }
  104. const alertStyles = ({
  105. type = DEFAULT_TYPE,
  106. system,
  107. opaque,
  108. expand,
  109. hovered,
  110. theme,
  111. }: AlertProps & {theme: Theme; hovered?: boolean}) => {
  112. const alertColors = theme.alert[type];
  113. const showExpand = defined(expand);
  114. return css`
  115. display: flex;
  116. margin: 0 0 ${space(2)};
  117. font-size: ${theme.fontSizeMedium};
  118. border-radius: ${theme.borderRadius};
  119. border: 1px solid ${alertColors.border};
  120. background: ${opaque
  121. ? `linear-gradient(${alertColors.backgroundLight}, ${alertColors.backgroundLight}), linear-gradient(${theme.background}, ${theme.background})`
  122. : `${alertColors.backgroundLight}`};
  123. a:not([role='button']) {
  124. color: ${theme.textColor};
  125. text-decoration-color: ${theme.translucentBorder};
  126. text-decoration-style: solid;
  127. text-decoration-line: underline;
  128. text-decoration-thickness: 0.08em;
  129. text-underline-offset: 0.06em;
  130. }
  131. a:not([role='button']):hover {
  132. text-decoration-color: ${theme.subText};
  133. text-decoration-style: solid;
  134. }
  135. pre {
  136. background: ${alertColors.backgroundLight};
  137. margin: ${space(0.5)} 0 0;
  138. }
  139. ${IconWrapper}, ${ExpandIconWrap} {
  140. color: ${alertColors.iconColor};
  141. }
  142. ${hovered &&
  143. `
  144. border-color: ${alertColors.borderHover};
  145. ${IconWrapper}, ${IconChevron} {
  146. color: ${alertColors.iconHoverColor};
  147. }
  148. `}
  149. ${showExpand &&
  150. `${IconWrapper}, ${MessageContainer} {
  151. cursor: pointer;
  152. }
  153. ${TrailingItems} {
  154. cursor: auto;
  155. }
  156. `}
  157. ${system &&
  158. `
  159. border-width: 0 0 1px 0;
  160. border-radius: 0;
  161. `}
  162. `;
  163. };
  164. const Wrap = styled('div')<AlertProps & {hovered: boolean}>`
  165. ${alertStyles}
  166. `;
  167. const IconWrapper = styled('div')`
  168. display: flex;
  169. height: calc(${p => p.theme.fontSizeMedium} * ${p => p.theme.text.lineHeightBody});
  170. padding: ${space(1.5)} ${space(1)} ${space(1.5)} ${space(2)};
  171. box-sizing: content-box;
  172. align-items: center;
  173. `;
  174. const ContentWrapper = styled('div')`
  175. width: 100%;
  176. `;
  177. const MessageContainer = styled('div')<{
  178. showIcon: boolean;
  179. showTrailingItems: boolean;
  180. }>`
  181. display: flex;
  182. width: 100%;
  183. padding-top: ${space(1.5)};
  184. padding-bottom: ${space(1.5)};
  185. padding-left: ${p => (p.showIcon ? '0' : space(2))};
  186. padding-right: ${p => (p.showTrailingItems ? space(1.5) : space(2))};
  187. `;
  188. const Message = styled('span')`
  189. line-height: ${p => p.theme.text.lineHeightBody};
  190. position: relative;
  191. flex: 1;
  192. `;
  193. const TrailingItems = styled('div')`
  194. height: calc(${p => p.theme.fontSizeMedium} * ${p => p.theme.text.lineHeightBody});
  195. display: grid;
  196. grid-auto-flow: column;
  197. grid-template-rows: 100%;
  198. align-items: center;
  199. gap: ${space(1)};
  200. `;
  201. const TrailingItemsWrap = styled(TrailingItems)`
  202. margin-left: ${space(1)};
  203. `;
  204. const ExpandIconWrap = styled('div')`
  205. height: 100%;
  206. display: flex;
  207. align-items: center;
  208. `;
  209. const ExpandContainer = styled('div')`
  210. display: grid;
  211. padding-right: ${space(1.5)};
  212. padding-bottom: ${space(1.5)};
  213. `;
  214. export {alertStyles};
  215. export default Alert;