import {useEffect, useState} from 'react'; import {browserHistory} from 'react-router'; import styled from '@emotion/styled'; import omit from 'lodash/omit'; import pick from 'lodash/pick'; import * as qs from 'query-string'; import type {SelectOption} from 'sentry/components/compactSelect'; import {CompactSelect} from 'sentry/components/compactSelect'; import {CompositeSelect} from 'sentry/components/compactSelect/composite'; import DropdownButton from 'sentry/components/dropdownButton'; import {IconEllipsis} from 'sentry/icons/iconEllipsis'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types'; import {trackAnalytics} from 'sentry/utils/analytics'; import type EventView from 'sentry/utils/discover/eventView'; import type {Field} from 'sentry/utils/discover/fields'; import {DisplayModes} from 'sentry/utils/discover/types'; import {useMEPSettingContext} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import {usePerformanceDisplayType} from 'sentry/utils/performance/contexts/performanceDisplayContext'; import useOrganization from 'sentry/utils/useOrganization'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import withOrganization from 'sentry/utils/withOrganization'; import {PerformanceScoreListWidget} from 'sentry/views/performance/landing/widgets/widgets/performanceScoreListWidget'; import SlowScreens from 'sentry/views/performance/landing/widgets/widgets/slowScreens'; import {GenericPerformanceWidgetDataType} from '../types'; import {_setChartSetting, filterAllowedChartsMetrics, getChartSetting} from '../utils'; import type {ChartDefinition, PerformanceWidgetSetting} from '../widgetDefinitions'; import {WIDGET_DEFINITIONS} from '../widgetDefinitions'; import {HistogramWidget} from '../widgets/histogramWidget'; import {LineChartListWidget} from '../widgets/lineChartListWidget'; import {PerformanceScoreWidget} from '../widgets/performanceScoreWidget'; import {SingleFieldAreaWidget} from '../widgets/singleFieldAreaWidget'; import {StackedAreaChartListWidget} from '../widgets/stackedAreaChartListWidget'; import {TrendsWidget} from '../widgets/trendsWidget'; import {VitalWidget} from '../widgets/vitalWidget'; import type {ChartRowProps} from './widgetChartRow'; interface Props extends ChartRowProps { allowedCharts: PerformanceWidgetSetting[]; chartHeight: number; defaultChartSetting: PerformanceWidgetSetting; eventView: EventView; index: number; organization: Organization; rowChartSettings: PerformanceWidgetSetting[]; setRowChartSettings: (settings: PerformanceWidgetSetting[]) => void; withStaticFilters: boolean; chartColor?: string; forceDefaultChartSetting?: boolean; } function trackChartSettingChange( previousChartSetting: PerformanceWidgetSetting, chartSetting: PerformanceWidgetSetting, fromDefault: boolean, organization: Organization ) { trackAnalytics('performance_views.landingv3.widget.switch', { organization, from_widget: previousChartSetting, to_widget: chartSetting, from_default: fromDefault, is_new_menu: organization.features.includes('performance-new-widget-designs'), }); } function _WidgetContainer(props: Props) { const { organization, index, chartHeight, rowChartSettings, setRowChartSettings, ...rest } = props; const performanceType = usePerformanceDisplayType(); let _chartSetting = getChartSetting( index, chartHeight, performanceType, rest.defaultChartSetting, rest.forceDefaultChartSetting ); const mepSetting = useMEPSettingContext(); const allowedCharts = filterAllowedChartsMetrics( props.organization, props.allowedCharts, mepSetting ); if (!allowedCharts.includes(_chartSetting)) { _chartSetting = rest.defaultChartSetting; } const [chartSetting, setChartSettingState] = useState(_chartSetting); const setChartSetting = (setting: PerformanceWidgetSetting) => { if (!props.forceDefaultChartSetting) { _setChartSetting(index, chartHeight, performanceType, setting); } setChartSettingState(setting); const newSettings = [...rowChartSettings]; newSettings[index] = setting; setRowChartSettings(newSettings); trackChartSettingChange( chartSetting, setting, rest.defaultChartSetting === chartSetting, organization ); }; useEffect(() => { setChartSettingState(_chartSetting); }, [rest.defaultChartSetting, _chartSetting]); const chartDefinition = WIDGET_DEFINITIONS({organization})[chartSetting]; // Construct an EventView that matches this widget's definition. The // `eventView` from the props is the _landing page_ EventView, which is different const widgetEventView = makeEventViewForWidget(props.eventView, chartDefinition); const showNewWidgetDesign = organization.features.includes( 'performance-new-widget-designs' ); const widgetProps = { ...chartDefinition, chartSetting, chartDefinition, InteractiveTitle: showNewWidgetDesign && allowedCharts.length > 2 ? containerProps => ( ) : null, ContainerActions: !showNewWidgetDesign ? containerProps => ( ) : null, }; const passedProps = pick(props, [ 'eventView', 'location', 'organization', 'chartHeight', 'withStaticFilters', ]); const titleTooltip = showNewWidgetDesign ? '' : widgetProps.titleTooltip; switch (widgetProps.dataType) { case GenericPerformanceWidgetDataType.TRENDS: return ( ); case GenericPerformanceWidgetDataType.AREA: return ( ); case GenericPerformanceWidgetDataType.VITALS: return ( ); case GenericPerformanceWidgetDataType.LINE_LIST: return ( ); case GenericPerformanceWidgetDataType.HISTOGRAM: return ( ); case GenericPerformanceWidgetDataType.STACKED_AREA: return ; case GenericPerformanceWidgetDataType.PERFORMANCE_SCORE_LIST: return ; case GenericPerformanceWidgetDataType.PERFORMANCE_SCORE: return ; case GenericPerformanceWidgetDataType.SLOW_SCREENS_BY_TTID: return ; default: throw new Error(`Widget type "${widgetProps.dataType}" has no implementation.`); } } export function WidgetInteractiveTitle({ chartSetting, eventView, setChartSetting, allowedCharts, rowChartSettings, }) { const organization = useOrganization(); const menuOptions: SelectOption[] = []; const settingsMap = WIDGET_DEFINITIONS({organization}); for (const setting of allowedCharts) { const options = settingsMap[setting]; menuOptions.push({ value: setting, label: options.title, disabled: setting !== chartSetting && rowChartSettings.includes(setting), }); } const chartDefinition = WIDGET_DEFINITIONS({organization})[chartSetting]; if (chartDefinition.allowsOpenInDiscover) { menuOptions.push({label: t('Open in Discover'), value: 'open_in_discover'}); } const handleChange = option => { if (option.value === 'open_in_discover') { browserHistory.push( normalizeUrl(getEventViewDiscoverPath(organization, eventView)) ); } else { setChartSetting(option.value); } }; return ( ); } const StyledCompactSelect = styled(CompactSelect)` /* Reset font-weight set by HeaderTitleLegend, buttons are already bold and * setting this higher up causes it to trickle into the menues */ font-weight: normal; margin: -${space(0.5)} -${space(1)} -${space(0.25)}; min-width: 0; button { padding: ${space(0.5)} ${space(1)}; font-size: ${p => p.theme.fontSizeLarge}; } `; export function WidgetContainerActions({ chartSetting, eventView, setChartSetting, allowedCharts, rowChartSettings, }: { allowedCharts: PerformanceWidgetSetting[]; chartSetting: PerformanceWidgetSetting; eventView: EventView; rowChartSettings: PerformanceWidgetSetting[]; setChartSetting: (setting: PerformanceWidgetSetting) => void; }) { const organization = useOrganization(); const menuOptions: SelectOption[] = []; const settingsMap = WIDGET_DEFINITIONS({organization}); for (const setting of allowedCharts) { const options = settingsMap[setting]; menuOptions.push({ value: setting, label: options.title, disabled: setting !== chartSetting && rowChartSettings.includes(setting), }); } const chartDefinition = WIDGET_DEFINITIONS({organization})[chartSetting]; function handleWidgetActionChange(value) { if (value === 'open_in_discover') { browserHistory.push( normalizeUrl(getEventViewDiscoverPath(organization, eventView)) ); } } return ( ( } /> )} position="bottom-end" > setChartSetting(opt.value)} /> {chartDefinition.allowsOpenInDiscover && ( handleWidgetActionChange(opt.value)} /> )} ); } const getEventViewDiscoverPath = ( organization: Organization, eventView: EventView ): string => { const discoverUrlTarget = eventView.getResultsViewUrlTarget(organization.slug); // The landing page EventView has some additional conditions, but // `EventView#getResultsViewUrlTarget` omits those! Get them manually discoverUrlTarget.query.query = eventView.getQueryWithAdditionalConditions(); return `${discoverUrlTarget.pathname}?${qs.stringify( omit(discoverUrlTarget.query, ['widths']) // Column widths are not useful in this case )}`; }; /** * Constructs an `EventView` that matches a widget's chart definition. * @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 * @param chartDefinition */ const makeEventViewForWidget = ( baseEventView: EventView, chartDefinition: ChartDefinition ): EventView => { const widgetEventView = baseEventView.clone(); widgetEventView.name = chartDefinition.title; widgetEventView.yAxis = chartDefinition.fields[0]; // All current widgets only have one field widgetEventView.display = DisplayModes.PREVIOUS; widgetEventView.fields = ['transaction', 'project', ...chartDefinition.fields].map( fieldName => ({field: fieldName}) as Field ); return widgetEventView; }; const WidgetContainer = withOrganization(_WidgetContainer); export default WidgetContainer;