addWidget.tsx 5.2 KB

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