widgetCardContextMenu.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. import {InjectedRouter} from 'react-router';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import {openDashboardWidgetQuerySelectorModal} from 'sentry/actionCreators/modal';
  5. import Feature from 'sentry/components/acl/feature';
  6. import Button from 'sentry/components/button';
  7. import {openConfirmModal} from 'sentry/components/confirm';
  8. import DropdownMenuControlV2 from 'sentry/components/dropdownMenuControlV2';
  9. import {MenuItemProps} from 'sentry/components/dropdownMenuItemV2';
  10. import {isWidgetViewerPath} from 'sentry/components/modals/widgetViewerModal/utils';
  11. import Tag from 'sentry/components/tag';
  12. import Tooltip from 'sentry/components/tooltip';
  13. import {IconEllipsis, IconExpand} from 'sentry/icons';
  14. import {t} from 'sentry/locale';
  15. import space from 'sentry/styles/space';
  16. import {Organization, PageFilters} from 'sentry/types';
  17. import {Series} from 'sentry/types/echarts';
  18. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  19. import {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
  20. import {
  21. getWidgetDiscoverUrl,
  22. getWidgetIssueUrl,
  23. isCustomMeasurementWidget,
  24. } from 'sentry/views/dashboardsV2/utils';
  25. import {Widget, WidgetType} from '../types';
  26. import {WidgetViewerContext} from '../widgetViewer/widgetViewerContext';
  27. import {useDashboardsMEPContext} from './dashboardsMEPContext';
  28. type Props = {
  29. location: Location;
  30. organization: Organization;
  31. router: InjectedRouter;
  32. selection: PageFilters;
  33. widget: Widget;
  34. widgetLimitReached: boolean;
  35. index?: string;
  36. isPreview?: boolean;
  37. onDelete?: () => void;
  38. onDuplicate?: () => void;
  39. onEdit?: () => void;
  40. pageLinks?: string;
  41. seriesData?: Series[];
  42. showContextMenu?: boolean;
  43. showWidgetViewerButton?: boolean;
  44. tableData?: TableDataWithTitle[];
  45. totalIssuesCount?: string;
  46. };
  47. function WidgetCardContextMenu({
  48. organization,
  49. selection,
  50. widget,
  51. widgetLimitReached,
  52. onDelete,
  53. onDuplicate,
  54. onEdit,
  55. showContextMenu,
  56. isPreview,
  57. showWidgetViewerButton,
  58. router,
  59. location,
  60. index,
  61. seriesData,
  62. tableData,
  63. pageLinks,
  64. totalIssuesCount,
  65. }: Props) {
  66. const {isMetricsData} = useDashboardsMEPContext();
  67. if (!showContextMenu) {
  68. return null;
  69. }
  70. const menuOptions: MenuItemProps[] = [];
  71. const usingCustomMeasurements = isCustomMeasurementWidget(widget);
  72. const disabledKeys: string[] = usingCustomMeasurements ? ['open-in-discover'] : [];
  73. const openWidgetViewerPath = (id: string | undefined) => {
  74. if (!isWidgetViewerPath(location.pathname)) {
  75. router.push({
  76. pathname: `${location.pathname}${
  77. location.pathname.endsWith('/') ? '' : '/'
  78. }widget/${id}/`,
  79. query: location.query,
  80. });
  81. }
  82. };
  83. if (isPreview) {
  84. return (
  85. <WidgetViewerContext.Consumer>
  86. {({setData}) => (
  87. <ContextWrapper>
  88. <StyledDropdownMenuControlV2
  89. items={[
  90. {
  91. key: 'preview',
  92. label: t(
  93. 'This is a preview only. To edit, you must add this dashboard.'
  94. ),
  95. },
  96. ]}
  97. triggerProps={{
  98. 'aria-label': t('Widget actions'),
  99. size: 'xs',
  100. borderless: true,
  101. showChevron: false,
  102. icon: <IconEllipsis direction="down" size="sm" />,
  103. }}
  104. placement="bottom right"
  105. disabledKeys={[...disabledKeys, 'preview']}
  106. />
  107. {showWidgetViewerButton && (
  108. <OpenWidgetViewerButton
  109. aria-label={t('Open Widget Viewer')}
  110. priority="link"
  111. size="zero"
  112. icon={<IconExpand size="xs" />}
  113. onClick={() => {
  114. (seriesData || tableData) &&
  115. setData({
  116. seriesData,
  117. tableData,
  118. pageLinks,
  119. totalIssuesCount,
  120. });
  121. openWidgetViewerPath(index);
  122. }}
  123. />
  124. )}
  125. </ContextWrapper>
  126. )}
  127. </WidgetViewerContext.Consumer>
  128. );
  129. }
  130. if (
  131. organization.features.includes('discover-basic') &&
  132. widget.widgetType === WidgetType.DISCOVER
  133. ) {
  134. // Open Widget in Discover
  135. if (widget.queries.length) {
  136. const discoverPath = getWidgetDiscoverUrl(
  137. widget,
  138. selection,
  139. organization,
  140. 0,
  141. isMetricsData
  142. );
  143. menuOptions.push({
  144. key: 'open-in-discover',
  145. label: usingCustomMeasurements ? (
  146. <Tooltip
  147. skipWrapper
  148. title={t(
  149. 'Widget using custom performance metrics cannot be opened in Discover.'
  150. )}
  151. >
  152. {t('Open in Discover')}
  153. </Tooltip>
  154. ) : (
  155. t('Open in Discover')
  156. ),
  157. to:
  158. !usingCustomMeasurements && widget.queries.length === 1
  159. ? discoverPath
  160. : undefined,
  161. onAction: () => {
  162. if (!usingCustomMeasurements) {
  163. if (widget.queries.length === 1) {
  164. trackAdvancedAnalyticsEvent('dashboards_views.open_in_discover.opened', {
  165. organization,
  166. widget_type: widget.displayType,
  167. });
  168. return;
  169. }
  170. trackAdvancedAnalyticsEvent('dashboards_views.query_selector.opened', {
  171. organization,
  172. widget_type: widget.displayType,
  173. });
  174. openDashboardWidgetQuerySelectorModal({organization, widget, isMetricsData});
  175. }
  176. },
  177. });
  178. }
  179. }
  180. if (widget.widgetType === WidgetType.ISSUE) {
  181. const issuesLocation = getWidgetIssueUrl(widget, selection, organization);
  182. menuOptions.push({
  183. key: 'open-in-issues',
  184. label: t('Open in Issues'),
  185. to: issuesLocation,
  186. });
  187. }
  188. if (organization.features.includes('dashboards-edit')) {
  189. menuOptions.push({
  190. key: 'duplicate-widget',
  191. label: t('Duplicate Widget'),
  192. onAction: () => onDuplicate?.(),
  193. });
  194. widgetLimitReached && disabledKeys.push('duplicate-widget');
  195. menuOptions.push({
  196. key: 'edit-widget',
  197. label: t('Edit Widget'),
  198. onAction: () => onEdit?.(),
  199. });
  200. menuOptions.push({
  201. key: 'delete-widget',
  202. label: t('Delete Widget'),
  203. priority: 'danger',
  204. onAction: () => {
  205. openConfirmModal({
  206. message: t('Are you sure you want to delete this widget?'),
  207. priority: 'danger',
  208. onConfirm: () => onDelete?.(),
  209. });
  210. },
  211. });
  212. }
  213. if (!menuOptions.length) {
  214. return null;
  215. }
  216. return (
  217. <WidgetViewerContext.Consumer>
  218. {({setData}) => (
  219. <ContextWrapper>
  220. <Feature organization={organization} features={['dashboards-mep']}>
  221. {isMetricsData === false && (
  222. <SampledTag
  223. tooltipText={t('This widget is only applicable to sampled events.')}
  224. >
  225. {t('Sampled')}
  226. </SampledTag>
  227. )}
  228. </Feature>
  229. <StyledDropdownMenuControlV2
  230. items={menuOptions}
  231. triggerProps={{
  232. 'aria-label': t('Widget actions'),
  233. size: 'xs',
  234. borderless: true,
  235. showChevron: false,
  236. icon: <IconEllipsis direction="down" size="sm" />,
  237. }}
  238. placement="bottom right"
  239. disabledKeys={disabledKeys}
  240. />
  241. {showWidgetViewerButton && (
  242. <OpenWidgetViewerButton
  243. aria-label={t('Open Widget Viewer')}
  244. priority="link"
  245. size="zero"
  246. icon={<IconExpand size="xs" />}
  247. onClick={() => {
  248. setData({
  249. seriesData,
  250. tableData,
  251. pageLinks,
  252. totalIssuesCount,
  253. });
  254. openWidgetViewerPath(widget.id ?? index);
  255. }}
  256. />
  257. )}
  258. </ContextWrapper>
  259. )}
  260. </WidgetViewerContext.Consumer>
  261. );
  262. }
  263. export default WidgetCardContextMenu;
  264. const ContextWrapper = styled('div')`
  265. display: flex;
  266. align-items: center;
  267. height: ${space(3)};
  268. margin-left: ${space(1)};
  269. `;
  270. const StyledDropdownMenuControlV2 = styled(DropdownMenuControlV2)`
  271. & > button {
  272. z-index: auto;
  273. }
  274. `;
  275. const OpenWidgetViewerButton = styled(Button)`
  276. padding: ${space(0.75)} ${space(1)};
  277. color: ${p => p.theme.textColor};
  278. &:hover {
  279. color: ${p => p.theme.textColor};
  280. background: ${p => p.theme.surface400};
  281. border-color: transparent;
  282. }
  283. `;
  284. const SampledTag = styled(Tag)`
  285. margin-right: ${space(0.5)};
  286. `;