addWidget.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import {useCallback, useMemo} from 'react';
  2. import {useSortable} from '@dnd-kit/sortable';
  3. import styled from '@emotion/styled';
  4. import type {ButtonProps} from 'sentry/components/button';
  5. import {Button} from 'sentry/components/button';
  6. import DropdownButton from 'sentry/components/dropdownButton';
  7. import type {MenuItemProps} from 'sentry/components/dropdownMenu';
  8. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  9. import FeatureBadge from 'sentry/components/featureBadge';
  10. import ExternalLink from 'sentry/components/links/externalLink';
  11. import {IconAdd} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import {trackAnalytics} from 'sentry/utils/analytics';
  15. import {hasCustomMetrics} from 'sentry/utils/metrics/features';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import {DataSet} from 'sentry/views/dashboards/widgetBuilder/utils';
  18. import {DisplayType} from './types';
  19. import WidgetWrapper from './widgetWrapper';
  20. export const ADD_WIDGET_BUTTON_DRAG_ID = 'add-widget-button';
  21. const initialStyles = {
  22. x: 0,
  23. y: 0,
  24. scaleX: 1,
  25. scaleY: 1,
  26. };
  27. type Props = {
  28. onAddWidget: (dataset: DataSet) => void;
  29. };
  30. function AddWidget({onAddWidget}: Props) {
  31. const {setNodeRef, transform} = useSortable({
  32. disabled: true,
  33. id: ADD_WIDGET_BUTTON_DRAG_ID,
  34. transition: null,
  35. });
  36. const organization = useOrganization();
  37. return (
  38. <WidgetWrapper
  39. key="add"
  40. ref={setNodeRef}
  41. displayType={DisplayType.BIG_NUMBER}
  42. layoutId={ADD_WIDGET_BUTTON_DRAG_ID}
  43. style={{originX: 0, originY: 0}}
  44. animate={
  45. transform
  46. ? {
  47. x: transform.x,
  48. y: transform.y,
  49. scaleX: transform?.scaleX && transform.scaleX <= 1 ? transform.scaleX : 1,
  50. scaleY: transform?.scaleY && transform.scaleY <= 1 ? transform.scaleY : 1,
  51. }
  52. : initialStyles
  53. }
  54. transition={{
  55. duration: 0.25,
  56. }}
  57. >
  58. {hasCustomMetrics(organization) ? (
  59. <InnerWrapper>
  60. <AddWidgetButton
  61. onAddWidget={onAddWidget}
  62. aria-label="Add Widget"
  63. data-test-id="widget-add"
  64. />
  65. </InnerWrapper>
  66. ) : (
  67. <InnerWrapper onClick={() => onAddWidget(DataSet.EVENTS)}>
  68. <AddButton
  69. data-test-id="widget-add"
  70. icon={<IconAdd size="lg" isCircled color="inactive" />}
  71. aria-label={t('Add widget')}
  72. />
  73. </InnerWrapper>
  74. )}
  75. </WidgetWrapper>
  76. );
  77. }
  78. const AddButton = styled(Button)`
  79. border: none;
  80. &,
  81. &:focus,
  82. &:active,
  83. &:hover {
  84. background: transparent;
  85. box-shadow: none;
  86. }
  87. `;
  88. export default AddWidget;
  89. export function AddWidgetButton({onAddWidget, ...buttonProps}: Props & ButtonProps) {
  90. const organization = useOrganization();
  91. const handleAction = useCallback(
  92. (dataset: DataSet) => {
  93. trackAnalytics('dashboards_views.widget_library.opened', {
  94. organization,
  95. });
  96. onAddWidget(dataset);
  97. },
  98. [organization, onAddWidget]
  99. );
  100. const items = useMemo(() => {
  101. const menuItems: MenuItemProps[] = [
  102. {
  103. key: DataSet.EVENTS,
  104. label: t('Errors and Transactions'),
  105. onAction: () => handleAction(DataSet.EVENTS),
  106. },
  107. {
  108. key: DataSet.ISSUES,
  109. label: t('Issues'),
  110. details: t('States, Assignment, Time, etc.'),
  111. onAction: () => handleAction(DataSet.ISSUES),
  112. },
  113. ];
  114. if (organization.features.includes('dashboards-rh-widget')) {
  115. menuItems.push({
  116. key: DataSet.RELEASES,
  117. label: t('Releases'),
  118. details: t('Sessions, Crash rates, etc.'),
  119. onAction: () => handleAction(DataSet.RELEASES),
  120. });
  121. }
  122. if (hasCustomMetrics(organization)) {
  123. menuItems.push({
  124. key: DataSet.METRICS,
  125. label: t('Custom Metrics'),
  126. onAction: () => handleAction(DataSet.METRICS),
  127. trailingItems: <FeatureBadge type="beta" />,
  128. });
  129. }
  130. return menuItems;
  131. }, [handleAction, organization]);
  132. return (
  133. <DropdownMenu
  134. items={items}
  135. trigger={triggerProps => (
  136. <DropdownButton
  137. {...triggerProps}
  138. {...buttonProps}
  139. data-test-id="widget-add"
  140. size="sm"
  141. icon={<IconAdd isCircled />}
  142. >
  143. {t('Add Widget')}
  144. </DropdownButton>
  145. )}
  146. menuTitle={
  147. <MenuTitle>
  148. {t('Dataset')}
  149. <ExternalLink href="https://docs.sentry.io/product/dashboards/widget-builder/#choose-your-dataset">
  150. {t('Learn more')}
  151. </ExternalLink>
  152. </MenuTitle>
  153. }
  154. />
  155. );
  156. }
  157. const InnerWrapper = styled('div')<{onClick?: () => void}>`
  158. width: 100%;
  159. height: 110px;
  160. border: 2px dashed ${p => p.theme.border};
  161. border-radius: ${p => p.theme.borderRadius};
  162. display: flex;
  163. align-items: center;
  164. justify-content: center;
  165. cursor: ${p => (p.onClick ? 'pointer' : '')};
  166. `;
  167. const MenuTitle = styled('span')`
  168. display: flex;
  169. gap: ${space(1)};
  170. & > a {
  171. font-weight: normal;
  172. }
  173. `;