widgetCardContextMenu.tsx 9.6 KB

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