123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- 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 => (
- <WidgetInteractiveTitle
- {...containerProps}
- eventView={widgetEventView}
- allowedCharts={allowedCharts}
- chartSetting={chartSetting}
- setChartSetting={setChartSetting}
- rowChartSettings={rowChartSettings}
- />
- )
- : null,
- ContainerActions: !showNewWidgetDesign
- ? containerProps => (
- <WidgetContainerActions
- {...containerProps}
- eventView={widgetEventView}
- allowedCharts={allowedCharts}
- chartSetting={chartSetting}
- setChartSetting={setChartSetting}
- rowChartSettings={rowChartSettings}
- />
- )
- : null,
- };
- const passedProps = pick(props, [
- 'eventView',
- 'location',
- 'organization',
- 'chartHeight',
- 'withStaticFilters',
- ]);
- const titleTooltip = showNewWidgetDesign ? '' : widgetProps.titleTooltip;
- switch (widgetProps.dataType) {
- case GenericPerformanceWidgetDataType.TRENDS:
- return (
- <TrendsWidget {...passedProps} {...widgetProps} titleTooltip={titleTooltip} />
- );
- case GenericPerformanceWidgetDataType.AREA:
- return (
- <SingleFieldAreaWidget
- {...passedProps}
- {...widgetProps}
- titleTooltip={titleTooltip}
- />
- );
- case GenericPerformanceWidgetDataType.VITALS:
- return (
- <VitalWidget {...passedProps} {...widgetProps} titleTooltip={titleTooltip} />
- );
- case GenericPerformanceWidgetDataType.LINE_LIST:
- return (
- <LineChartListWidget
- {...passedProps}
- {...widgetProps}
- titleTooltip={titleTooltip}
- />
- );
- case GenericPerformanceWidgetDataType.HISTOGRAM:
- return (
- <HistogramWidget {...passedProps} {...widgetProps} titleTooltip={titleTooltip} />
- );
- case GenericPerformanceWidgetDataType.STACKED_AREA:
- return <StackedAreaChartListWidget {...passedProps} {...widgetProps} />;
- case GenericPerformanceWidgetDataType.PERFORMANCE_SCORE_LIST:
- return <PerformanceScoreListWidget {...passedProps} {...widgetProps} />;
- case GenericPerformanceWidgetDataType.PERFORMANCE_SCORE:
- return <PerformanceScoreWidget {...passedProps} {...widgetProps} />;
- case GenericPerformanceWidgetDataType.SLOW_SCREENS_BY_TTID:
- return <SlowScreens {...passedProps} {...widgetProps} />;
- 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<string>[] = [];
- 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 (
- <StyledCompactSelect
- options={menuOptions}
- value={chartSetting}
- onChange={handleChange}
- triggerProps={{borderless: true, size: 'zero'}}
- offset={4}
- />
- );
- }
- 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<PerformanceWidgetSetting>[] = [];
- 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 (
- <CompositeSelect
- trigger={triggerProps => (
- <DropdownButton
- {...triggerProps}
- size="xs"
- borderless
- showChevron={false}
- icon={<IconEllipsis aria-label={t('More')} />}
- />
- )}
- position="bottom-end"
- >
- <CompositeSelect.Region
- label={t('Display')}
- options={menuOptions}
- value={chartSetting}
- onChange={opt => setChartSetting(opt.value)}
- />
- {chartDefinition.allowsOpenInDiscover && (
- <CompositeSelect.Region
- label={t('Other')}
- options={[{label: t('Open in Discover'), value: 'open_in_discover'}]}
- value=""
- onChange={opt => handleWidgetActionChange(opt.value)}
- />
- )}
- </CompositeSelect>
- );
- }
- 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;
|