metricQueryContextMenu.tsx 8.5 KB

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