alert.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  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. onClick={handleClick}
  68. {...messageHoverProps}
  69. {...props}
  70. >
  71. {showIcon && (
  72. <IconWrapper onClick={handleClick} {...iconHoverProps}>
  73. {icon ?? getIcon()}
  74. </IconWrapper>
  75. )}
  76. <ContentWrapper>
  77. <ContentWrapperInner>
  78. <MessageContainer>
  79. <Message>{children}</Message>
  80. {showTrailingItems && (
  81. <TrailingItemsWrap>
  82. <TrailingItems onClick={e => e.stopPropagation()}>
  83. {trailingItems}
  84. </TrailingItems>
  85. </TrailingItemsWrap>
  86. )}
  87. </MessageContainer>
  88. {isExpanded && (
  89. <ExpandContainer>
  90. {Array.isArray(expand) ? expand.map(item => item) : expand}
  91. </ExpandContainer>
  92. )}
  93. </ContentWrapperInner>
  94. {showExpand && (
  95. <ExpandIconWrap>
  96. <IconChevron direction={isExpanded ? 'up' : 'down'} />
  97. </ExpandIconWrap>
  98. )}
  99. </ContentWrapper>
  100. </Wrap>
  101. );
  102. }
  103. const alertStyles = ({
  104. type = DEFAULT_TYPE,
  105. system,
  106. opaque,
  107. expand,
  108. hovered,
  109. theme,
  110. }: AlertProps & {theme: Theme; hovered?: boolean}) => {
  111. const alertColors = theme.alert[type];
  112. const showExpand = defined(expand);
  113. return css`
  114. display: flex;
  115. margin: 0 0 ${space(2)};
  116. font-size: ${theme.fontSizeMedium};
  117. border-radius: ${theme.borderRadius};
  118. border: 1px solid ${alertColors.border};
  119. background: ${opaque
  120. ? `linear-gradient(${alertColors.backgroundLight}, ${alertColors.backgroundLight}), linear-gradient(${theme.background}, ${theme.background})`
  121. : `${alertColors.backgroundLight}`};
  122. a:not([role='button']) {
  123. color: ${theme.textColor};
  124. text-decoration-color: ${theme.translucentBorder};
  125. text-decoration-style: solid;
  126. text-decoration-line: underline;
  127. text-decoration-thickness: 0.08em;
  128. text-underline-offset: 0.06em;
  129. }
  130. a:not([role='button']):hover {
  131. text-decoration-color: ${theme.subText};
  132. text-decoration-style: solid;
  133. }
  134. pre {
  135. background: ${alertColors.backgroundLight};
  136. margin: ${space(0.5)} 0 0;
  137. }
  138. ${IconWrapper}, ${ExpandIconWrap} {
  139. color: ${alertColors.iconColor};
  140. }
  141. ${hovered &&
  142. `
  143. border-color: ${alertColors.borderHover};
  144. ${IconWrapper}, ${IconChevron} {
  145. color: ${alertColors.iconHoverColor};
  146. }
  147. `}
  148. ${showExpand &&
  149. `cursor: pointer;
  150. ${TrailingItems} {
  151. cursor: auto;
  152. }
  153. `}
  154. ${system &&
  155. `
  156. border-width: 0 0 1px 0;
  157. border-radius: 0;
  158. `}
  159. `;
  160. };
  161. const Wrap = styled('div')<AlertProps & {hovered: boolean}>`
  162. ${alertStyles}
  163. padding: ${space(1.5)}
  164. `;
  165. const IconWrapper = styled('div')`
  166. display: flex;
  167. height: calc(${p => p.theme.fontSizeMedium} * ${p => p.theme.text.lineHeightBody});
  168. padding-right: ${space(0.5)};
  169. padding-left: ${space(0.5)};
  170. box-sizing: content-box;
  171. align-items: center;
  172. `;
  173. const ContentWrapper = styled('div')`
  174. width: 100%;
  175. display: flex;
  176. flex-direction: row;
  177. `;
  178. const ContentWrapperInner = styled('div')`
  179. flex-grow: 1;
  180. `;
  181. const MessageContainer = styled('div')`
  182. display: flex;
  183. width: 100%;
  184. padding-left: ${space(0.5)};
  185. padding-right: ${space(0.5)};
  186. flex-direction: row;
  187. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  188. flex-direction: column;
  189. align-items: start;
  190. }
  191. `;
  192. const Message = styled('span')`
  193. line-height: ${p => p.theme.text.lineHeightBody};
  194. position: relative;
  195. flex: 1;
  196. `;
  197. const TrailingItems = styled('div')`
  198. height: calc(${p => p.theme.fontSizeMedium} * ${p => p.theme.text.lineHeightBody});
  199. display: grid;
  200. grid-auto-flow: column;
  201. grid-template-rows: 100%;
  202. align-items: center;
  203. gap: ${space(1)};
  204. `;
  205. const TrailingItemsWrap = styled(TrailingItems)`
  206. margin-left: ${space(1)};
  207. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  208. margin-left: 0;
  209. margin-top: ${space(2)};
  210. }
  211. `;
  212. const ExpandIconWrap = styled('div')`
  213. height: 100%;
  214. display: flex;
  215. align-items: start;
  216. padding-left: ${space(0.5)};
  217. padding-right: ${space(0.5)};
  218. `;
  219. const ExpandContainer = styled('div')`
  220. display: grid;
  221. padding-top: ${space(1.5)};
  222. padding-right: ${space(1.5)};
  223. padding-left: ${space(0.5)};
  224. `;
  225. export {alertStyles};
  226. export default Alert;