addWidget.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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. };
  31. function AddWidget({onAddWidget}: Props) {
  32. const {setNodeRef, transform} = useSortable({
  33. disabled: true,
  34. id: ADD_WIDGET_BUTTON_DRAG_ID,
  35. transition: null,
  36. });
  37. const organization = useOrganization();
  38. const defaultDataset = organization.features.includes(
  39. 'performance-discover-dataset-selector'
  40. )
  41. ? DataSet.ERRORS
  42. : DataSet.EVENTS;
  43. return (
  44. <Feature features="dashboards-edit">
  45. <WidgetWrapper
  46. key="add"
  47. ref={setNodeRef}
  48. displayType={DisplayType.BIG_NUMBER}
  49. layoutId={ADD_WIDGET_BUTTON_DRAG_ID}
  50. style={{originX: 0, originY: 0}}
  51. animate={
  52. transform
  53. ? {
  54. x: transform.x,
  55. y: transform.y,
  56. scaleX: transform?.scaleX && transform.scaleX <= 1 ? transform.scaleX : 1,
  57. scaleY: transform?.scaleY && transform.scaleY <= 1 ? transform.scaleY : 1,
  58. }
  59. : initialStyles
  60. }
  61. transition={{
  62. duration: 0.25,
  63. }}
  64. >
  65. {hasCustomMetrics(organization) ? (
  66. <InnerWrapper>
  67. <AddWidgetButton
  68. onAddWidget={onAddWidget}
  69. aria-label={t('Add Widget')}
  70. data-test-id="widget-add"
  71. />
  72. </InnerWrapper>
  73. ) : (
  74. <InnerWrapper onClick={() => onAddWidget(defaultDataset)}>
  75. <AddButton
  76. data-test-id="widget-add"
  77. icon={<IconAdd size="lg" isCircled color="inactive" />}
  78. aria-label={t('Add widget')}
  79. />
  80. </InnerWrapper>
  81. )}
  82. </WidgetWrapper>
  83. </Feature>
  84. );
  85. }
  86. const AddButton = styled(Button)`
  87. border: none;
  88. &,
  89. &:focus,
  90. &:active,
  91. &:hover {
  92. background: transparent;
  93. box-shadow: none;
  94. }
  95. `;
  96. export default AddWidget;
  97. export function AddWidgetButton({onAddWidget, ...buttonProps}: Props & ButtonProps) {
  98. const organization = useOrganization();
  99. const handleAction = useCallback(
  100. (dataset: DataSet) => {
  101. trackAnalytics('dashboards_views.widget_library.opened', {
  102. organization,
  103. });
  104. onAddWidget(dataset);
  105. },
  106. [organization, onAddWidget]
  107. );
  108. const items = useMemo(() => {
  109. const menuItems: MenuItemProps[] = [];
  110. if (organization.features.includes('performance-discover-dataset-selector')) {
  111. menuItems.push({
  112. key: DataSet.ERRORS,
  113. label: t('Errors'),
  114. onAction: () => handleAction(DataSet.ERRORS),
  115. });
  116. menuItems.push({
  117. key: DataSet.TRANSACTIONS,
  118. label: t('Transactions'),
  119. onAction: () => handleAction(DataSet.TRANSACTIONS),
  120. });
  121. } else {
  122. menuItems.push({
  123. key: DataSet.EVENTS,
  124. label: t('Errors and Transactions'),
  125. onAction: () => handleAction(DataSet.EVENTS),
  126. });
  127. }
  128. menuItems.push({
  129. key: DataSet.ISSUES,
  130. label: t('Issues'),
  131. details: t('States, Assignment, Time, etc.'),
  132. onAction: () => handleAction(DataSet.ISSUES),
  133. });
  134. menuItems.push({
  135. key: DataSet.RELEASES,
  136. label: t('Releases'),
  137. details: t('Sessions, Crash rates, etc.'),
  138. onAction: () => handleAction(DataSet.RELEASES),
  139. });
  140. if (hasCustomMetrics(organization)) {
  141. menuItems.push({
  142. key: DataSet.METRICS,
  143. label: t('Metrics'),
  144. onAction: () => handleAction(DataSet.METRICS),
  145. trailingItems: (
  146. <FeatureBadge
  147. type="beta"
  148. title={
  149. 'The Metrics beta will end and we will retire the current solution on October 7th, 2024'
  150. }
  151. />
  152. ),
  153. });
  154. }
  155. return menuItems;
  156. }, [handleAction, organization]);
  157. return (
  158. <DropdownMenu
  159. items={items}
  160. trigger={triggerProps => (
  161. <DropdownButton
  162. {...triggerProps}
  163. {...buttonProps}
  164. data-test-id="widget-add"
  165. size="sm"
  166. icon={<IconAdd isCircled />}
  167. >
  168. {t('Add Widget')}
  169. </DropdownButton>
  170. )}
  171. menuTitle={
  172. <MenuTitle>
  173. {t('Dataset')}
  174. <ExternalLink href="https://docs.sentry.io/product/dashboards/widget-builder/#choose-your-dataset">
  175. {t('Learn more')}
  176. </ExternalLink>
  177. </MenuTitle>
  178. }
  179. />
  180. );
  181. }
  182. const InnerWrapper = styled('div')<{onClick?: () => void}>`
  183. width: 100%;
  184. height: 110px;
  185. border: 2px dashed ${p => p.theme.border};
  186. border-radius: ${p => p.theme.borderRadius};
  187. display: flex;
  188. align-items: center;
  189. justify-content: center;
  190. cursor: ${p => (p.onClick ? 'pointer' : '')};
  191. `;
  192. const MenuTitle = styled('span')`
  193. display: flex;
  194. gap: ${space(1)};
  195. & > a {
  196. font-weight: ${p => p.theme.fontWeightNormal};
  197. }
  198. `;