addWidget.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import {useCallback, useMemo} from 'react';
  2. import {useSortable} from '@dnd-kit/sortable';
  3. import styled from '@emotion/styled';
  4. import Feature from 'sentry/components/acl/feature';
  5. import FeatureBadge from 'sentry/components/badge/featureBadge';
  6. import type {ButtonProps} from 'sentry/components/button';
  7. import {Button} from 'sentry/components/button';
  8. import DropdownButton from 'sentry/components/dropdownButton';
  9. import type {MenuItemProps} from 'sentry/components/dropdownMenu';
  10. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  11. import ExternalLink from 'sentry/components/links/externalLink';
  12. import {IconAdd} from 'sentry/icons';
  13. import {t} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import {trackAnalytics} from 'sentry/utils/analytics';
  16. import {hasCustomMetrics} from 'sentry/utils/metrics/features';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import {DataSet} from 'sentry/views/dashboards/widgetBuilder/utils';
  19. import {DisplayType} from './types';
  20. import WidgetWrapper from './widgetWrapper';
  21. export const ADD_WIDGET_BUTTON_DRAG_ID = 'add-widget-button';
  22. const initialStyles = {
  23. x: 0,
  24. y: 0,
  25. scaleX: 1,
  26. scaleY: 1,
  27. };
  28. type Props = {
  29. onAddWidget: (dataset: DataSet) => void;
  30. onAddWidgetFromNewWidgetBuilder?: (
  31. dataset: DataSet,
  32. openWidgetTemplates?: boolean
  33. ) => void;
  34. };
  35. function AddWidget({onAddWidget, onAddWidgetFromNewWidgetBuilder}: Props) {
  36. const {setNodeRef, transform} = useSortable({
  37. disabled: true,
  38. id: ADD_WIDGET_BUTTON_DRAG_ID,
  39. transition: null,
  40. });
  41. const organization = useOrganization();
  42. const defaultDataset = organization.features.includes(
  43. 'performance-discover-dataset-selector'
  44. )
  45. ? DataSet.ERRORS
  46. : DataSet.EVENTS;
  47. const addWidgetDropdownItems: MenuItemProps[] = [
  48. {
  49. key: 'from-widget-library',
  50. label: t('From Widget Library'),
  51. onAction: () => onAddWidgetFromNewWidgetBuilder?.(defaultDataset, true),
  52. },
  53. {
  54. key: 'create-custom-widget',
  55. label: t('Create Custom Widget'),
  56. onAction: () => onAddWidgetFromNewWidgetBuilder?.(defaultDataset, false),
  57. },
  58. ];
  59. return (
  60. <Feature features="dashboards-edit">
  61. <WidgetWrapper
  62. key="add"
  63. ref={setNodeRef}
  64. displayType={DisplayType.BIG_NUMBER}
  65. layoutId={ADD_WIDGET_BUTTON_DRAG_ID}
  66. style={{originX: 0, originY: 0}}
  67. animate={
  68. transform
  69. ? {
  70. x: transform.x,
  71. y: transform.y,
  72. scaleX: transform?.scaleX && transform.scaleX <= 1 ? transform.scaleX : 1,
  73. scaleY: transform?.scaleY && transform.scaleY <= 1 ? transform.scaleY : 1,
  74. }
  75. : initialStyles
  76. }
  77. transition={{
  78. duration: 0.25,
  79. }}
  80. >
  81. {hasCustomMetrics(organization) ? (
  82. <InnerWrapper>
  83. <AddWidgetButton
  84. onAddWidget={onAddWidget}
  85. aria-label={t('Add Widget')}
  86. data-test-id="widget-add"
  87. />
  88. </InnerWrapper>
  89. ) : organization.features.includes('dashboards-widget-builder-redesign') ? (
  90. <InnerWrapper onClick={() => onAddWidgetFromNewWidgetBuilder?.(defaultDataset)}>
  91. <DropdownMenu
  92. items={addWidgetDropdownItems}
  93. data-test-id="widget-add"
  94. triggerProps={{
  95. 'aria-label': t('Add Widget'),
  96. size: 'md',
  97. showChevron: false,
  98. icon: <IconAdd isCircled size="lg" color="inactive" />,
  99. borderless: true,
  100. }}
  101. />
  102. </InnerWrapper>
  103. ) : (
  104. <InnerWrapper onClick={() => onAddWidget(defaultDataset)}>
  105. <AddButton
  106. data-test-id="widget-add"
  107. icon={<IconAdd size="lg" isCircled color="inactive" />}
  108. aria-label={t('Add widget')}
  109. />
  110. </InnerWrapper>
  111. )}
  112. </WidgetWrapper>
  113. </Feature>
  114. );
  115. }
  116. const AddButton = styled(Button)`
  117. border: none;
  118. &,
  119. &:focus,
  120. &:active,
  121. &:hover {
  122. background: transparent;
  123. box-shadow: none;
  124. }
  125. `;
  126. export default AddWidget;
  127. export function AddWidgetButton({onAddWidget, ...buttonProps}: Props & ButtonProps) {
  128. const organization = useOrganization();
  129. const handleAction = useCallback(
  130. (dataset: DataSet) => {
  131. trackAnalytics('dashboards_views.widget_library.opened', {
  132. organization,
  133. });
  134. onAddWidget(dataset);
  135. },
  136. [organization, onAddWidget]
  137. );
  138. const items = useMemo(() => {
  139. const menuItems: MenuItemProps[] = [];
  140. if (organization.features.includes('performance-discover-dataset-selector')) {
  141. menuItems.push({
  142. key: DataSet.ERRORS,
  143. label: t('Errors'),
  144. onAction: () => handleAction(DataSet.ERRORS),
  145. });
  146. menuItems.push({
  147. key: DataSet.TRANSACTIONS,
  148. label: t('Transactions'),
  149. onAction: () => handleAction(DataSet.TRANSACTIONS),
  150. });
  151. } else {
  152. menuItems.push({
  153. key: DataSet.EVENTS,
  154. label: t('Errors and Transactions'),
  155. onAction: () => handleAction(DataSet.EVENTS),
  156. });
  157. }
  158. menuItems.push({
  159. key: DataSet.ISSUES,
  160. label: t('Issues'),
  161. details: t('States, Assignment, Time, etc.'),
  162. onAction: () => handleAction(DataSet.ISSUES),
  163. });
  164. menuItems.push({
  165. key: DataSet.RELEASES,
  166. label: t('Releases'),
  167. details: t('Sessions, Crash rates, etc.'),
  168. onAction: () => handleAction(DataSet.RELEASES),
  169. });
  170. if (hasCustomMetrics(organization)) {
  171. menuItems.push({
  172. key: DataSet.METRICS,
  173. label: t('Metrics'),
  174. onAction: () => handleAction(DataSet.METRICS),
  175. trailingItems: (
  176. <FeatureBadge
  177. type="beta"
  178. title={
  179. 'The Metrics beta will end and we will retire the current solution on October 7th, 2024'
  180. }
  181. />
  182. ),
  183. });
  184. }
  185. return menuItems;
  186. }, [handleAction, organization]);
  187. return (
  188. <DropdownMenu
  189. items={items}
  190. trigger={triggerProps => (
  191. <DropdownButton
  192. {...triggerProps}
  193. {...buttonProps}
  194. data-test-id="widget-add"
  195. size="sm"
  196. icon={<IconAdd isCircled />}
  197. >
  198. {t('Add Widget')}
  199. </DropdownButton>
  200. )}
  201. menuTitle={
  202. <MenuTitle>
  203. {t('Dataset')}
  204. <ExternalLink href="https://docs.sentry.io/product/dashboards/widget-builder/#choose-your-dataset">
  205. {t('Learn more')}
  206. </ExternalLink>
  207. </MenuTitle>
  208. }
  209. />
  210. );
  211. }
  212. const InnerWrapper = styled('div')<{onClick?: () => void}>`
  213. width: 100%;
  214. height: 110px;
  215. border: 2px dashed ${p => p.theme.border};
  216. border-radius: ${p => p.theme.borderRadius};
  217. display: flex;
  218. align-items: center;
  219. justify-content: center;
  220. cursor: ${p => (p.onClick ? 'pointer' : '')};
  221. `;
  222. const MenuTitle = styled('span')`
  223. display: flex;
  224. gap: ${space(1)};
  225. & > a {
  226. font-weight: ${p => p.theme.fontWeightNormal};
  227. }
  228. `;