metricQueryContextMenu.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import {useMemo} from 'react';
  2. import {openAddToDashboardModal} from 'sentry/actionCreators/modal';
  3. import {navigateTo} from 'sentry/actionCreators/navigation';
  4. import Feature from 'sentry/components/acl/feature';
  5. import FeatureDisabled from 'sentry/components/acl/featureDisabled';
  6. import type {MenuItemProps} from 'sentry/components/dropdownMenu';
  7. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  8. import {Hovercard} from 'sentry/components/hovercard';
  9. import {CreateMetricAlertFeature} from 'sentry/components/metrics/createMetricAlertFeature';
  10. import {
  11. IconClose,
  12. IconCopy,
  13. IconDashboard,
  14. IconEllipsis,
  15. IconSettings,
  16. IconSiren,
  17. } from 'sentry/icons';
  18. import {t} from 'sentry/locale';
  19. import type {Organization} from 'sentry/types/organization';
  20. import {trackAnalytics} from 'sentry/utils/analytics';
  21. import {isCustomMeasurement, isCustomMetric, isVirtualMetric} from 'sentry/utils/metrics';
  22. import {
  23. convertToDashboardWidget,
  24. encodeWidgetQuery,
  25. getWidgetAsQueryParams,
  26. getWidgetQuery,
  27. } from 'sentry/utils/metrics/dashboard';
  28. import {hasCustomMetrics, hasMetricAlertFeature} from 'sentry/utils/metrics/features';
  29. import {
  30. isMetricsQueryWidget,
  31. type MetricDisplayType,
  32. type MetricsQuery,
  33. } from 'sentry/utils/metrics/types';
  34. import {useVirtualMetricsContext} from 'sentry/utils/metrics/virtualMetricsContext';
  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 {openCreateAlertModal} from 'sentry/views/metrics/createAlertModal';
  40. type ContextMenuProps = {
  41. displayType: MetricDisplayType;
  42. metricsQuery: MetricsQuery;
  43. widgetIndex: number;
  44. };
  45. export function MetricQueryContextMenu({
  46. metricsQuery,
  47. displayType,
  48. widgetIndex,
  49. }: ContextMenuProps) {
  50. const organization = useOrganization();
  51. const router = useRouter();
  52. const {removeWidget, duplicateWidget, widgets} = useMetricsContext();
  53. const createAlert = getCreateAlert(organization, metricsQuery);
  54. const createDashboardWidget = useCreateDashboardWidget(
  55. organization,
  56. metricsQuery,
  57. displayType
  58. );
  59. // At least one query must remain
  60. const canDelete = widgets.filter(isMetricsQueryWidget).length > 1;
  61. const hasDashboardFeature = organization.features.includes('dashboards-edit');
  62. const items = useMemo<MenuItemProps[]>(() => {
  63. const duplicateItem = {
  64. leadingItems: [<IconCopy key="icon" />],
  65. key: 'duplicate',
  66. label: t('Duplicate'),
  67. onAction: () => {
  68. trackAnalytics('ddm.widget.duplicate', {
  69. organization,
  70. });
  71. duplicateWidget(widgetIndex);
  72. },
  73. };
  74. const createAlertItem = {
  75. leadingItems: [<IconSiren key="icon" />],
  76. key: 'add-alert',
  77. label: <CreateMetricAlertFeature>{t('Create Alert')}</CreateMetricAlertFeature>,
  78. disabled: !createAlert || !hasMetricAlertFeature(organization),
  79. onAction: () => {
  80. trackAnalytics('ddm.create-alert', {
  81. organization,
  82. source: 'widget',
  83. });
  84. createAlert?.();
  85. },
  86. };
  87. const addToDashboardItem = {
  88. leadingItems: [<IconDashboard key="icon" />],
  89. key: 'add-dashboard',
  90. label: (
  91. <Feature
  92. organization={organization}
  93. hookName="feature-disabled:dashboards-edit"
  94. features="dashboards-edit"
  95. renderDisabled={p => (
  96. <Hovercard
  97. body={
  98. <FeatureDisabled
  99. features={p.features}
  100. hideHelpToggle
  101. featureName={t('Metric Alerts')}
  102. />
  103. }
  104. >
  105. {typeof p.children === 'function' ? p.children(p) : p.children}
  106. </Hovercard>
  107. )}
  108. >
  109. <span>{t('Add to Dashboard')}</span>
  110. </Feature>
  111. ),
  112. disabled: !createDashboardWidget || !hasDashboardFeature,
  113. onAction: () => {
  114. if (!organization.features.includes('dashboards-edit')) {
  115. return;
  116. }
  117. trackAnalytics('ddm.add-to-dashboard', {
  118. organization,
  119. source: 'widget',
  120. });
  121. createDashboardWidget?.();
  122. },
  123. };
  124. const settingsItem = {
  125. leadingItems: [<IconSettings key="icon" />],
  126. key: 'settings',
  127. disabled: !isCustomMetric({mri: metricsQuery.mri}),
  128. label: t('Configure Metric'),
  129. onAction: () => {
  130. trackAnalytics('ddm.widget.settings', {
  131. organization,
  132. });
  133. if (!isVirtualMetric(metricsQuery)) {
  134. navigateTo(
  135. `/settings/${organization.slug}/projects/:projectId/metrics/${encodeURIComponent(
  136. metricsQuery.mri
  137. )}`,
  138. router
  139. );
  140. }
  141. },
  142. };
  143. const deleteItem = {
  144. leadingItems: [<IconClose key="icon" />],
  145. key: 'delete',
  146. label: t('Delete'),
  147. disabled: !canDelete,
  148. onAction: () => {
  149. trackAnalytics('ddm.widget.delete', {
  150. organization,
  151. });
  152. removeWidget(widgetIndex);
  153. },
  154. };
  155. if (hasCustomMetrics(organization)) {
  156. return [
  157. duplicateItem,
  158. createAlertItem,
  159. addToDashboardItem,
  160. settingsItem,
  161. deleteItem,
  162. ];
  163. }
  164. return [duplicateItem, settingsItem, deleteItem];
  165. }, [
  166. createAlert,
  167. organization,
  168. metricsQuery,
  169. createDashboardWidget,
  170. hasDashboardFeature,
  171. canDelete,
  172. duplicateWidget,
  173. widgetIndex,
  174. router,
  175. removeWidget,
  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. function canCreateAlert(organization: Organization, metricsQuery: MetricsQuery) {
  191. return (
  192. organization.access.includes('alerts:write') &&
  193. metricsQuery.mri &&
  194. metricsQuery.aggregation &&
  195. !isCustomMeasurement(metricsQuery)
  196. );
  197. }
  198. export function getCreateAlert(organization: Organization, metricsQuery: MetricsQuery) {
  199. if (!canCreateAlert(organization, metricsQuery)) {
  200. return undefined;
  201. }
  202. return function () {
  203. openCreateAlertModal({metricsQuery, organization});
  204. };
  205. }
  206. function useCreateDashboardWidget(
  207. organization: Organization,
  208. metricsQuery: MetricsQuery,
  209. displayType?: MetricDisplayType
  210. ) {
  211. const router = useRouter();
  212. const {resolveVirtualMRI} = useVirtualMetricsContext();
  213. const {selection} = usePageFilters();
  214. return useMemo(() => {
  215. if (
  216. !metricsQuery.mri ||
  217. !metricsQuery.aggregation ||
  218. isCustomMeasurement(metricsQuery)
  219. ) {
  220. return undefined;
  221. }
  222. const queryCopy = {...metricsQuery};
  223. if (isVirtualMetric(metricsQuery) && metricsQuery.condition) {
  224. const {mri, aggregation} = resolveVirtualMRI(
  225. metricsQuery.mri,
  226. metricsQuery.condition,
  227. metricsQuery.aggregation
  228. );
  229. queryCopy.mri = mri;
  230. queryCopy.aggregation = aggregation;
  231. }
  232. const widgetQuery = getWidgetQuery(queryCopy);
  233. const urlWidgetQuery = encodeWidgetQuery(widgetQuery);
  234. const widgetAsQueryParams = getWidgetAsQueryParams(
  235. selection,
  236. urlWidgetQuery,
  237. displayType
  238. );
  239. return () =>
  240. openAddToDashboardModal({
  241. organization,
  242. selection,
  243. widget: convertToDashboardWidget([queryCopy], displayType),
  244. router,
  245. // Previously undetected because the type relied on implicit any.
  246. // @ts-expect-error TS(2741): Property 'source' is missing in type '{ start: Dat... Remove this comment to see the full error message
  247. widgetAsQueryParams,
  248. location: router.location,
  249. actions: ['add-and-open-dashboard', 'add-and-stay-on-current-page'],
  250. allowCreateNewDashboard: false,
  251. });
  252. }, [metricsQuery, selection, displayType, resolveVirtualMRI, organization, router]);
  253. }