widgetContainer.tsx 8.7 KB

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