featureBadge.tsx 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import {Fragment} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {captureException, withScope} from '@sentry/react';
  5. import type {SeverityLevel} from '@sentry/types';
  6. import Badge from 'sentry/components/badge';
  7. import CircleIndicator from 'sentry/components/circleIndicator';
  8. import {Tooltip, TooltipProps} from 'sentry/components/tooltip';
  9. import {t} from 'sentry/locale';
  10. import {space, ValidSize} from 'sentry/styles/space';
  11. export type BadgeType = 'alpha' | 'beta' | 'new' | 'experimental';
  12. type BadgeProps = {
  13. type: BadgeType;
  14. condensed?: boolean;
  15. expiresAt?: Date;
  16. title?: string;
  17. tooltipProps?: Partial<TooltipProps>;
  18. variant?: 'badge' | 'indicator' | 'short';
  19. };
  20. type Props = Omit<React.HTMLAttributes<HTMLDivElement>, keyof BadgeProps> & BadgeProps;
  21. const defaultTitles: Record<BadgeType, string> = {
  22. alpha: t('This feature is internal and available for QA purposes'),
  23. beta: t('This feature is available for early adopters and may change'),
  24. new: t('This feature is new! Try it out and let us know what you think'),
  25. experimental: t(
  26. 'This feature is experimental! Try it out and let us know what you think. No promises!'
  27. ),
  28. };
  29. const labels: Record<BadgeType, string> = {
  30. alpha: t('alpha'),
  31. beta: t('beta'),
  32. new: t('new'),
  33. experimental: t('experimental'),
  34. };
  35. const shortLabels: Record<BadgeType, string> = {
  36. alpha: 'A',
  37. beta: 'B',
  38. new: 'N',
  39. experimental: 'E',
  40. };
  41. function BaseFeatureBadge({
  42. type,
  43. variant = 'badge',
  44. title,
  45. tooltipProps,
  46. expiresAt,
  47. ...props
  48. }: Props) {
  49. const theme = useTheme();
  50. if (expiresAt && expiresAt.valueOf() < Date.now()) {
  51. // Only get 1% of events as we don't need many to know that a badge needs to be cleaned up.
  52. if (Math.random() < 0.01) {
  53. withScope(scope => {
  54. scope.setTag('title', title);
  55. scope.setTag('type', type);
  56. scope.setLevel('warning' as SeverityLevel);
  57. captureException(new Error('Expired Feature Badge'));
  58. });
  59. }
  60. return null;
  61. }
  62. return (
  63. <div {...props}>
  64. <Tooltip title={title ?? defaultTitles[type]} position="right" {...tooltipProps}>
  65. <Fragment>
  66. {variant === 'badge' && <StyledBadge type={type} text={labels[type]} />}
  67. {variant === 'short' && <StyledBadge type={type} text={shortLabels[type]} />}
  68. {variant === 'indicator' && (
  69. <CircleIndicator color={theme.badge[type].indicatorColor} size={8} />
  70. )}
  71. </Fragment>
  72. </Tooltip>
  73. </div>
  74. );
  75. }
  76. const StyledBadge = styled(Badge)`
  77. margin: 0;
  78. padding: 0 ${space(0.75)};
  79. line-height: ${space(2)};
  80. height: ${space(2)};
  81. font-weight: normal;
  82. font-size: ${p => p.theme.fontSizeExtraSmall};
  83. vertical-align: middle;
  84. `;
  85. const FeatureBadge = styled(BaseFeatureBadge)<{space?: ValidSize}>`
  86. display: inline-flex;
  87. align-items: center;
  88. margin-left: ${p => space(p.space ?? 0.75)};
  89. `;
  90. export default FeatureBadge;