@@ -52,13 +52,15 @@ interface ViewEventListProps {
5252    events : ReadonlyArray < CollectedEvent > ; 
5353    filteredEvents : ReadonlyArray < CollectedEvent > ; 
5454    selectedEvent : CollectedEvent  |  undefined ; 
55+     selectedEventIds : Set < string > ; 
5556    isPaused : boolean ; 
5657
5758    contextMenuBuilder : ViewEventContextMenuBuilder ; 
5859    uiStore : UiStore ; 
5960
6061    moveSelection : ( distance : number )  =>  void ; 
6162    onSelected : ( event : CollectedEvent  |  undefined )  =>  void ; 
63+     onEventToggled : ( event : CollectedEvent )  =>  void ; 
6264} 
6365
6466const  ListContainer  =  styled . div < {  role : 'table'  } > ` 
@@ -253,6 +255,18 @@ const EventListRow = styled.div<{ role: 'row' }>`
253255        } 
254256    } 
255257
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+ 
256270    &:focus { 
257271        outline: thin dotted ${ p  =>  p . theme . popColor }  ; 
258272    } 
@@ -330,22 +344,25 @@ export const TableHeaderRow = styled.div<{ role: 'row' }>`
330344interface  EventRowProps  extends  ListChildComponentProps  { 
331345    data : { 
332346        selectedEvent : CollectedEvent  |  undefined ; 
347+         selectedEventIds : Set < string > ; 
333348        events : ReadonlyArray < CollectedEvent > ; 
334349        contextMenuBuilder : ViewEventContextMenuBuilder ; 
335350    } 
336351} 
337352
338353const  EventRow  =  observer ( ( props : EventRowProps )  =>  { 
339354    const  {  index,  style }  =  props ; 
340-     const  {  events,  selectedEvent,  contextMenuBuilder }  =  props . data ; 
355+     const  {  events,  selectedEvent,  selectedEventIds ,   contextMenuBuilder }  =  props . data ; 
341356    const  event  =  events [ index ] ; 
342357
343358    const  isSelected  =  ( selectedEvent  ===  event ) ; 
359+     const  isMultiSelected  =  selectedEventIds . has ( event . id ) ; 
344360
345361    if  ( event . isTlsFailure ( )  ||  event . isTlsTunnel ( ) )  { 
346362        return  < TlsRow 
347363            index = { index } 
348364            isSelected = { isSelected } 
365+             isMultiSelected = { isMultiSelected } 
349366            style = { style } 
350367            tlsEvent = { event } 
351368        /> ; 
@@ -354,6 +371,7 @@ const EventRow = observer((props: EventRowProps) => {
354371            return  < BuiltInApiRow 
355372                index = { index } 
356373                isSelected = { isSelected } 
374+                 isMultiSelected = { isMultiSelected } 
357375                style = { style } 
358376                exchange = { event } 
359377                contextMenuBuilder = { contextMenuBuilder } 
@@ -362,6 +380,7 @@ const EventRow = observer((props: EventRowProps) => {
362380            return  < ExchangeRow 
363381                index = { index } 
364382                isSelected = { isSelected } 
383+                 isMultiSelected = { isMultiSelected } 
365384                style = { style } 
366385                exchange = { event } 
367386                contextMenuBuilder = { contextMenuBuilder } 
@@ -371,13 +390,15 @@ const EventRow = observer((props: EventRowProps) => {
371390        return  < RTCConnectionRow 
372391            index = { index } 
373392            isSelected = { isSelected } 
393+             isMultiSelected = { isMultiSelected } 
374394            style = { style } 
375395            event = { event } 
376396        /> ; 
377397    }  else  if  ( event . isRTCDataChannel ( )  ||  event . isRTCMediaTrack ( ) )  { 
378398        return  < RTCStreamRow 
379399            index = { index } 
380400            isSelected = { isSelected } 
401+             isMultiSelected = { isMultiSelected } 
381402            style = { style } 
382403            event = { event } 
383404        /> ; 
@@ -389,12 +410,14 @@ const EventRow = observer((props: EventRowProps) => {
389410const  ExchangeRow  =  inject ( 'uiStore' ) ( observer ( ( { 
390411    index, 
391412    isSelected, 
413+     isMultiSelected, 
392414    style, 
393415    exchange, 
394416    contextMenuBuilder
395417} : { 
396418    index : number , 
397419    isSelected : boolean , 
420+     isMultiSelected : boolean , 
398421    style : { } , 
399422    exchange : HttpExchange , 
400423    contextMenuBuilder : ViewEventContextMenuBuilder 
@@ -406,6 +429,8 @@ const ExchangeRow = inject('uiStore')(observer(({
406429        category
407430    }  =  exchange ; 
408431
432+     const  className  =  isSelected  ? 'selected'  : isMultiSelected  ? 'multi-selected'  : '' ; 
433+ 
409434    return  < TrafficEventListRow 
410435        role = "row" 
411436        aria-label = { 
@@ -431,7 +456,7 @@ const ExchangeRow = inject('uiStore')(observer(({
431456        data-event-id = { exchange . id } 
432457        tabIndex = { isSelected  ? 0  : - 1 } 
433458        onContextMenu = { contextMenuBuilder . getContextMenuCallback ( exchange ) } 
434-         className = { isSelected  ?  'selected'  :  '' } 
459+         className = { className } 
435460        style = { style } 
436461    > 
437462        < RowPin  aria-label = { pinned  ? 'Pinned'  : undefined }  pinned = { pinned } /> 
@@ -503,16 +528,20 @@ const ConnectedSpinnerIcon = styled(Icon).attrs(() => ({
503528const  RTCConnectionRow  =  observer ( ( { 
504529    index, 
505530    isSelected, 
531+     isMultiSelected, 
506532    style, 
507533    event
508534} : { 
509535    index : number , 
510536    isSelected : boolean , 
537+     isMultiSelected : boolean , 
511538    style : { } , 
512539    event : RTCConnection 
513540} )  =>  { 
514541    const  {  category,  pinned }  =  event ; 
515542
543+     const  className  =  isSelected  ? 'selected'  : isMultiSelected  ? 'multi-selected'  : '' ; 
544+ 
516545    return  < TrafficEventListRow 
517546        role = "row" 
518547        aria-label = { 
@@ -530,7 +559,7 @@ const RTCConnectionRow = observer(({
530559        data-event-id = { event . id } 
531560        tabIndex = { isSelected  ? 0  : - 1 } 
532561
533-         className = { isSelected  ?  'selected'  :  '' } 
562+         className = { className } 
534563        style = { style } 
535564    > 
536565        < RowPin  pinned = { pinned } /> 
@@ -557,16 +586,20 @@ const RTCConnectionRow = observer(({
557586const  RTCStreamRow  =  observer ( ( { 
558587    index, 
559588    isSelected, 
589+     isMultiSelected, 
560590    style, 
561591    event
562592} : { 
563593    index : number , 
564594    isSelected : boolean , 
595+     isMultiSelected : boolean , 
565596    style : { } , 
566597    event : RTCStream 
567598} )  =>  { 
568599    const  {  category,  pinned }  =  event ; 
569600
601+     const  className  =  isSelected  ? 'selected'  : isMultiSelected  ? 'multi-selected'  : '' ; 
602+ 
570603    return  < TrafficEventListRow 
571604        role = "row" 
572605        aria-label = { 
@@ -598,7 +631,7 @@ const RTCStreamRow = observer(({
598631        data-event-id = { event . id } 
599632        tabIndex = { isSelected  ? 0  : - 1 } 
600633
601-         className = { isSelected  ?  'selected'  :  '' } 
634+         className = { className } 
602635        style = { style } 
603636    > 
604637        < RowPin  pinned = { pinned } /> 
@@ -648,6 +681,7 @@ const BuiltInApiRow = observer((p: {
648681    index : number , 
649682    exchange : HttpExchange , 
650683    isSelected : boolean , 
684+     isMultiSelected : boolean , 
651685    style : { } , 
652686    contextMenuBuilder : ViewEventContextMenuBuilder 
653687} )  =>  { 
@@ -668,6 +702,8 @@ const BuiltInApiRow = observer((p: {
668702        . map ( param  =>  `${ param . name }  =${ JSON . stringify ( param . value ) }  ` ) 
669703        . join ( ', ' ) ; 
670704
705+     const  className  =  p . isSelected  ? 'selected'  : p . isMultiSelected  ? 'multi-selected'  : '' ; 
706+ 
671707    return  < TrafficEventListRow 
672708        role = "row" 
673709        aria-label = { 
@@ -688,7 +724,7 @@ const BuiltInApiRow = observer((p: {
688724        tabIndex = { p . isSelected  ? 0  : - 1 } 
689725
690726        onContextMenu = { p . contextMenuBuilder . getContextMenuCallback ( p . exchange ) } 
691-         className = { p . isSelected  ?  'selected'  :  '' } 
727+         className = { className } 
692728        style = { p . style } 
693729    > 
694730        < RowPin  pinned = { pinned } /> 
@@ -712,6 +748,7 @@ const TlsRow = observer((p: {
712748    index : number , 
713749    tlsEvent : FailedTlsConnection  |  TlsTunnel , 
714750    isSelected : boolean , 
751+     isMultiSelected : boolean , 
715752    style : { } 
716753} )  =>  { 
717754    const  {  tlsEvent }  =  p ; 
@@ -728,14 +765,16 @@ const TlsRow = observer((p: {
728765
729766    const  connectionTarget  =  tlsEvent . upstreamHostname  ||  'unknown domain' ; 
730767
768+     const  className  =  p . isSelected  ? 'selected'  : p . isMultiSelected  ? 'multi-selected'  : '' ; 
769+ 
731770    return  < TlsListRow 
732771        role = "row" 
733772        aria-label = { `${ description }   connection to ${ connectionTarget }  ` } 
734773        aria-rowindex = { p . index  +  1 } 
735774        data-event-id = { tlsEvent . id } 
736775        tabIndex = { p . isSelected  ? 0  : - 1 } 
737776
738-         className = { p . isSelected  ?  'selected'  :  '' } 
777+         className = { className } 
739778        style = { p . style } 
740779    > 
741780        { 
@@ -761,6 +800,7 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
761800    @computed  get  listItemData ( ) : EventRowProps [ 'data' ]  { 
762801        return  { 
763802            selectedEvent : this . props . selectedEvent , 
803+             selectedEventIds : this . props . selectedEventIds , 
764804            events : this . props . filteredEvents , 
765805            contextMenuBuilder : this . props . contextMenuBuilder 
766806        } ; 
@@ -1002,11 +1042,18 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
10021042
10031043        const  eventIndex  =  parseInt ( ariaRowIndex ,  10 )  -  1 ; 
10041044        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 ) ; 
10071049        }  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+             } 
10101057        } 
10111058    } 
10121059
@@ -1020,6 +1067,11 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
10201067        this . props . onSelected ( undefined ) ; 
10211068    } 
10221069
1070+     @action . bound 
1071+     onEventToggled ( event : CollectedEvent )  { 
1072+         this . props . onEventToggled ( event ) ; 
1073+     } 
1074+ 
10231075    @action . bound 
10241076    onKeyDown ( event : React . KeyboardEvent < HTMLDivElement > )  { 
10251077        const  {  moveSelection }  =  this . props ; 
0 commit comments