widgetContainer.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import {useEffect, useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import omit from 'lodash/omit';
  4. import pick from 'lodash/pick';
  5. import * as qs from 'query-string';
  6. import DropdownButtonV2 from 'sentry/components/dropdownButtonV2';
  7. import CompositeSelect from 'sentry/components/forms/compositeSelect';
  8. import {IconEllipsis} from 'sentry/icons/iconEllipsis';
  9. import {t} from 'sentry/locale';
  10. import {Organization} from 'sentry/types';
  11. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  12. import EventView from 'sentry/utils/discover/eventView';
  13. import {Field} from 'sentry/utils/discover/fields';
  14. import {DisplayModes} from 'sentry/utils/discover/types';
  15. import {usePerformanceDisplayType} from 'sentry/utils/performance/contexts/performanceDisplayContext';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import withOrganization from 'sentry/utils/withOrganization';
  18. import {GenericPerformanceWidgetDataType} from '../types';
  19. import {_setChartSetting, getChartSetting} from '../utils';
  20. import {
  21. ChartDefinition,
  22. PerformanceWidgetSetting,
  23. WIDGET_DEFINITIONS,
  24. } from '../widgetDefinitions';
  25. import {HistogramWidget} from '../widgets/histogramWidget';
  26. import {LineChartListWidget} from '../widgets/lineChartListWidget';
  27. import {SingleFieldAreaWidget} from '../widgets/singleFieldAreaWidget';
  28. import {TrendsWidget} from '../widgets/trendsWidget';
  29. import {VitalWidget} from '../widgets/vitalWidget';
  30. import {ChartRowProps} from './widgetChartRow';
  31. type Props = {
  32. allowedCharts: PerformanceWidgetSetting[];
  33. chartHeight: number;
  34. defaultChartSetting: PerformanceWidgetSetting;
  35. eventView: EventView;
  36. index: number;
  37. organization: Organization;
  38. rowChartSettings: PerformanceWidgetSetting[];
  39. setRowChartSettings: (settings: PerformanceWidgetSetting[]) => void;
  40. withStaticFilters: boolean;
  41. chartColor?: string;
  42. forceDefaultChartSetting?: boolean;
  43. } & ChartRowProps;
  44. function trackChartSettingChange(
  45. previousChartSetting: PerformanceWidgetSetting,
  46. chartSetting: PerformanceWidgetSetting,
  47. fromDefault: boolean,
  48. organization: Organization
  49. ) {
  50. trackAdvancedAnalyticsEvent('performance_views.landingv3.widget.switch', {
  51. organization,
  52. from_widget: previousChartSetting,
  53. to_widget: chartSetting,
  54. from_default: fromDefault,
  55. });
  56. }
  57. const _WidgetContainer = (props: Props) => {
  58. const {
  59. organization,
  60. index,
  61. chartHeight,
  62. allowedCharts,
  63. rowChartSettings,
  64. setRowChartSettings,
  65. ...rest
  66. } = props;
  67. const performanceType = usePerformanceDisplayType();
  68. let _chartSetting = getChartSetting(
  69. index,
  70. chartHeight,
  71. performanceType,
  72. rest.defaultChartSetting,
  73. rest.forceDefaultChartSetting
  74. );
  75. if (!allowedCharts.includes(_chartSetting)) {
  76. _chartSetting = rest.defaultChartSetting;
  77. }
  78. const [chartSetting, setChartSettingState] = useState(_chartSetting);
  79. const setChartSetting = (setting: PerformanceWidgetSetting) => {
  80. if (!props.forceDefaultChartSetting) {
  81. _setChartSetting(index, chartHeight, performanceType, setting);
  82. }
  83. setChartSettingState(setting);
  84. const newSettings = [...rowChartSettings];
  85. newSettings[index] = setting;
  86. setRowChartSettings(newSettings);
  87. trackChartSettingChange(
  88. chartSetting,
  89. setting,
  90. rest.defaultChartSetting === chartSetting,
  91. organization
  92. );
  93. };
  94. useEffect(() => {
  95. setChartSettingState(_chartSetting);
  96. }, [rest.defaultChartSetting]);
  97. const chartDefinition = WIDGET_DEFINITIONS({organization})[chartSetting];
  98. // Construct an EventView that matches this widget's definition. The
  99. // `eventView` from the props is the _landing page_ EventView, which is different
  100. const widgetEventView = makeEventViewForWidget(props.eventView, chartDefinition);
  101. const widgetProps = {
  102. ...chartDefinition,
  103. chartSetting,
  104. chartDefinition,
  105. ContainerActions: containerProps => (
  106. <WidgetContainerActions
  107. {...containerProps}
  108. eventView={widgetEventView}
  109. allowedCharts={props.allowedCharts}
  110. chartSetting={chartSetting}
  111. setChartSetting={setChartSetting}
  112. rowChartSettings={rowChartSettings}
  113. />
  114. ),
  115. };
  116. const passedProps = pick(props, [
  117. 'eventView',
  118. 'location',
  119. 'organization',
  120. 'chartHeight',
  121. 'withStaticFilters',
  122. ]);
  123. switch (widgetProps.dataType) {
  124. case GenericPerformanceWidgetDataType.trends:
  125. return <TrendsWidget {...passedProps} {...widgetProps} />;
  126. case GenericPerformanceWidgetDataType.area:
  127. return <SingleFieldAreaWidget {...passedProps} {...widgetProps} />;
  128. case GenericPerformanceWidgetDataType.vitals:
  129. return <VitalWidget {...passedProps} {...widgetProps} />;
  130. case GenericPerformanceWidgetDataType.line_list:
  131. return <LineChartListWidget {...passedProps} {...widgetProps} />;
  132. case GenericPerformanceWidgetDataType.histogram:
  133. return <HistogramWidget {...passedProps} {...widgetProps} />;
  134. default:
  135. throw new Error(`Widget type "${widgetProps.dataType}" has no implementation.`);
  136. }
  137. };
  138. export const WidgetContainerActions = ({
  139. chartSetting,
  140. eventView,
  141. setChartSetting,
  142. allowedCharts,
  143. rowChartSettings,
  144. }: {
  145. allowedCharts: PerformanceWidgetSetting[];
  146. chartSetting: PerformanceWidgetSetting;
  147. eventView: EventView;
  148. rowChartSettings: PerformanceWidgetSetting[];
  149. setChartSetting: (setting: PerformanceWidgetSetting) => void;
  150. }) => {
  151. const organization = useOrganization();
  152. const menuOptions: React.ComponentProps<
  153. typeof CompositeSelect
  154. >['sections'][number]['options'] = [];
  155. const settingsMap = WIDGET_DEFINITIONS({organization});
  156. for (const setting of allowedCharts) {
  157. const options = settingsMap[setting];
  158. menuOptions.push({
  159. value: setting,
  160. label: options.title,
  161. disabled: setting !== chartSetting && rowChartSettings.includes(setting),
  162. });
  163. }
  164. const chartDefinition = WIDGET_DEFINITIONS({organization})[chartSetting];
  165. function trigger({props, ref}) {
  166. return (
  167. <DropdownButtonV2
  168. ref={ref}
  169. {...props}
  170. size="xs"
  171. borderless
  172. showChevron={false}
  173. icon={<IconEllipsis aria-label={t('More')} />}
  174. />
  175. );
  176. }
  177. function handleWidgetActionChange(value) {
  178. if (value === 'open_in_discover') {
  179. browserHistory.push(getEventViewDiscoverPath(organization, eventView));
  180. }
  181. }
  182. return (
  183. <CompositeSelect
  184. sections={
  185. [
  186. {
  187. label: t('Display'),
  188. options: menuOptions,
  189. value: chartSetting,
  190. onChange: setChartSetting,
  191. },
  192. chartDefinition.allowsOpenInDiscover
  193. ? {
  194. label: t('Other'),
  195. options: [{label: t('Open in Discover'), value: 'open_in_discover'}],
  196. value: '',
  197. onChange: handleWidgetActionChange,
  198. }
  199. : null,
  200. ].filter(Boolean) as React.ComponentProps<typeof CompositeSelect>['sections']
  201. }
  202. isOptionDisabled={opt => opt.disabled}
  203. trigger={trigger}
  204. placement="bottom right"
  205. />
  206. );
  207. };
  208. const getEventViewDiscoverPath = (
  209. organization: Organization,
  210. eventView: EventView
  211. ): string => {
  212. const discoverUrlTarget = eventView.getResultsViewUrlTarget(organization.slug);
  213. // The landing page EventView has some additional conditions, but
  214. // `EventView#getResultsViewUrlTarget` omits those! Get them manually
  215. discoverUrlTarget.query.query = eventView.getQueryWithAdditionalConditions();
  216. return `${discoverUrlTarget.pathname}?${qs.stringify(
  217. omit(discoverUrlTarget.query, ['widths']) // Column widths are not useful in this case
  218. )}`;
  219. };
  220. /**
  221. * Constructs an `EventView` that matches a widget's chart definition.
  222. * @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
  223. * @param chartDefinition
  224. */
  225. const makeEventViewForWidget = (
  226. baseEventView: EventView,
  227. chartDefinition: ChartDefinition
  228. ): EventView => {
  229. const widgetEventView = baseEventView.clone();
  230. widgetEventView.name = chartDefinition.title;
  231. widgetEventView.yAxis = chartDefinition.fields[0]; // All current widgets only have one field
  232. widgetEventView.display = DisplayModes.PREVIOUS;
  233. widgetEventView.fields = ['transaction', 'project', ...chartDefinition.fields].map(
  234. fieldName => ({field: fieldName} as Field)
  235. );
  236. return widgetEventView;
  237. };
  238. const WidgetContainer = withOrganization(_WidgetContainer);
  239. export default WidgetContainer;