widgetContainer.tsx 12 KB

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