widgetContainer.tsx 12 KB

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