contextMenu.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import {Fragment, 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 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 {
  10. IconClose,
  11. IconCopy,
  12. IconDashboard,
  13. IconEllipsis,
  14. IconSettings,
  15. IconSiren,
  16. } from 'sentry/icons';
  17. import {t} from 'sentry/locale';
  18. import type {Organization} from 'sentry/types';
  19. import {trackAnalytics} from 'sentry/utils/analytics';
  20. import {isCustomMeasurement, isCustomMetric} from 'sentry/utils/metrics';
  21. import {
  22. convertToDashboardWidget,
  23. encodeWidgetQuery,
  24. getWidgetAsQueryParams,
  25. getWidgetQuery,
  26. } from 'sentry/utils/metrics/dashboard';
  27. import {hasDDMFeature} from 'sentry/utils/metrics/features';
  28. import type {MetricDisplayType, MetricsQuery} from 'sentry/utils/metrics/types';
  29. import useOrganization from 'sentry/utils/useOrganization';
  30. import useRouter from 'sentry/utils/useRouter';
  31. import {useDDMContext} from 'sentry/views/ddm/context';
  32. import {CreateAlertModal} from 'sentry/views/ddm/createAlertModal';
  33. import {OrganizationContext} from 'sentry/views/organizationContext';
  34. type ContextMenuProps = {
  35. displayType: MetricDisplayType;
  36. hasDashboardFeature: boolean;
  37. metricsQuery: MetricsQuery;
  38. widgetIndex: number;
  39. };
  40. export function MetricQueryContextMenu({
  41. metricsQuery,
  42. displayType,
  43. widgetIndex,
  44. hasDashboardFeature,
  45. }: ContextMenuProps) {
  46. const organization = useOrganization();
  47. const router = useRouter();
  48. const {removeWidget, duplicateWidget, widgets} = useDDMContext();
  49. const createAlert = useMemo(
  50. () => getCreateAlert(organization, metricsQuery),
  51. [metricsQuery, organization]
  52. );
  53. const createDashboardWidget = useCreateDashboardWidget(
  54. organization,
  55. metricsQuery,
  56. displayType
  57. );
  58. const canDelete = widgets.length > 1;
  59. const items = useMemo<MenuItemProps[]>(
  60. () => [
  61. {
  62. leadingItems: [<IconCopy key="icon" />],
  63. key: 'duplicate',
  64. label: t('Duplicate'),
  65. onAction: () => {
  66. trackAnalytics('ddm.widget.duplicate', {
  67. organization,
  68. });
  69. Sentry.metrics.increment('ddm.widget.duplicate');
  70. duplicateWidget(widgetIndex);
  71. },
  72. },
  73. {
  74. leadingItems: [<IconSiren key="icon" />],
  75. key: 'add-alert',
  76. label: t('Create Alert'),
  77. disabled: !createAlert,
  78. onAction: () => {
  79. trackAnalytics('ddm.create-alert', {
  80. organization,
  81. source: 'widget',
  82. });
  83. Sentry.metrics.increment('ddm.widget.alert');
  84. createAlert?.();
  85. },
  86. },
  87. {
  88. leadingItems: [<IconDashboard key="icon" />],
  89. key: 'add-dashoard',
  90. label: hasDashboardFeature ? (
  91. <Fragment>{t('Add to Dashboard')}</Fragment>
  92. ) : (
  93. <Hovercard
  94. body={
  95. <FeatureDisabled
  96. features="organizations:dashboards-edit1"
  97. hideHelpToggle
  98. featureName={t('Dashboard Editing')}
  99. />
  100. }
  101. >
  102. {t('Add to Dashboard')}
  103. </Hovercard>
  104. ),
  105. disabled: !hasDashboardFeature || !createDashboardWidget,
  106. onAction: () => {
  107. trackAnalytics('ddm.add-to-dashboard', {
  108. organization,
  109. source: 'widget',
  110. });
  111. Sentry.metrics.increment('ddm.widget.dashboard');
  112. createDashboardWidget?.();
  113. },
  114. },
  115. {
  116. leadingItems: [<IconSettings key="icon" />],
  117. key: 'settings',
  118. label: t('Metric Settings'),
  119. disabled: !isCustomMetric({mri: metricsQuery.mri}),
  120. onAction: () => {
  121. trackAnalytics('ddm.widget.settings', {
  122. organization,
  123. });
  124. Sentry.metrics.increment('ddm.widget.settings');
  125. navigateTo(
  126. `/settings/projects/:projectId/metrics/${encodeURIComponent(
  127. metricsQuery.mri
  128. )}`,
  129. router
  130. );
  131. },
  132. },
  133. {
  134. leadingItems: [<IconClose key="icon" />],
  135. key: 'delete',
  136. label: t('Remove Query'),
  137. disabled: !canDelete,
  138. onAction: () => {
  139. Sentry.metrics.increment('ddm.widget.delete');
  140. removeWidget(widgetIndex);
  141. },
  142. },
  143. ],
  144. [
  145. hasDashboardFeature,
  146. createAlert,
  147. createDashboardWidget,
  148. metricsQuery.mri,
  149. canDelete,
  150. organization,
  151. duplicateWidget,
  152. widgetIndex,
  153. router,
  154. removeWidget,
  155. ]
  156. );
  157. if (!hasDDMFeature(organization)) {
  158. return null;
  159. }
  160. return (
  161. <DropdownMenu
  162. items={items}
  163. triggerProps={{
  164. 'aria-label': t('Widget actions'),
  165. size: 'md',
  166. showChevron: false,
  167. icon: <IconEllipsis direction="down" size="sm" />,
  168. }}
  169. position="bottom-end"
  170. />
  171. );
  172. }
  173. export function getCreateAlert(organization: Organization, metricsQuery: MetricsQuery) {
  174. if (
  175. !metricsQuery.mri ||
  176. !metricsQuery.op ||
  177. isCustomMeasurement(metricsQuery) ||
  178. !organization.access.includes('alerts:write')
  179. ) {
  180. return undefined;
  181. }
  182. return function () {
  183. return openModal(deps => (
  184. <OrganizationContext.Provider value={organization}>
  185. <CreateAlertModal metricsQuery={metricsQuery} {...deps} />
  186. </OrganizationContext.Provider>
  187. ));
  188. };
  189. }
  190. export function useCreateDashboardWidget(
  191. organization: Organization,
  192. metricsQuery: MetricsQuery,
  193. displayType?: MetricDisplayType
  194. ) {
  195. const router = useRouter();
  196. const {projects, environments, datetime} = metricsQuery;
  197. return useMemo(() => {
  198. if (!metricsQuery.mri || !metricsQuery.op || isCustomMeasurement(metricsQuery)) {
  199. return undefined;
  200. }
  201. const widgetQuery = getWidgetQuery(metricsQuery);
  202. const urlWidgetQuery = encodeWidgetQuery(widgetQuery);
  203. const widgetAsQueryParams = getWidgetAsQueryParams(
  204. metricsQuery,
  205. urlWidgetQuery,
  206. displayType
  207. );
  208. return () =>
  209. openAddToDashboardModal({
  210. organization,
  211. selection: {
  212. projects,
  213. environments,
  214. datetime,
  215. },
  216. widget: convertToDashboardWidget(metricsQuery, displayType),
  217. router,
  218. widgetAsQueryParams,
  219. location: router.location,
  220. });
  221. }, [metricsQuery, datetime, displayType, environments, organization, projects, router]);
  222. }