widgetContainer.tsx 13 KB

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