widgetContainer.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. import {useEffect, useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import omit from 'lodash/omit';
  5. import pick from 'lodash/pick';
  6. import * as qs from 'query-string';
  7. import {CompactSelect, SelectOption} from 'sentry/components/compactSelect';
  8. import {CompositeSelect} from 'sentry/components/compactSelect/composite';
  9. import DropdownButton from 'sentry/components/dropdownButton';
  10. import {IconEllipsis} from 'sentry/icons/iconEllipsis';
  11. import {t} from 'sentry/locale';
  12. import {space} from 'sentry/styles/space';
  13. import {Organization} from 'sentry/types';
  14. import {trackAnalytics} from 'sentry/utils/analytics';
  15. import EventView from 'sentry/utils/discover/eventView';
  16. import {Field} from 'sentry/utils/discover/fields';
  17. import {DisplayModes} from 'sentry/utils/discover/types';
  18. import {useMEPSettingContext} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  19. import {usePerformanceDisplayType} from 'sentry/utils/performance/contexts/performanceDisplayContext';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  22. import withOrganization from 'sentry/utils/withOrganization';
  23. import {GenericPerformanceWidgetDataType} from '../types';
  24. import {_setChartSetting, filterAllowedChartsMetrics, getChartSetting} from '../utils';
  25. import {
  26. ChartDefinition,
  27. PerformanceWidgetSetting,
  28. WIDGET_DEFINITIONS,
  29. } from '../widgetDefinitions';
  30. import {HistogramWidget} from '../widgets/histogramWidget';
  31. import {LineChartListWidget} from '../widgets/lineChartListWidget';
  32. import {SingleFieldAreaWidget} from '../widgets/singleFieldAreaWidget';
  33. import {StackedAreaChartListWidget} from '../widgets/stackedAreaChartListWidget';
  34. import {TrendsWidget} from '../widgets/trendsWidget';
  35. import {VitalWidget} from '../widgets/vitalWidget';
  36. import {ChartRowProps} from './widgetChartRow';
  37. interface Props extends ChartRowProps {
  38. allowedCharts: PerformanceWidgetSetting[];
  39. chartHeight: number;
  40. defaultChartSetting: PerformanceWidgetSetting;
  41. eventView: EventView;
  42. index: number;
  43. organization: Organization;
  44. rowChartSettings: PerformanceWidgetSetting[];
  45. setRowChartSettings: (settings: PerformanceWidgetSetting[]) => void;
  46. withStaticFilters: boolean;
  47. chartColor?: string;
  48. forceDefaultChartSetting?: boolean;
  49. }
  50. function trackChartSettingChange(
  51. previousChartSetting: PerformanceWidgetSetting,
  52. chartSetting: PerformanceWidgetSetting,
  53. fromDefault: boolean,
  54. organization: Organization
  55. ) {
  56. trackAnalytics('performance_views.landingv3.widget.switch', {
  57. organization,
  58. from_widget: previousChartSetting,
  59. to_widget: chartSetting,
  60. from_default: fromDefault,
  61. is_new_menu: organization.features.includes('performance-new-widget-designs'),
  62. });
  63. }
  64. function _WidgetContainer(props: Props) {
  65. const {
  66. organization,
  67. index,
  68. chartHeight,
  69. rowChartSettings,
  70. setRowChartSettings,
  71. ...rest
  72. } = props;
  73. const performanceType = usePerformanceDisplayType();
  74. let _chartSetting = getChartSetting(
  75. index,
  76. chartHeight,
  77. performanceType,
  78. rest.defaultChartSetting,
  79. rest.forceDefaultChartSetting
  80. );
  81. const mepSetting = useMEPSettingContext();
  82. const allowedCharts = filterAllowedChartsMetrics(
  83. props.organization,
  84. props.allowedCharts,
  85. mepSetting
  86. );
  87. if (!allowedCharts.includes(_chartSetting)) {
  88. _chartSetting = rest.defaultChartSetting;
  89. }
  90. const [chartSetting, setChartSettingState] = useState(_chartSetting);
  91. const setChartSetting = (setting: PerformanceWidgetSetting) => {
  92. if (!props.forceDefaultChartSetting) {
  93. _setChartSetting(index, chartHeight, performanceType, setting);
  94. }
  95. setChartSettingState(setting);
  96. const newSettings = [...rowChartSettings];
  97. newSettings[index] = setting;
  98. setRowChartSettings(newSettings);
  99. trackChartSettingChange(
  100. chartSetting,
  101. setting,
  102. rest.defaultChartSetting === chartSetting,
  103. organization
  104. );
  105. };
  106. useEffect(() => {
  107. setChartSettingState(_chartSetting);
  108. }, [rest.defaultChartSetting, _chartSetting]);
  109. const chartDefinition = WIDGET_DEFINITIONS({organization})[chartSetting];
  110. // Construct an EventView that matches this widget's definition. The
  111. // `eventView` from the props is the _landing page_ EventView, which is different
  112. const widgetEventView = makeEventViewForWidget(props.eventView, chartDefinition);
  113. const showNewWidgetDesign = organization.features.includes(
  114. 'performance-new-widget-designs'
  115. );
  116. const widgetProps = {
  117. ...chartDefinition,
  118. chartSetting,
  119. chartDefinition,
  120. InteractiveTitle:
  121. showNewWidgetDesign && allowedCharts.length > 2
  122. ? containerProps => (
  123. <WidgetInteractiveTitle
  124. {...containerProps}
  125. eventView={widgetEventView}
  126. allowedCharts={allowedCharts}
  127. chartSetting={chartSetting}
  128. setChartSetting={setChartSetting}
  129. rowChartSettings={rowChartSettings}
  130. />
  131. )
  132. : null,
  133. ContainerActions: !showNewWidgetDesign
  134. ? containerProps => (
  135. <WidgetContainerActions
  136. {...containerProps}
  137. eventView={widgetEventView}
  138. allowedCharts={allowedCharts}
  139. chartSetting={chartSetting}
  140. setChartSetting={setChartSetting}
  141. rowChartSettings={rowChartSettings}
  142. />
  143. )
  144. : null,
  145. };
  146. const passedProps = pick(props, [
  147. 'eventView',
  148. 'location',
  149. 'organization',
  150. 'chartHeight',
  151. 'withStaticFilters',
  152. ]);
  153. const titleTooltip = showNewWidgetDesign ? '' : widgetProps.titleTooltip;
  154. switch (widgetProps.dataType) {
  155. case GenericPerformanceWidgetDataType.TRENDS:
  156. return (
  157. <TrendsWidget {...passedProps} {...widgetProps} titleTooltip={titleTooltip} />
  158. );
  159. case GenericPerformanceWidgetDataType.AREA:
  160. return (
  161. <SingleFieldAreaWidget
  162. {...passedProps}
  163. {...widgetProps}
  164. titleTooltip={titleTooltip}
  165. />
  166. );
  167. case GenericPerformanceWidgetDataType.VITALS:
  168. return (
  169. <VitalWidget {...passedProps} {...widgetProps} titleTooltip={titleTooltip} />
  170. );
  171. case GenericPerformanceWidgetDataType.LINE_LIST:
  172. return (
  173. <LineChartListWidget
  174. {...passedProps}
  175. {...widgetProps}
  176. titleTooltip={titleTooltip}
  177. />
  178. );
  179. case GenericPerformanceWidgetDataType.HISTOGRAM:
  180. return (
  181. <HistogramWidget {...passedProps} {...widgetProps} titleTooltip={titleTooltip} />
  182. );
  183. case GenericPerformanceWidgetDataType.STACKED_AREA:
  184. return <StackedAreaChartListWidget {...passedProps} {...widgetProps} />;
  185. default:
  186. throw new Error(`Widget type "${widgetProps.dataType}" has no implementation.`);
  187. }
  188. }
  189. export function WidgetInteractiveTitle({
  190. chartSetting,
  191. eventView,
  192. setChartSetting,
  193. allowedCharts,
  194. rowChartSettings,
  195. }) {
  196. const organization = useOrganization();
  197. const menuOptions: SelectOption<string>[] = [];
  198. const settingsMap = WIDGET_DEFINITIONS({organization});
  199. for (const setting of allowedCharts) {
  200. const options = settingsMap[setting];
  201. menuOptions.push({
  202. value: setting,
  203. label: options.title,
  204. disabled: setting !== chartSetting && rowChartSettings.includes(setting),
  205. });
  206. }
  207. const chartDefinition = WIDGET_DEFINITIONS({organization})[chartSetting];
  208. if (chartDefinition.allowsOpenInDiscover) {
  209. menuOptions.push({label: t('Open in Discover'), value: 'open_in_discover'});
  210. }
  211. const handleChange = option => {
  212. if (option.value === 'open_in_discover') {
  213. browserHistory.push(
  214. normalizeUrl(getEventViewDiscoverPath(organization, eventView))
  215. );
  216. } else {
  217. setChartSetting(option.value);
  218. }
  219. };
  220. return (
  221. <StyledCompactSelect
  222. options={menuOptions}
  223. value={chartSetting}
  224. onChange={handleChange}
  225. triggerProps={{borderless: true, size: 'zero'}}
  226. offset={4}
  227. />
  228. );
  229. }
  230. const StyledCompactSelect = styled(CompactSelect)`
  231. /* Reset font-weight set by HeaderTitleLegend, buttons are already bold and
  232. * setting this higher up causes it to trickle into the menues */
  233. font-weight: normal;
  234. margin: -${space(0.5)} -${space(1)} -${space(0.25)};
  235. min-width: 0;
  236. button {
  237. padding: ${space(0.5)} ${space(1)};
  238. font-size: ${p => p.theme.fontSizeLarge};
  239. }
  240. `;
  241. export function WidgetContainerActions({
  242. chartSetting,
  243. eventView,
  244. setChartSetting,
  245. allowedCharts,
  246. rowChartSettings,
  247. }: {
  248. allowedCharts: PerformanceWidgetSetting[];
  249. chartSetting: PerformanceWidgetSetting;
  250. eventView: EventView;
  251. rowChartSettings: PerformanceWidgetSetting[];
  252. setChartSetting: (setting: PerformanceWidgetSetting) => void;
  253. }) {
  254. const organization = useOrganization();
  255. const menuOptions: SelectOption<PerformanceWidgetSetting>[] = [];
  256. const settingsMap = WIDGET_DEFINITIONS({organization});
  257. for (const setting of allowedCharts) {
  258. const options = settingsMap[setting];
  259. menuOptions.push({
  260. value: setting,
  261. label: options.title,
  262. disabled: setting !== chartSetting && rowChartSettings.includes(setting),
  263. });
  264. }
  265. const chartDefinition = WIDGET_DEFINITIONS({organization})[chartSetting];
  266. function handleWidgetActionChange(value) {
  267. if (value === 'open_in_discover') {
  268. browserHistory.push(
  269. normalizeUrl(getEventViewDiscoverPath(organization, eventView))
  270. );
  271. }
  272. }
  273. return (
  274. <CompositeSelect
  275. trigger={triggerProps => (
  276. <DropdownButton
  277. {...triggerProps}
  278. size="xs"
  279. borderless
  280. showChevron={false}
  281. icon={<IconEllipsis aria-label={t('More')} />}
  282. />
  283. )}
  284. position="bottom-end"
  285. >
  286. <CompositeSelect.Region
  287. label={t('Display')}
  288. options={menuOptions}
  289. value={chartSetting}
  290. onChange={opt => setChartSetting(opt.value)}
  291. />
  292. {chartDefinition.allowsOpenInDiscover && (
  293. <CompositeSelect.Region
  294. label={t('Other')}
  295. options={[{label: t('Open in Discover'), value: 'open_in_discover'}]}
  296. value=""
  297. onChange={opt => handleWidgetActionChange(opt.value)}
  298. />
  299. )}
  300. </CompositeSelect>
  301. );
  302. }
  303. const getEventViewDiscoverPath = (
  304. organization: Organization,
  305. eventView: EventView
  306. ): string => {
  307. const discoverUrlTarget = eventView.getResultsViewUrlTarget(organization.slug);
  308. // The landing page EventView has some additional conditions, but
  309. // `EventView#getResultsViewUrlTarget` omits those! Get them manually
  310. discoverUrlTarget.query.query = eventView.getQueryWithAdditionalConditions();
  311. return `${discoverUrlTarget.pathname}?${qs.stringify(
  312. omit(discoverUrlTarget.query, ['widths']) // Column widths are not useful in this case
  313. )}`;
  314. };
  315. /**
  316. * Constructs an `EventView` that matches a widget's chart definition.
  317. * @param baseEventView Any valid event view. The easiest way to make a new EventView is to clone an existing one, because `EventView#constructor` takes too many abstract arguments
  318. * @param chartDefinition
  319. */
  320. const makeEventViewForWidget = (
  321. baseEventView: EventView,
  322. chartDefinition: ChartDefinition
  323. ): EventView => {
  324. const widgetEventView = baseEventView.clone();
  325. widgetEventView.name = chartDefinition.title;
  326. widgetEventView.yAxis = chartDefinition.fields[0]; // All current widgets only have one field
  327. widgetEventView.display = DisplayModes.PREVIOUS;
  328. widgetEventView.fields = ['transaction', 'project', ...chartDefinition.fields].map(
  329. fieldName => ({field: fieldName}) as Field
  330. );
  331. return widgetEventView;
  332. };
  333. const WidgetContainer = withOrganization(_WidgetContainer);
  334. export default WidgetContainer;