metricQueryContextMenu.tsx 9.2 KB

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