metricQueryContextMenu.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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('Configure Metric'),
  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(
  152. metricsQuery.mri,
  153. metricsQuery.condition!
  154. );
  155. if (extractionRule) {
  156. openExtractionRuleEditModal({
  157. metricExtractionRule: extractionRule,
  158. onSubmitSuccess: data => {
  159. // Keep the unit of the MRI in sync with the unit of the extraction rule
  160. // TODO: Remove this once we have a better way to handle this
  161. const newMRI = metricsQuery.mri.replace(/@.*$/, `@${data.unit}`);
  162. updateWidget(widgetIndex, {
  163. mri: newMRI,
  164. } as Partial<MetricsQueryWidget>);
  165. },
  166. });
  167. }
  168. }
  169. },
  170. },
  171. {
  172. leadingItems: [<IconClose key="icon" />],
  173. key: 'delete',
  174. label: t('Remove Metric'),
  175. disabled: !canDelete,
  176. onAction: () => {
  177. Sentry.metrics.increment('ddm.widget.delete');
  178. removeWidget(widgetIndex);
  179. },
  180. },
  181. ],
  182. [
  183. createAlert,
  184. organization,
  185. metricsQuery,
  186. createDashboardWidget,
  187. hasDashboardFeature,
  188. canDelete,
  189. duplicateWidget,
  190. widgetIndex,
  191. router,
  192. getExtractionRule,
  193. updateWidget,
  194. removeWidget,
  195. ]
  196. );
  197. if (!hasCustomMetrics(organization)) {
  198. return null;
  199. }
  200. return (
  201. <DropdownMenu
  202. items={items}
  203. triggerProps={{
  204. 'aria-label': t('Widget actions'),
  205. size: 'md',
  206. showChevron: false,
  207. icon: <IconEllipsis direction="down" size="sm" />,
  208. }}
  209. position="bottom-end"
  210. />
  211. );
  212. }
  213. export function getCreateAlert(organization: Organization, metricsQuery: MetricsQuery) {
  214. const {resolveVirtualMRI} = useVirtualMetricsContext();
  215. const queryCopy = {...metricsQuery};
  216. if (isVirtualMetric(metricsQuery) && metricsQuery.condition) {
  217. const {mri, aggregation} = resolveVirtualMRI(
  218. metricsQuery.mri,
  219. metricsQuery.condition,
  220. metricsQuery.aggregation
  221. );
  222. queryCopy.mri = mri;
  223. queryCopy.aggregation = aggregation;
  224. }
  225. if (
  226. !metricsQuery.mri ||
  227. !metricsQuery.aggregation ||
  228. isCustomMeasurement(metricsQuery) ||
  229. !organization.access.includes('alerts:write')
  230. ) {
  231. return undefined;
  232. }
  233. return function () {
  234. return openModal(deps => (
  235. <OrganizationContext.Provider value={organization}>
  236. <CreateAlertModal metricsQuery={queryCopy} {...deps} />
  237. </OrganizationContext.Provider>
  238. ));
  239. };
  240. }
  241. export function useCreateDashboardWidget(
  242. organization: Organization,
  243. metricsQuery: MetricsQuery,
  244. displayType?: MetricDisplayType
  245. ) {
  246. const router = useRouter();
  247. const {resolveVirtualMRI} = useVirtualMetricsContext();
  248. const {selection} = usePageFilters();
  249. return useMemo(() => {
  250. if (
  251. !metricsQuery.mri ||
  252. !metricsQuery.aggregation ||
  253. isCustomMeasurement(metricsQuery)
  254. ) {
  255. return undefined;
  256. }
  257. const queryCopy = {...metricsQuery};
  258. if (isVirtualMetric(metricsQuery) && metricsQuery.condition) {
  259. const {mri, aggregation} = resolveVirtualMRI(
  260. metricsQuery.mri,
  261. metricsQuery.condition,
  262. metricsQuery.aggregation
  263. );
  264. queryCopy.mri = mri;
  265. queryCopy.aggregation = aggregation;
  266. }
  267. const widgetQuery = getWidgetQuery(queryCopy);
  268. const urlWidgetQuery = encodeWidgetQuery(widgetQuery);
  269. const widgetAsQueryParams = getWidgetAsQueryParams(
  270. selection,
  271. urlWidgetQuery,
  272. displayType
  273. );
  274. return () =>
  275. openAddToDashboardModal({
  276. organization,
  277. selection,
  278. widget: convertToDashboardWidget([queryCopy], displayType),
  279. router,
  280. widgetAsQueryParams,
  281. location: router.location,
  282. actions: ['add-and-open-dashboard', 'add-and-stay-on-current-page'],
  283. allowCreateNewDashboard: false,
  284. });
  285. }, [metricsQuery, selection, displayType, resolveVirtualMRI, organization, router]);
  286. }