import {useMemo, useState} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {LinkButton} from 'sentry/components/button'; import {BarChart, type BarChartSeries} from 'sentry/components/charts/barChart'; import Legend from 'sentry/components/charts/components/legend'; import {useChartZoom} from 'sentry/components/charts/useChartZoom'; import InteractionStateLayer from 'sentry/components/interactionStateLayer'; import {IconTelescope} from 'sentry/icons'; import {t, tn} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {SeriesDataUnit} from 'sentry/types/echarts'; import type {Event} from 'sentry/types/event'; import type {Group} from 'sentry/types/group'; import type {EventsStats, MultiSeriesEventsStats} from 'sentry/types/organization'; import {SavedQueryDatasets} from 'sentry/utils/discover/types'; import {formatAbbreviatedNumber} from 'sentry/utils/formatters'; import useOrganization from 'sentry/utils/useOrganization'; import {hasDatasetSelector} from 'sentry/views/dashboards/utils'; import useFlagSeries from 'sentry/views/issueDetails/streamline/flagSeries'; import {useIssueDetailsEventView} from 'sentry/views/issueDetails/streamline/useIssueDetailsDiscoverQuery'; export const enum EventGraphSeries { EVENT = 'event', USER = 'user', } interface EventGraphProps { event: Event; group: Group; groupStats: MultiSeriesEventsStats; searchQuery: string; } function createSeriesAndCount(stats: EventsStats) { return stats?.data?.reduce( (result, [timestamp, countData]) => { const count = countData?.[0]?.count ?? 0; return { series: [ ...result.series, { name: timestamp * 1000, // ms -> s value: count, }, ], count: result.count + count, }; }, {series: [] as SeriesDataUnit[], count: 0} ); } export function EventGraph({group, groupStats, searchQuery, event}: EventGraphProps) { const theme = useTheme(); const organization = useOrganization(); const [visibleSeries, setVisibleSeries] = useState( EventGraphSeries.EVENT ); const [isGraphHovered, setIsGraphHovered] = useState(false); const eventStats = groupStats['count()']; const {series: eventSeries, count: eventCount} = useMemo( () => createSeriesAndCount(eventStats), [eventStats] ); const userStats = groupStats['count_unique(user)']; const {series: userSeries, count: userCount} = useMemo( () => createSeriesAndCount(userStats), [userStats] ); const eventView = useIssueDetailsEventView({group, queryProps: {query: searchQuery}}); const discoverUrl = eventView.getResultsViewUrlTarget( organization.slug, false, hasDatasetSelector(organization) ? SavedQueryDatasets.ERRORS : undefined ); const chartZoomProps = useChartZoom({ saveOnZoom: true, }); const flagSeries = useFlagSeries({ query: { start: eventView.start, end: eventView.end, statsPeriod: eventView.statsPeriod, }, event, }); const series = useMemo((): BarChartSeries[] => { const seriesData: BarChartSeries[] = []; if (eventStats && visibleSeries === EventGraphSeries.USER) { seriesData.push({ seriesName: t('Users'), itemStyle: { borderRadius: [2, 2, 0, 0], borderColor: theme.translucentGray200, color: theme.purple200, }, stack: 'stats', data: userSeries, }); } if (eventStats && visibleSeries === EventGraphSeries.EVENT) { seriesData.push({ seriesName: t('Events'), itemStyle: { borderRadius: [2, 2, 0, 0], borderColor: theme.translucentGray200, color: theme.gray200, }, stack: 'stats', data: eventSeries, }); } if (flagSeries.markLine) { seriesData.push(flagSeries as BarChartSeries); } return seriesData; }, [eventStats, visibleSeries, userSeries, eventSeries, flagSeries, theme]); const [legendSelected, setLegendSelected] = useState({ ['Feature Flags']: true, }); const legend = Legend({ theme: theme, icon: 'path://M 10 10 H 500 V 9000 H 10 L 10 10', orient: 'horizontal', align: 'left', show: true, right: 35, top: 5, data: ['Feature Flags'], selected: legendSelected, }); const onLegendSelectChanged = useMemo( () => ({name, selected: record}) => { const newValue = record[name]; setLegendSelected(prevState => ({ ...prevState, [name]: newValue, })); }, [] ); return ( visibleSeries === EventGraphSeries.USER && setVisibleSeries(EventGraphSeries.EVENT) } isActive={visibleSeries === EventGraphSeries.EVENT} disabled={visibleSeries === EventGraphSeries.EVENT} > visibleSeries === EventGraphSeries.EVENT && setVisibleSeries(EventGraphSeries.USER) } isActive={visibleSeries === EventGraphSeries.USER} disabled={visibleSeries === EventGraphSeries.USER} > setIsGraphHovered(true)} onMouseLeave={() => setIsGraphHovered(false)} > { return formatAbbreviatedNumber(value); }, }, }} {...chartZoomProps} /> {discoverUrl && isGraphHovered && ( } to={discoverUrl} aria-label={t('Open in Discover')} title={t('Open in Discover')} /> )} ); } const GraphWrapper = styled('div')` display: grid; grid-template-columns: auto 1fr; `; const SummaryContainer = styled('div')` display: flex; flex-direction: column; margin-right: space(1); border-radius: ${p => p.theme.borderRadiusLeft}; `; const Callout = styled('button')<{isActive: boolean}>` flex: 1; cursor: ${p => (p.isActive ? 'initial' : 'pointer')}; outline: 0; position: relative; border: 1px solid ${p => p.theme.translucentInnerBorder}; background: ${p => (p.isActive ? p.theme.background : p.theme.backgroundSecondary)}; text-align: left; padding: ${space(1)} ${space(2)}; &:first-child { border-radius: ${p => p.theme.borderRadius} 0 ${p => p.theme.borderRadius} 0; border-width: ${p => (p.isActive ? '0' : '0 1px 1px 0')}; } &:last-child { border-radius: 0 ${p => p.theme.borderRadius} 0 ${p => p.theme.borderRadius}; border-width: ${p => (p.isActive ? '0' : '1px 1px 0 0')}; } `; const Label = styled('div')` font-size: ${p => p.theme.fontSizeSmall}; color: ${p => p.theme.subText}; font-weight: ${p => p.theme.fontWeightBold}; line-height: 1; `; const Count = styled('div')` font-size: ${p => p.theme.headerFontSize}; margin-top: ${space(0.5)}; line-height: 1; `; const ChartContainer = styled('div')` padding: ${space(0.75)} ${space(1)} ${space(0.75)} 0; position: relative; `; const OpenInDiscoverButton = styled('div')` position: absolute; top: ${space(1)}; right: ${space(1)}; `;