toastIndicator.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import styled from '@emotion/styled';
  2. import classNames from 'classnames';
  3. import {motion} from 'framer-motion';
  4. import {Indicator} from 'sentry/actionCreators/indicator';
  5. import {Button} from 'sentry/components/button';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import {IconCheckmark, IconClose} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import testableTransition from 'sentry/utils/testableTransition';
  11. type Props = {
  12. indicator: Indicator;
  13. onDismiss: (indicator: Indicator, event: React.MouseEvent) => void;
  14. className?: string;
  15. };
  16. function ToastIndicator({indicator, onDismiss, className, ...props}: Props) {
  17. let icon: React.ReactNode;
  18. const {options, message, type} = indicator;
  19. const {undo, disableDismiss} = options || {};
  20. const showUndo = typeof undo === 'function';
  21. const handleClick = (e: React.MouseEvent) => {
  22. if (disableDismiss) {
  23. return;
  24. }
  25. if (typeof onDismiss === 'function') {
  26. onDismiss(indicator, e);
  27. }
  28. };
  29. if (type === 'success') {
  30. icon = <IconCheckmark size="lg" isCircled />;
  31. } else if (type === 'error') {
  32. icon = <IconClose size="lg" isCircled />;
  33. }
  34. // TODO(billy): Remove ref- className after removing usage from getsentry
  35. return (
  36. <Toast
  37. onClick={handleClick}
  38. data-test-id={type ? `toast-${type}` : 'toast'}
  39. className={classNames(className, 'ref-toast', `ref-${type}`)}
  40. {...props}
  41. >
  42. {type === 'loading' ? (
  43. <StyledLoadingIndicator mini />
  44. ) : (
  45. <Icon type={type}>{icon}</Icon>
  46. )}
  47. <Message>{message}</Message>
  48. {showUndo && (
  49. <Undo priority="link" onClick={undo}>
  50. {t('Undo')}
  51. </Undo>
  52. )}
  53. </Toast>
  54. );
  55. }
  56. const Toast = styled(motion.div)`
  57. display: flex;
  58. align-items: center;
  59. height: 40px;
  60. max-width: calc(100vw - ${space(4)} * 2);
  61. padding: 0 15px 0 10px;
  62. margin-top: 15px;
  63. background: ${p => p.theme.inverted.background};
  64. color: ${p => p.theme.inverted.textColor};
  65. border-radius: 44px 7px 7px 44px;
  66. box-shadow: ${p => p.theme.dropShadowHeavy};
  67. position: relative;
  68. `;
  69. Toast.defaultProps = {
  70. initial: {
  71. opacity: 0,
  72. y: 70,
  73. },
  74. animate: {
  75. opacity: 1,
  76. y: 0,
  77. },
  78. exit: {
  79. opacity: 0,
  80. y: 70,
  81. },
  82. transition: testableTransition({
  83. type: 'spring',
  84. stiffness: 450,
  85. damping: 25,
  86. }),
  87. };
  88. const Icon = styled('div', {shouldForwardProp: p => p !== 'type'})<{type: string}>`
  89. margin-right: ${space(0.75)};
  90. svg {
  91. display: block;
  92. }
  93. color: ${p => (p.type === 'success' ? p.theme.successText : p.theme.errorText)};
  94. `;
  95. const Message = styled('div')`
  96. flex: 1;
  97. ${p => p.theme.overflowEllipsis}
  98. `;
  99. const Undo = styled(Button)`
  100. color: ${p => p.theme.inverted.linkColor};
  101. margin-left: ${space(2)};
  102. &:hover {
  103. color: ${p => p.theme.inverted.linkHoverColor};
  104. }
  105. `;
  106. const StyledLoadingIndicator = styled(LoadingIndicator)`
  107. .loading-indicator {
  108. border-color: ${p => p.theme.inverted.border};
  109. border-left-color: ${p => p.theme.inverted.purple300};
  110. }
  111. `;
  112. export default ToastIndicator;