metricQueryContextMenu.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  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, isVirtualMetric} 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 {formatMRI} from 'sentry/utils/metrics/mri';
  31. import {
  32. isMetricsQueryWidget,
  33. type MetricDisplayType,
  34. type MetricsQuery,
  35. } from 'sentry/utils/metrics/types';
  36. import {useVirtualMetricsContext} from 'sentry/utils/metrics/virtualMetricsContext';
  37. import useOrganization from 'sentry/utils/useOrganization';
  38. import usePageFilters from 'sentry/utils/usePageFilters';
  39. import useRouter from 'sentry/utils/useRouter';
  40. import {useMetricsContext} from 'sentry/views/metrics/context';
  41. import {CreateAlertModal} from 'sentry/views/metrics/createAlertModal';
  42. import {useSelectedProjects} from 'sentry/views/metrics/utils/useSelectedProjects';
  43. import {OrganizationContext} from 'sentry/views/organizationContext';
  44. type ContextMenuProps = {
  45. displayType: MetricDisplayType;
  46. metricsQuery: MetricsQuery;
  47. widgetIndex: number;
  48. };
  49. export function MetricQueryContextMenu({
  50. metricsQuery,
  51. displayType,
  52. widgetIndex,
  53. }: ContextMenuProps) {
  54. const {getVirtualMeta} = useVirtualMetricsContext();
  55. const selectedProjects = useSelectedProjects();
  56. const organization = useOrganization();
  57. const router = useRouter();
  58. const {removeWidget, duplicateWidget, widgets} = useMetricsContext();
  59. const createAlert = useMemo(
  60. () => getCreateAlert(organization, metricsQuery),
  61. [metricsQuery, organization]
  62. );
  63. const createDashboardWidget = useCreateDashboardWidget(
  64. organization,
  65. metricsQuery,
  66. displayType
  67. );
  68. // At least one query must remain
  69. const canDelete = widgets.filter(isMetricsQueryWidget).length > 1;
  70. const hasDashboardFeature = organization.features.includes('dashboards-edit');
  71. const items = useMemo<MenuItemProps[]>(
  72. () => [
  73. {
  74. leadingItems: [<IconCopy key="icon" />],
  75. key: 'duplicate',
  76. label: t('Duplicate'),
  77. onAction: () => {
  78. trackAnalytics('ddm.widget.duplicate', {
  79. organization,
  80. });
  81. Sentry.metrics.increment('ddm.widget.duplicate');
  82. duplicateWidget(widgetIndex);
  83. },
  84. },
  85. {
  86. leadingItems: [<IconSiren key="icon" />],
  87. key: 'add-alert',
  88. label: <CreateMetricAlertFeature>{t('Create Alert')}</CreateMetricAlertFeature>,
  89. disabled:
  90. !createAlert ||
  91. !hasMetricAlertFeature(organization) ||
  92. isVirtualMetric(metricsQuery),
  93. onAction: () => {
  94. trackAnalytics('ddm.create-alert', {
  95. organization,
  96. source: 'widget',
  97. });
  98. Sentry.metrics.increment('ddm.widget.alert');
  99. createAlert?.();
  100. },
  101. },
  102. {
  103. leadingItems: [<IconDashboard key="icon" />],
  104. key: 'add-dashboard',
  105. label: (
  106. <Feature
  107. organization={organization}
  108. hookName="feature-disabled:dashboards-edit"
  109. features="dashboards-edit"
  110. renderDisabled={p => (
  111. <Hovercard
  112. body={
  113. <FeatureDisabled
  114. features={p.features}
  115. hideHelpToggle
  116. featureName={t('Metric Alerts')}
  117. />
  118. }
  119. >
  120. {typeof p.children === 'function' ? p.children(p) : p.children}
  121. </Hovercard>
  122. )}
  123. >
  124. <span>{t('Add to Dashboard')}</span>
  125. </Feature>
  126. ),
  127. disabled: !createDashboardWidget || !hasDashboardFeature,
  128. onAction: () => {
  129. if (!organization.features.includes('dashboards-edit')) {
  130. return;
  131. }
  132. trackAnalytics('ddm.add-to-dashboard', {
  133. organization,
  134. source: 'widget',
  135. });
  136. Sentry.metrics.increment('ddm.widget.dashboard');
  137. createDashboardWidget?.();
  138. },
  139. },
  140. {
  141. leadingItems: [<IconSettings key="icon" />],
  142. key: 'settings',
  143. disabled: !isCustomMetric({mri: metricsQuery.mri}),
  144. label: t('Metric Settings'),
  145. onAction: () => {
  146. trackAnalytics('ddm.widget.settings', {
  147. organization,
  148. });
  149. Sentry.metrics.increment('ddm.widget.settings');
  150. if (!isVirtualMetric(metricsQuery)) {
  151. navigateTo(
  152. `/settings/projects/:projectId/metrics/${encodeURIComponent(
  153. metricsQuery.mri
  154. )}`,
  155. router
  156. );
  157. } else {
  158. const metricsMeta = getVirtualMeta(metricsQuery.mri);
  159. const targetProject = selectedProjects.find(
  160. p => p.id === String(metricsMeta.projectIds[0])
  161. );
  162. navigateTo(
  163. `/settings/projects/${targetProject?.slug || ':projectId'}/metrics/${formatMRI(metricsQuery.mri)}/edit/`,
  164. router
  165. );
  166. }
  167. },
  168. },
  169. {
  170. leadingItems: [<IconClose key="icon" />],
  171. key: 'delete',
  172. label: t('Remove Metric'),
  173. disabled: !canDelete,
  174. onAction: () => {
  175. Sentry.metrics.increment('ddm.widget.delete');
  176. removeWidget(widgetIndex);
  177. },
  178. },
  179. ],
  180. [
  181. createAlert,
  182. organization,
  183. createDashboardWidget,
  184. hasDashboardFeature,
  185. metricsQuery,
  186. canDelete,
  187. duplicateWidget,
  188. widgetIndex,
  189. router,
  190. getVirtualMeta,
  191. selectedProjects,
  192. removeWidget,
  193. ]
  194. );
  195. if (!hasCustomMetrics(organization)) {
  196. return null;
  197. }
  198. return (
  199. <DropdownMenu
  200. items={items}
  201. triggerProps={{
  202. 'aria-label': t('Widget actions'),
  203. size: 'md',
  204. showChevron: false,
  205. icon: <IconEllipsis direction="down" size="sm" />,
  206. }}
  207. position="bottom-end"
  208. />
  209. );
  210. }
  211. export function getCreateAlert(organization: Organization, metricsQuery: MetricsQuery) {
  212. if (
  213. !metricsQuery.mri ||
  214. !metricsQuery.aggregation ||
  215. isCustomMeasurement(metricsQuery) ||
  216. !organization.access.includes('alerts:write')
  217. ) {
  218. return undefined;
  219. }
  220. return function () {
  221. return openModal(deps => (
  222. <OrganizationContext.Provider value={organization}>
  223. <CreateAlertModal metricsQuery={metricsQuery} {...deps} />
  224. </OrganizationContext.Provider>
  225. ));
  226. };
  227. }
  228. export function useCreateDashboardWidget(
  229. organization: Organization,
  230. metricsQuery: MetricsQuery,
  231. displayType?: MetricDisplayType
  232. ) {
  233. const router = useRouter();
  234. const {resolveVirtualMRI} = useVirtualMetricsContext();
  235. const {selection} = usePageFilters();
  236. return useMemo(() => {
  237. if (
  238. !metricsQuery.mri ||
  239. !metricsQuery.aggregation ||
  240. isCustomMeasurement(metricsQuery)
  241. ) {
  242. return undefined;
  243. }
  244. const queryCopy = {...metricsQuery};
  245. if (isVirtualMetric(metricsQuery) && metricsQuery.condition) {
  246. const {mri, aggregation} = resolveVirtualMRI(
  247. metricsQuery.mri,
  248. metricsQuery.condition,
  249. metricsQuery.aggregation
  250. );
  251. queryCopy.mri = mri;
  252. queryCopy.aggregation = aggregation;
  253. }
  254. const widgetQuery = getWidgetQuery(queryCopy);
  255. const urlWidgetQuery = encodeWidgetQuery(widgetQuery);
  256. const widgetAsQueryParams = getWidgetAsQueryParams(
  257. selection,
  258. urlWidgetQuery,
  259. displayType
  260. );
  261. return () =>
  262. openAddToDashboardModal({
  263. organization,
  264. selection,
  265. widget: convertToDashboardWidget([queryCopy], displayType),
  266. router,
  267. widgetAsQueryParams,
  268. location: router.location,
  269. actions: ['add-and-open-dashboard', 'add-and-stay-on-current-page'],
  270. allowCreateNewDashboard: false,
  271. });
  272. }, [metricsQuery, selection, displayType, resolveVirtualMRI, organization, router]);
  273. }