Skip to content

Commit 7c8502b

Browse files
Copilotpimterry
andcommitted
Implement multi-row selection functionality in view event list
Co-authored-by: pimterry <[email protected]>
1 parent 81096d4 commit 7c8502b

File tree

4 files changed

+154
-11
lines changed

4 files changed

+154
-11
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as React from 'react';
2+
import { observer } from 'mobx-react';
3+
4+
import { styled } from '../../styles';
5+
import { PaneOuterContainer } from './view-details-pane';
6+
7+
interface MultiSelectionPanelProps {
8+
className?: string;
9+
selectedCount: number;
10+
}
11+
12+
const MultiSelectionContent = styled.div`
13+
display: flex;
14+
flex-direction: column;
15+
align-items: center;
16+
justify-content: center;
17+
height: 100%;
18+
padding: 20px;
19+
text-align: center;
20+
color: ${p => p.theme.mainColor};
21+
background-color: ${p => p.theme.mainBackground};
22+
`;
23+
24+
const SelectionCountText = styled.h2`
25+
font-size: 2em;
26+
font-weight: bold;
27+
margin: 0 0 10px 0;
28+
color: ${p => p.theme.popColor};
29+
`;
30+
31+
const SelectionDescription = styled.p`
32+
font-size: 1.2em;
33+
margin: 0;
34+
opacity: 0.7;
35+
`;
36+
37+
export const MultiSelectionPanel = observer(({ className, selectedCount }: MultiSelectionPanelProps) => {
38+
return (
39+
<PaneOuterContainer className={className}>
40+
<MultiSelectionContent>
41+
<SelectionCountText>
42+
{selectedCount}
43+
</SelectionCountText>
44+
<SelectionDescription>
45+
{selectedCount === 1 ? 'event selected' : 'events selected'}
46+
</SelectionDescription>
47+
</MultiSelectionContent>
48+
</PaneOuterContainer>
49+
);
50+
});

src/components/view/view-event-list.tsx

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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

6466
const 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' }>`
330344
interface EventRowProps extends ListChildComponentProps {
331345
data: {
332346
selectedEvent: CollectedEvent | undefined;
347+
selectedEventIds: Set<string>;
333348
events: ReadonlyArray<CollectedEvent>;
334349
contextMenuBuilder: ViewEventContextMenuBuilder;
335350
}
336351
}
337352

338353
const 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) => {
389410
const 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(() => ({
503528
const 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(({
557586
const 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;

src/components/view/view-page.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { TlsTunnelDetailsPane } from './tls/tls-tunnel-details-pane';
4545
import { RTCDataChannelDetailsPane } from './rtc/rtc-data-channel-details-pane';
4646
import { RTCMediaDetailsPane } from './rtc/rtc-media-details-pane';
4747
import { RTCConnectionDetailsPane } from './rtc/rtc-connection-details-pane';
48+
import { MultiSelectionPanel } from './multi-selection-panel';
4849

4950
interface ViewPageProps {
5051
className?: string;
@@ -350,7 +351,12 @@ class ViewPage extends React.Component<ViewPageProps> {
350351
const { filteredEvents, filteredEventCount } = this.filteredEventState;
351352

352353
let rightPane: JSX.Element | null;
353-
if (!this.selectedEvent) {
354+
if (this.props.uiStore.hasMultipleSelectedEvents) {
355+
// Show multi-selection panel
356+
rightPane = <MultiSelectionPanel
357+
selectedCount={this.props.uiStore.selectedEventIds.size}
358+
/>;
359+
} else if (!this.selectedEvent) {
354360
if (this.splitDirection === 'vertical') {
355361
rightPane = <EmptyState key='details' icon='ArrowLeft'>
356362
Select an exchange to see the full details.
@@ -453,10 +459,12 @@ class ViewPage extends React.Component<ViewPageProps> {
453459
events={events}
454460
filteredEvents={filteredEvents}
455461
selectedEvent={this.selectedEvent}
462+
selectedEventIds={this.props.uiStore.selectedEventIds}
456463
isPaused={isPaused}
457464

458465
moveSelection={this.moveSelection}
459466
onSelected={this.onSelected}
467+
onEventToggled={this.onEventToggled}
460468
contextMenuBuilder={this.contextMenuBuilder}
461469
uiStore={this.props.uiStore}
462470

@@ -509,6 +517,11 @@ class ViewPage extends React.Component<ViewPageProps> {
509517
);
510518
}
511519

520+
@action.bound
521+
onEventToggled(event: CollectedEvent) {
522+
this.props.uiStore.toggleEventSelection(event.id);
523+
}
524+
512525
@action.bound
513526
moveSelection(distance: number) {
514527
const { filteredEvents } = this.filteredEventState;

0 commit comments

Comments
 (0)