addWidget.tsx 4.8 KB

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