metricQueryContextMenu.tsx 7.3 KB

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