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;