mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
379 lines
12 KiB
TypeScript
379 lines
12 KiB
TypeScript
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;
|