8
8
} from 'react' ;
9
9
import { useTheme } from '@emotion/react' ;
10
10
import styled from '@emotion/styled' ;
11
- import { useResizeObserver } from '@react-aria/utils' ;
11
+ import { mergeRefs , useResizeObserver } from '@react-aria/utils' ;
12
12
import Color from 'color' ;
13
13
14
14
import { BarChart , type BarChartSeries } from 'sentry/components/charts/barChart' ;
@@ -22,7 +22,7 @@ import InteractionStateLayer from 'sentry/components/interactionStateLayer';
22
22
import Placeholder from 'sentry/components/placeholder' ;
23
23
import { t , tct , tn } from 'sentry/locale' ;
24
24
import { space } from 'sentry/styles/space' ;
25
- import type { SeriesDataUnit } from 'sentry/types/echarts' ;
25
+ import type { ReactEchartsRef , SeriesDataUnit } from 'sentry/types/echarts' ;
26
26
import type { Event } from 'sentry/types/event' ;
27
27
import type { Group } from 'sentry/types/group' ;
28
28
import type { EventsStats , MultiSeriesEventsStats } from 'sentry/types/organization' ;
@@ -33,6 +33,7 @@ import {useApiQuery} from 'sentry/utils/queryClient';
33
33
import { useLocalStorageState } from 'sentry/utils/useLocalStorageState' ;
34
34
import { useLocation } from 'sentry/utils/useLocation' ;
35
35
import useOrganization from 'sentry/utils/useOrganization' ;
36
+ import { useReleaseStats } from 'sentry/utils/useReleaseStats' ;
36
37
import { getBucketSize } from 'sentry/views/dashboards/utils/getBucketSize' ;
37
38
import { useIssueDetails } from 'sentry/views/issueDetails/streamline/context' ;
38
39
import useFlagSeries from 'sentry/views/issueDetails/streamline/hooks/featureFlags/useFlagSeries' ;
@@ -44,6 +45,7 @@ import {
44
45
import { useReleaseMarkLineSeries } from 'sentry/views/issueDetails/streamline/hooks/useReleaseMarkLineSeries' ;
45
46
import { Tab } from 'sentry/views/issueDetails/types' ;
46
47
import { useGroupDetailsRoute } from 'sentry/views/issueDetails/useGroupDetailsRoute' ;
48
+ import { useReleaseBubbles } from 'sentry/views/releases/releaseBubbles/useReleaseBubbles' ;
47
49
48
50
const enum EventGraphSeries {
49
51
EVENT = 'event' ,
@@ -54,6 +56,24 @@ interface EventGraphProps {
54
56
event : Event | undefined ;
55
57
group : Group ;
56
58
className ?: string ;
59
+ /**
60
+ * Disables navigation via router when the chart is zoomed. This is so the
61
+ * release bubbles can zoom in on the chart when it renders and not trigger
62
+ * navigation (which would update the page filters and affect the main
63
+ * chart).
64
+ */
65
+ disableZoomNavigation ?: boolean ;
66
+ ref ?: React . Ref < ReactEchartsRef > ;
67
+ /**
68
+ * Configures showing releases on the chart as bubbles or lines. This is used
69
+ * when showing the chart inside of the flyout drawer. Bubbles are shown when
70
+ * this prop is anything besides "line".
71
+ */
72
+ showReleasesAs ?: 'line' | 'bubble' ;
73
+ /**
74
+ * Enable/disables showing the event and user summary
75
+ */
76
+ showSummary ?: boolean ;
57
77
style ?: CSSProperties ;
58
78
}
59
79
@@ -76,7 +96,15 @@ function createSeriesAndCount(stats: EventsStats) {
76
96
) ;
77
97
}
78
98
79
- export function EventGraph ( { group, event, ...styleProps } : EventGraphProps ) {
99
+ export function EventGraph ( {
100
+ group,
101
+ event,
102
+ disableZoomNavigation = false ,
103
+ showReleasesAs,
104
+ showSummary = true ,
105
+ ref,
106
+ ...styleProps
107
+ } : EventGraphProps ) {
80
108
const theme = useTheme ( ) ;
81
109
const organization = useOrganization ( ) ;
82
110
const chartContainerRef = useRef < HTMLDivElement | null > ( null ) ;
@@ -116,6 +144,8 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
116
144
} ,
117
145
} ) ;
118
146
147
+ const hasReleaseBubblesSeries = organization . features . includes ( 'release-bubbles-ui' ) ;
148
+
119
149
const noQueryEventView = eventView . clone ( ) ;
120
150
noQueryEventView . query = `issue:${ group . shortId } ` ;
121
151
noQueryEventView . environment = [ ] ;
@@ -200,7 +230,75 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
200
230
event,
201
231
group,
202
232
} ) ;
203
- const releaseSeries = useReleaseMarkLineSeries ( { group} ) ;
233
+
234
+ const [ legendSelected , setLegendSelected ] = useLocalStorageState (
235
+ 'issue-details-graph-legend' ,
236
+ {
237
+ [ 'Feature Flags' ] : true ,
238
+ [ 'Releases' ] : false ,
239
+ }
240
+ ) ;
241
+
242
+ const { releases = [ ] } = useReleaseStats (
243
+ {
244
+ projects : eventView . project ,
245
+ environments : eventView . environment ,
246
+ datetime : {
247
+ start : eventView . start ,
248
+ end : eventView . end ,
249
+ period : eventView . statsPeriod ,
250
+ } ,
251
+ } ,
252
+ {
253
+ staleTime : 0 ,
254
+ }
255
+ ) ;
256
+
257
+ const releaseSeries = useReleaseMarkLineSeries ( {
258
+ group,
259
+ releases : hasReleaseBubblesSeries && showReleasesAs !== 'line' ? [ ] : releases ,
260
+ } ) ;
261
+
262
+ const {
263
+ connectReleaseBubbleChartRef,
264
+ releaseBubbleEventHandlers,
265
+ releaseBubbleSeries,
266
+ releaseBubbleXAxis,
267
+ releaseBubbleGrid,
268
+ } = useReleaseBubbles ( {
269
+ chartRenderer : ( { chartRef} ) => {
270
+ return (
271
+ < EventGraph
272
+ ref = { chartRef }
273
+ group = { group }
274
+ event = { event }
275
+ showSummary = { false }
276
+ showReleasesAs = "line"
277
+ disableZoomNavigation
278
+ { ...styleProps }
279
+ />
280
+ ) ;
281
+ } ,
282
+ legendSelected : legendSelected . Releases ,
283
+ desiredBuckets : eventSeries . length ,
284
+ minTime : eventSeries . length && ( eventSeries . at ( 0 ) ?. name as number ) ,
285
+ maxTime : eventSeries . length && ( eventSeries . at ( - 1 ) ?. name as number ) ,
286
+ releases : hasReleaseBubblesSeries && showReleasesAs !== 'line' ? releases : [ ] ,
287
+ projects : eventView . project ,
288
+ environments : eventView . environment ,
289
+ datetime : {
290
+ start : eventView . start ,
291
+ end : eventView . end ,
292
+ period : eventView . statsPeriod ,
293
+ } ,
294
+ } ) ;
295
+
296
+ const handleConnectRef = useCallback (
297
+ ( e : ReactEchartsRef | null ) => {
298
+ connectReleaseBubbleChartRef ( e ) ;
299
+ } ,
300
+ [ connectReleaseBubbleChartRef ]
301
+ ) ;
204
302
const flagSeries = useFlagSeries ( {
205
303
query : {
206
304
start : eventView . start ,
@@ -273,7 +371,7 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
273
371
seriesData . push ( currentEventSeries as BarChartSeries ) ;
274
372
}
275
373
276
- if ( releaseSeries . markLine ) {
374
+ if ( releaseSeries ? .markLine ) {
277
375
seriesData . push ( releaseSeries as BarChartSeries ) ;
278
376
}
279
377
@@ -298,14 +396,6 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
298
396
299
397
const bucketSize = eventSeries ? getBucketSize ( series ) : undefined ;
300
398
301
- const [ legendSelected , setLegendSelected ] = useLocalStorageState (
302
- 'issue-details-graph-legend' ,
303
- {
304
- [ 'Feature Flags' ] : true ,
305
- [ 'Releases' ] : false ,
306
- }
307
- ) ;
308
-
309
399
const legend = Legend ( {
310
400
theme,
311
401
orient : 'horizontal' ,
@@ -363,32 +453,39 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
363
453
364
454
return (
365
455
< GraphWrapper { ...styleProps } >
366
- < SummaryContainer >
367
- < GraphButton
368
- onClick = { ( ) =>
369
- visibleSeries === EventGraphSeries . USER &&
370
- setVisibleSeries ( EventGraphSeries . EVENT )
371
- }
372
- isActive = { visibleSeries === EventGraphSeries . EVENT }
373
- disabled = { visibleSeries === EventGraphSeries . EVENT }
374
- label = { tn ( 'Event' , 'Events' , eventCount ) }
375
- count = { String ( eventCount ) }
376
- />
377
- < GraphButton
378
- onClick = { ( ) =>
379
- visibleSeries === EventGraphSeries . EVENT &&
380
- setVisibleSeries ( EventGraphSeries . USER )
381
- }
382
- isActive = { visibleSeries === EventGraphSeries . USER }
383
- disabled = { visibleSeries === EventGraphSeries . USER }
384
- label = { tn ( 'User' , 'Users' , userCount ) }
385
- count = { String ( userCount ) }
386
- />
387
- </ SummaryContainer >
456
+ { showSummary ? (
457
+ < SummaryContainer >
458
+ < GraphButton
459
+ onClick = { ( ) =>
460
+ visibleSeries === EventGraphSeries . USER &&
461
+ setVisibleSeries ( EventGraphSeries . EVENT )
462
+ }
463
+ isActive = { visibleSeries === EventGraphSeries . EVENT }
464
+ disabled = { visibleSeries === EventGraphSeries . EVENT }
465
+ label = { tn ( 'Event' , 'Events' , eventCount ) }
466
+ count = { String ( eventCount ) }
467
+ />
468
+ < GraphButton
469
+ onClick = { ( ) =>
470
+ visibleSeries === EventGraphSeries . EVENT &&
471
+ setVisibleSeries ( EventGraphSeries . USER )
472
+ }
473
+ isActive = { visibleSeries === EventGraphSeries . USER }
474
+ disabled = { visibleSeries === EventGraphSeries . USER }
475
+ label = { tn ( 'User' , 'Users' , userCount ) }
476
+ count = { String ( userCount ) }
477
+ />
478
+ </ SummaryContainer >
479
+ ) : (
480
+ < div />
481
+ ) }
388
482
< ChartContainer role = "figure" ref = { chartContainerRef } >
389
483
< BarChart
484
+ { ...releaseBubbleEventHandlers }
485
+ ref = { mergeRefs ( ref , handleConnectRef ) }
390
486
height = { 100 }
391
487
series = { series }
488
+ additionalSeries = { releaseBubbleSeries ? [ releaseBubbleSeries ] : [ ] }
392
489
legend = { legend }
393
490
onLegendSelectChanged = { onLegendSelectChanged }
394
491
showTimeInTooltip
@@ -397,6 +494,7 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
397
494
right : 8 ,
398
495
top : 20 ,
399
496
bottom : 0 ,
497
+ ...releaseBubbleGrid ,
400
498
} }
401
499
tooltip = { {
402
500
formatAxisLabel : (
@@ -428,7 +526,15 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
428
526
} ,
429
527
} ,
430
528
} }
431
- { ...chartZoomProps }
529
+ xAxis = { {
530
+ ...releaseBubbleXAxis ,
531
+ } }
532
+ { ...( disableZoomNavigation
533
+ ? {
534
+ isGroupedByDate : true ,
535
+ dataZoom : chartZoomProps . dataZoom ,
536
+ }
537
+ : chartZoomProps ) }
432
538
/>
433
539
</ ChartContainer >
434
540
</ GraphWrapper >
0 commit comments