contextMenu.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import {useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import {openAddToDashboardModal, openModal} from 'sentry/actionCreators/modal';
  5. import {navigateTo} from 'sentry/actionCreators/navigation';
  6. import Feature from 'sentry/components/acl/feature';
  7. import type {MenuItemProps} from 'sentry/components/dropdownMenu';
  8. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  9. import {
  10. IconClose,
  11. IconCopy,
  12. IconDashboard,
  13. IconEllipsis,
  14. IconSettings,
  15. IconSiren,
  16. } from 'sentry/icons';
  17. import {t} from 'sentry/locale';
  18. import type {Organization} from 'sentry/types';
  19. import {trackAnalytics} from 'sentry/utils/analytics';
  20. import {isCustomMeasurement, isCustomMetric} from 'sentry/utils/metrics';
  21. import {
  22. convertToDashboardWidget,
  23. encodeWidgetQuery,
  24. getWidgetAsQueryParams,
  25. getWidgetQuery,
  26. } from 'sentry/utils/metrics/dashboard';
  27. import {hasDDMFeature} from 'sentry/utils/metrics/features';
  28. import type {MetricDisplayType, MetricsQuery} from 'sentry/utils/metrics/types';
  29. import useOrganization from 'sentry/utils/useOrganization';
  30. import usePageFilters from 'sentry/utils/usePageFilters';
  31. import useRouter from 'sentry/utils/useRouter';
  32. import {useDDMContext} from 'sentry/views/ddm/context';
  33. import {CreateAlertModal} from 'sentry/views/ddm/createAlertModal';
  34. import {OrganizationContext} from 'sentry/views/organizationContext';
  35. type ContextMenuProps = {
  36. displayType: MetricDisplayType;
  37. metricsQuery: MetricsQuery;
  38. widgetIndex: number;
  39. };
  40. export function MetricQueryContextMenu({
  41. metricsQuery,
  42. displayType,
  43. widgetIndex,
  44. }: ContextMenuProps) {
  45. const organization = useOrganization();
  46. const router = useRouter();
  47. const {removeWidget, duplicateWidget, widgets} = useDDMContext();
  48. const createAlert = useMemo(
  49. () => getCreateAlert(organization, metricsQuery),
  50. [metricsQuery, organization]
  51. );
  52. const createDashboardWidget = useCreateDashboardWidget(
  53. organization,
  54. metricsQuery,
  55. displayType
  56. );
  57. const canDelete = widgets.length > 1;
  58. const items = useMemo<MenuItemProps[]>(
  59. () => [
  60. {
  61. leadingItems: [<IconCopy key="icon" />],
  62. key: 'duplicate',
  63. label: t('Duplicate'),
  64. onAction: () => {
  65. trackAnalytics('ddm.widget.duplicate', {
  66. organization,
  67. });
  68. Sentry.metrics.increment('ddm.widget.duplicate');
  69. duplicateWidget(widgetIndex);
  70. },
  71. },
  72. {
  73. leadingItems: [<IconSiren key="icon" />],
  74. key: 'add-alert',
  75. label: t('Create Alert'),
  76. disabled: !createAlert,
  77. onAction: () => {
  78. trackAnalytics('ddm.create-alert', {
  79. organization,
  80. source: 'widget',
  81. });
  82. Sentry.metrics.increment('ddm.widget.alert');
  83. createAlert?.();
  84. },
  85. },
  86. {
  87. leadingItems: [<IconDashboard key="icon" />],
  88. key: 'add-dashboard',
  89. label: (
  90. <Feature
  91. organization={organization}
  92. hookName="feature-disabled:dashboards-edit"
  93. features="dashboards-edit"
  94. >
  95. {({hasFeature}) => (
  96. <AddToDashboardItem disabled={!hasFeature}>
  97. {t('Add to Dashboard')}
  98. </AddToDashboardItem>
  99. )}
  100. </Feature>
  101. ),
  102. disabled: !createDashboardWidget,
  103. onAction: () => {
  104. if (!organization.features.includes('dashboards-edit')) {
  105. return;
  106. }
  107. trackAnalytics('ddm.add-to-dashboard', {
  108. organization,
  109. source: 'widget',
  110. });
  111. Sentry.metrics.increment('ddm.widget.dashboard');
  112. createDashboardWidget?.();
  113. },
  114. },
  115. {
  116. leadingItems: [<IconSettings key="icon" />],
  117. key: 'settings',
  118. label: t('Metric Settings'),
  119. disabled: !isCustomMetric({mri: metricsQuery.mri}),
  120. onAction: () => {
  121. trackAnalytics('ddm.widget.settings', {
  122. organization,
  123. });
  124. Sentry.metrics.increment('ddm.widget.settings');
  125. navigateTo(
  126. `/settings/projects/:projectId/metrics/${encodeURIComponent(
  127. metricsQuery.mri
  128. )}`,
  129. router
  130. );
  131. },
  132. },
  133. {
  134. leadingItems: [<IconClose key="icon" />],
  135. key: 'delete',
  136. label: t('Remove Query'),
  137. disabled: !canDelete,
  138. onAction: () => {
  139. Sentry.metrics.increment('ddm.widget.delete');
  140. removeWidget(widgetIndex);
  141. },
  142. },
  143. ],
  144. [
  145. createAlert,
  146. createDashboardWidget,
  147. metricsQuery.mri,
  148. canDelete,
  149. organization,
  150. duplicateWidget,
  151. widgetIndex,
  152. router,
  153. removeWidget,
  154. ]
  155. );
  156. if (!hasDDMFeature(organization)) {
  157. return null;
  158. }
  159. return (
  160. <DropdownMenu
  161. items={items}
  162. triggerProps={{
  163. 'aria-label': t('Widget actions'),
  164. size: 'md',
  165. showChevron: false,
  166. icon: <IconEllipsis direction="down" size="sm" />,
  167. }}
  168. position="bottom-end"
  169. />
  170. );
  171. }
  172. export function getCreateAlert(organization: Organization, metricsQuery: MetricsQuery) {
  173. if (
  174. !metricsQuery.mri ||
  175. !metricsQuery.op ||
  176. isCustomMeasurement(metricsQuery) ||
  177. !organization.access.includes('alerts:write')
  178. ) {
  179. return undefined;
  180. }
  181. return function () {
  182. return openModal(deps => (
  183. <OrganizationContext.Provider value={organization}>
  184. <CreateAlertModal metricsQuery={metricsQuery} {...deps} />
  185. </OrganizationContext.Provider>
  186. ));
  187. };
  188. }
  189. export function useCreateDashboardWidget(
  190. organization: Organization,
  191. metricsQuery: MetricsQuery,
  192. displayType?: MetricDisplayType
  193. ) {
  194. const router = useRouter();
  195. const {selection} = usePageFilters();
  196. return useMemo(() => {
  197. if (!metricsQuery.mri || !metricsQuery.op || isCustomMeasurement(metricsQuery)) {
  198. return undefined;
  199. }
  200. const widgetQuery = getWidgetQuery(metricsQuery);
  201. const urlWidgetQuery = encodeWidgetQuery(widgetQuery);
  202. const widgetAsQueryParams = getWidgetAsQueryParams(
  203. selection,
  204. urlWidgetQuery,
  205. displayType
  206. );
  207. return () =>
  208. openAddToDashboardModal({
  209. organization,
  210. selection,
  211. widget: convertToDashboardWidget([metricsQuery], displayType),
  212. router,
  213. widgetAsQueryParams,
  214. location: router.location,
  215. actions: ['add-and-open-dashboard', 'add-and-stay-on-current-page'],
  216. });
  217. }, [metricsQuery, selection, displayType, organization, router]);
  218. }
  219. const AddToDashboardItem = styled('div')<{disabled: boolean}>`
  220. color: ${p => (p.disabled ? p.theme.disabled : p.theme.textColor)};
  221. `;