addWidget.tsx 4.9 KB

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