contextMenu.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import {useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import {openAddToDashboardModal, openModal} from 'sentry/actionCreators/modal';
  5. import {navigateTo} from 'sentry/actionCreators/navigation';
  6. import Feature from 'sentry/components/acl/feature';
  7. import type {MenuItemProps} from 'sentry/components/dropdownMenu';
  8. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  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. metricsQuery: MetricsQuery;
  37. widgetIndex: number;
  38. };
  39. export function MetricQueryContextMenu({
  40. metricsQuery,
  41. displayType,
  42. widgetIndex,
  43. }: ContextMenuProps) {
  44. const organization = useOrganization();
  45. const router = useRouter();
  46. const {removeWidget, duplicateWidget, widgets} = useDDMContext();
  47. const createAlert = useMemo(
  48. () => getCreateAlert(organization, metricsQuery),
  49. [metricsQuery, organization]
  50. );
  51. const createDashboardWidget = useCreateDashboardWidget(
  52. organization,
  53. metricsQuery,
  54. displayType
  55. );
  56. const canDelete = widgets.length > 1;
  57. const items = useMemo<MenuItemProps[]>(
  58. () => [
  59. {
  60. leadingItems: [<IconCopy key="icon" />],
  61. key: 'duplicate',
  62. label: t('Duplicate'),
  63. onAction: () => {
  64. trackAnalytics('ddm.widget.duplicate', {
  65. organization,
  66. });
  67. Sentry.metrics.increment('ddm.widget.duplicate');
  68. duplicateWidget(widgetIndex);
  69. },
  70. },
  71. {
  72. leadingItems: [<IconSiren key="icon" />],
  73. key: 'add-alert',
  74. label: t('Create Alert'),
  75. disabled: !createAlert,
  76. onAction: () => {
  77. trackAnalytics('ddm.create-alert', {
  78. organization,
  79. source: 'widget',
  80. });
  81. Sentry.metrics.increment('ddm.widget.alert');
  82. createAlert?.();
  83. },
  84. },
  85. {
  86. leadingItems: [<IconDashboard key="icon" />],
  87. key: 'add-dashboard',
  88. label: (
  89. <Feature
  90. organization={organization}
  91. hookName="feature-disabled:dashboards-edit"
  92. features="dashboards-edit"
  93. >
  94. {({hasFeature}) => (
  95. <AddToDashboardItem disabled={!hasFeature}>
  96. {t('Add to Dashboard')}
  97. </AddToDashboardItem>
  98. )}
  99. </Feature>
  100. ),
  101. disabled: !createDashboardWidget,
  102. onAction: () => {
  103. if (!organization.features.includes('dashboards-edit')) {
  104. return;
  105. }
  106. trackAnalytics('ddm.add-to-dashboard', {
  107. organization,
  108. source: 'widget',
  109. });
  110. Sentry.metrics.increment('ddm.widget.dashboard');
  111. createDashboardWidget?.();
  112. },
  113. },
  114. {
  115. leadingItems: [<IconSettings key="icon" />],
  116. key: 'settings',
  117. label: t('Metric Settings'),
  118. disabled: !isCustomMetric({mri: metricsQuery.mri}),
  119. onAction: () => {
  120. trackAnalytics('ddm.widget.settings', {
  121. organization,
  122. });
  123. Sentry.metrics.increment('ddm.widget.settings');
  124. navigateTo(
  125. `/settings/projects/:projectId/metrics/${encodeURIComponent(
  126. metricsQuery.mri
  127. )}`,
  128. router
  129. );
  130. },
  131. },
  132. {
  133. leadingItems: [<IconClose key="icon" />],
  134. key: 'delete',
  135. label: t('Remove Query'),
  136. disabled: !canDelete,
  137. onAction: () => {
  138. Sentry.metrics.increment('ddm.widget.delete');
  139. removeWidget(widgetIndex);
  140. },
  141. },
  142. ],
  143. [
  144. createAlert,
  145. createDashboardWidget,
  146. metricsQuery.mri,
  147. canDelete,
  148. organization,
  149. duplicateWidget,
  150. widgetIndex,
  151. router,
  152. removeWidget,
  153. ]
  154. );
  155. if (!hasDDMFeature(organization)) {
  156. return null;
  157. }
  158. return (
  159. <DropdownMenu
  160. items={items}
  161. triggerProps={{
  162. 'aria-label': t('Widget actions'),
  163. size: 'md',
  164. showChevron: false,
  165. icon: <IconEllipsis direction="down" size="sm" />,
  166. }}
  167. position="bottom-end"
  168. />
  169. );
  170. }
  171. export function getCreateAlert(organization: Organization, metricsQuery: MetricsQuery) {
  172. if (
  173. !metricsQuery.mri ||
  174. !metricsQuery.op ||
  175. isCustomMeasurement(metricsQuery) ||
  176. !organization.access.includes('alerts:write')
  177. ) {
  178. return undefined;
  179. }
  180. return function () {
  181. return openModal(deps => (
  182. <OrganizationContext.Provider value={organization}>
  183. <CreateAlertModal metricsQuery={metricsQuery} {...deps} />
  184. </OrganizationContext.Provider>
  185. ));
  186. };
  187. }
  188. export function useCreateDashboardWidget(
  189. organization: Organization,
  190. metricsQuery: MetricsQuery,
  191. displayType?: MetricDisplayType
  192. ) {
  193. const router = useRouter();
  194. const {projects, environments, datetime} = metricsQuery;
  195. return useMemo(() => {
  196. if (!metricsQuery.mri || !metricsQuery.op || isCustomMeasurement(metricsQuery)) {
  197. return undefined;
  198. }
  199. const widgetQuery = getWidgetQuery(metricsQuery);
  200. const urlWidgetQuery = encodeWidgetQuery(widgetQuery);
  201. const widgetAsQueryParams = getWidgetAsQueryParams(
  202. metricsQuery,
  203. urlWidgetQuery,
  204. displayType
  205. );
  206. return () =>
  207. openAddToDashboardModal({
  208. organization,
  209. selection: {
  210. projects,
  211. environments,
  212. datetime,
  213. },
  214. widget: convertToDashboardWidget(metricsQuery, displayType),
  215. router,
  216. widgetAsQueryParams,
  217. location: router.location,
  218. actions: ['add-and-open-dashboard', 'add-and-stay-on-current-page'],
  219. });
  220. }, [metricsQuery, datetime, displayType, environments, organization, projects, router]);
  221. }
  222. const AddToDashboardItem = styled('div')<{disabled: boolean}>`
  223. color: ${p => (p.disabled ? p.theme.disabled : p.theme.textColor)};
  224. `;