@@ -52,13 +52,15 @@ interface ViewEventListProps {
52
52
events : ReadonlyArray < CollectedEvent > ;
53
53
filteredEvents : ReadonlyArray < CollectedEvent > ;
54
54
selectedEvent : CollectedEvent | undefined ;
55
+ selectedEventIds : Set < string > ;
55
56
isPaused : boolean ;
56
57
57
58
contextMenuBuilder : ViewEventContextMenuBuilder ;
58
59
uiStore : UiStore ;
59
60
60
61
moveSelection : ( distance : number ) => void ;
61
62
onSelected : ( event : CollectedEvent | undefined ) => void ;
63
+ onEventToggled : ( event : CollectedEvent ) => void ;
62
64
}
63
65
64
66
const ListContainer = styled . div < { role : 'table' } > `
@@ -253,6 +255,18 @@ const EventListRow = styled.div<{ role: 'row' }>`
253
255
}
254
256
}
255
257
258
+ &.multi-selected {
259
+ background-color: ${ p => p . theme . highlightBackground } ;
260
+ border: 2px solid ${ p => p . theme . popColor } ;
261
+ color: ${ p => p . theme . highlightColor } ;
262
+ fill: ${ p => p . theme . highlightColor } ;
263
+ box-sizing: border-box;
264
+ * {
265
+ color: ${ p => p . theme . highlightColor } ;
266
+ fill: ${ p => p . theme . highlightColor } ;
267
+ }
268
+ }
269
+
256
270
&:focus {
257
271
outline: thin dotted ${ p => p . theme . popColor } ;
258
272
}
@@ -330,22 +344,25 @@ export const TableHeaderRow = styled.div<{ role: 'row' }>`
330
344
interface EventRowProps extends ListChildComponentProps {
331
345
data : {
332
346
selectedEvent : CollectedEvent | undefined ;
347
+ selectedEventIds : Set < string > ;
333
348
events : ReadonlyArray < CollectedEvent > ;
334
349
contextMenuBuilder : ViewEventContextMenuBuilder ;
335
350
}
336
351
}
337
352
338
353
const EventRow = observer ( ( props : EventRowProps ) => {
339
354
const { index, style } = props ;
340
- const { events, selectedEvent, contextMenuBuilder } = props . data ;
355
+ const { events, selectedEvent, selectedEventIds , contextMenuBuilder } = props . data ;
341
356
const event = events [ index ] ;
342
357
343
358
const isSelected = ( selectedEvent === event ) ;
359
+ const isMultiSelected = selectedEventIds . has ( event . id ) ;
344
360
345
361
if ( event . isTlsFailure ( ) || event . isTlsTunnel ( ) ) {
346
362
return < TlsRow
347
363
index = { index }
348
364
isSelected = { isSelected }
365
+ isMultiSelected = { isMultiSelected }
349
366
style = { style }
350
367
tlsEvent = { event }
351
368
/> ;
@@ -354,6 +371,7 @@ const EventRow = observer((props: EventRowProps) => {
354
371
return < BuiltInApiRow
355
372
index = { index }
356
373
isSelected = { isSelected }
374
+ isMultiSelected = { isMultiSelected }
357
375
style = { style }
358
376
exchange = { event }
359
377
contextMenuBuilder = { contextMenuBuilder }
@@ -362,6 +380,7 @@ const EventRow = observer((props: EventRowProps) => {
362
380
return < ExchangeRow
363
381
index = { index }
364
382
isSelected = { isSelected }
383
+ isMultiSelected = { isMultiSelected }
365
384
style = { style }
366
385
exchange = { event }
367
386
contextMenuBuilder = { contextMenuBuilder }
@@ -371,13 +390,15 @@ const EventRow = observer((props: EventRowProps) => {
371
390
return < RTCConnectionRow
372
391
index = { index }
373
392
isSelected = { isSelected }
393
+ isMultiSelected = { isMultiSelected }
374
394
style = { style }
375
395
event = { event }
376
396
/> ;
377
397
} else if ( event . isRTCDataChannel ( ) || event . isRTCMediaTrack ( ) ) {
378
398
return < RTCStreamRow
379
399
index = { index }
380
400
isSelected = { isSelected }
401
+ isMultiSelected = { isMultiSelected }
381
402
style = { style }
382
403
event = { event }
383
404
/> ;
@@ -389,12 +410,14 @@ const EventRow = observer((props: EventRowProps) => {
389
410
const ExchangeRow = inject ( 'uiStore' ) ( observer ( ( {
390
411
index,
391
412
isSelected,
413
+ isMultiSelected,
392
414
style,
393
415
exchange,
394
416
contextMenuBuilder
395
417
} : {
396
418
index : number ,
397
419
isSelected : boolean ,
420
+ isMultiSelected : boolean ,
398
421
style : { } ,
399
422
exchange : HttpExchange ,
400
423
contextMenuBuilder : ViewEventContextMenuBuilder
@@ -406,6 +429,8 @@ const ExchangeRow = inject('uiStore')(observer(({
406
429
category
407
430
} = exchange ;
408
431
432
+ const className = isSelected ? 'selected' : isMultiSelected ? 'multi-selected' : '' ;
433
+
409
434
return < TrafficEventListRow
410
435
role = "row"
411
436
aria-label = {
@@ -431,7 +456,7 @@ const ExchangeRow = inject('uiStore')(observer(({
431
456
data-event-id = { exchange . id }
432
457
tabIndex = { isSelected ? 0 : - 1 }
433
458
onContextMenu = { contextMenuBuilder . getContextMenuCallback ( exchange ) }
434
- className = { isSelected ? 'selected' : '' }
459
+ className = { className }
435
460
style = { style }
436
461
>
437
462
< RowPin aria-label = { pinned ? 'Pinned' : undefined } pinned = { pinned } />
@@ -503,16 +528,20 @@ const ConnectedSpinnerIcon = styled(Icon).attrs(() => ({
503
528
const RTCConnectionRow = observer ( ( {
504
529
index,
505
530
isSelected,
531
+ isMultiSelected,
506
532
style,
507
533
event
508
534
} : {
509
535
index : number ,
510
536
isSelected : boolean ,
537
+ isMultiSelected : boolean ,
511
538
style : { } ,
512
539
event : RTCConnection
513
540
} ) => {
514
541
const { category, pinned } = event ;
515
542
543
+ const className = isSelected ? 'selected' : isMultiSelected ? 'multi-selected' : '' ;
544
+
516
545
return < TrafficEventListRow
517
546
role = "row"
518
547
aria-label = {
@@ -530,7 +559,7 @@ const RTCConnectionRow = observer(({
530
559
data-event-id = { event . id }
531
560
tabIndex = { isSelected ? 0 : - 1 }
532
561
533
- className = { isSelected ? 'selected' : '' }
562
+ className = { className }
534
563
style = { style }
535
564
>
536
565
< RowPin pinned = { pinned } />
@@ -557,16 +586,20 @@ const RTCConnectionRow = observer(({
557
586
const RTCStreamRow = observer ( ( {
558
587
index,
559
588
isSelected,
589
+ isMultiSelected,
560
590
style,
561
591
event
562
592
} : {
563
593
index : number ,
564
594
isSelected : boolean ,
595
+ isMultiSelected : boolean ,
565
596
style : { } ,
566
597
event : RTCStream
567
598
} ) => {
568
599
const { category, pinned } = event ;
569
600
601
+ const className = isSelected ? 'selected' : isMultiSelected ? 'multi-selected' : '' ;
602
+
570
603
return < TrafficEventListRow
571
604
role = "row"
572
605
aria-label = {
@@ -598,7 +631,7 @@ const RTCStreamRow = observer(({
598
631
data-event-id = { event . id }
599
632
tabIndex = { isSelected ? 0 : - 1 }
600
633
601
- className = { isSelected ? 'selected' : '' }
634
+ className = { className }
602
635
style = { style }
603
636
>
604
637
< RowPin pinned = { pinned } />
@@ -648,6 +681,7 @@ const BuiltInApiRow = observer((p: {
648
681
index : number ,
649
682
exchange : HttpExchange ,
650
683
isSelected : boolean ,
684
+ isMultiSelected : boolean ,
651
685
style : { } ,
652
686
contextMenuBuilder : ViewEventContextMenuBuilder
653
687
} ) => {
@@ -668,6 +702,8 @@ const BuiltInApiRow = observer((p: {
668
702
. map ( param => `${ param . name } =${ JSON . stringify ( param . value ) } ` )
669
703
. join ( ', ' ) ;
670
704
705
+ const className = p . isSelected ? 'selected' : p . isMultiSelected ? 'multi-selected' : '' ;
706
+
671
707
return < TrafficEventListRow
672
708
role = "row"
673
709
aria-label = {
@@ -688,7 +724,7 @@ const BuiltInApiRow = observer((p: {
688
724
tabIndex = { p . isSelected ? 0 : - 1 }
689
725
690
726
onContextMenu = { p . contextMenuBuilder . getContextMenuCallback ( p . exchange ) }
691
- className = { p . isSelected ? 'selected' : '' }
727
+ className = { className }
692
728
style = { p . style }
693
729
>
694
730
< RowPin pinned = { pinned } />
@@ -712,6 +748,7 @@ const TlsRow = observer((p: {
712
748
index : number ,
713
749
tlsEvent : FailedTlsConnection | TlsTunnel ,
714
750
isSelected : boolean ,
751
+ isMultiSelected : boolean ,
715
752
style : { }
716
753
} ) => {
717
754
const { tlsEvent } = p ;
@@ -728,14 +765,16 @@ const TlsRow = observer((p: {
728
765
729
766
const connectionTarget = tlsEvent . upstreamHostname || 'unknown domain' ;
730
767
768
+ const className = p . isSelected ? 'selected' : p . isMultiSelected ? 'multi-selected' : '' ;
769
+
731
770
return < TlsListRow
732
771
role = "row"
733
772
aria-label = { `${ description } connection to ${ connectionTarget } ` }
734
773
aria-rowindex = { p . index + 1 }
735
774
data-event-id = { tlsEvent . id }
736
775
tabIndex = { p . isSelected ? 0 : - 1 }
737
776
738
- className = { p . isSelected ? 'selected' : '' }
777
+ className = { className }
739
778
style = { p . style }
740
779
>
741
780
{
@@ -761,6 +800,7 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
761
800
@computed get listItemData ( ) : EventRowProps [ 'data' ] {
762
801
return {
763
802
selectedEvent : this . props . selectedEvent ,
803
+ selectedEventIds : this . props . selectedEventIds ,
764
804
events : this . props . filteredEvents ,
765
805
contextMenuBuilder : this . props . contextMenuBuilder
766
806
} ;
@@ -1002,11 +1042,18 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
1002
1042
1003
1043
const eventIndex = parseInt ( ariaRowIndex , 10 ) - 1 ;
1004
1044
const event = this . props . filteredEvents [ eventIndex ] ;
1005
- if ( event !== this . props . selectedEvent ) {
1006
- this . onEventSelected ( eventIndex ) ;
1045
+
1046
+ // Handle multi-selection with Ctrl+Click (or Cmd+Click on Mac)
1047
+ if ( mouseEvent . ctrlKey || mouseEvent . metaKey ) {
1048
+ this . onEventToggled ( event ) ;
1007
1049
} else {
1008
- // Clicking the selected row deselects it
1009
- this . onEventDeselected ( ) ;
1050
+ // Normal single selection behavior
1051
+ if ( event !== this . props . selectedEvent ) {
1052
+ this . onEventSelected ( eventIndex ) ;
1053
+ } else {
1054
+ // Clicking the selected row deselects it
1055
+ this . onEventDeselected ( ) ;
1056
+ }
1010
1057
}
1011
1058
}
1012
1059
@@ -1020,6 +1067,11 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
1020
1067
this . props . onSelected ( undefined ) ;
1021
1068
}
1022
1069
1070
+ @action . bound
1071
+ onEventToggled ( event : CollectedEvent ) {
1072
+ this . props . onEventToggled ( event ) ;
1073
+ }
1074
+
1023
1075
@action . bound
1024
1076
onKeyDown ( event : React . KeyboardEvent < HTMLDivElement > ) {
1025
1077
const { moveSelection } = this . props ;
0 commit comments