Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Initial multi-select support for events #95

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/components/view/http/http-details-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export const HttpDetailsFooter = inject('rulesStore')(

event: CollectedEvent,
onDelete: (event: CollectedEvent) => void,
onPin: (event: CollectedEvent) => void,
onScrollToEvent: (event: CollectedEvent) => void,
onBuildRuleFromExchange: (event: HttpExchange) => void,
isPaidUser: boolean,
Expand All @@ -107,9 +108,7 @@ export const HttpDetailsFooter = inject('rulesStore')(
/>
<PinButton
pinned={pinned}
onClick={action(() => {
event.pinned = !event.pinned;
})}
onClick={() => props.onPin(event)}
/>
<DeleteButton
pinned={pinned}
Expand Down
3 changes: 3 additions & 0 deletions src/components/view/http/http-details-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class HttpDetailsPane extends React.Component<{

navigate: (path: string) => void,
onDelete: (event: CollectedEvent) => void,
onPin: (event: CollectedEvent) => void,
onScrollToEvent: (event: CollectedEvent) => void,
onBuildRuleFromExchange: (exchange: HttpExchange) => void,

Expand All @@ -83,6 +84,7 @@ export class HttpDetailsPane extends React.Component<{
const {
exchange,
onDelete,
onPin,
onScrollToEvent,
onBuildRuleFromExchange,
uiStore,
Expand Down Expand Up @@ -129,6 +131,7 @@ export class HttpDetailsPane extends React.Component<{
<HttpDetailsFooter
event={exchange}
onDelete={onDelete}
onPin={onPin}
onScrollToEvent={onScrollToEvent}
onBuildRuleFromExchange={onBuildRuleFromExchange}
navigate={navigate}
Expand Down
4 changes: 3 additions & 1 deletion src/components/view/view-event-list-buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const ClearAllButton = observer((props: {

export const ExportAsHarButton = inject('accountStore')(observer((props: {
className?: string,
isMultiSelectEnabled: boolean,
accountStore?: AccountStore,
events: CollectedEvent[]
}) => {
Expand All @@ -45,8 +46,9 @@ export const ExportAsHarButton = inject('accountStore')(observer((props: {
}
disabled={!isPaidUser || props.events.length === 0}
onClick={async () => {
const serialize = props.isMultiSelectEnabled ? props.events.filter(evt=> evt.mulitSelected) : props.events;
const harContent = JSON.stringify(
await generateHar(props.events)
await generateHar(serialize)
);
const filename = `HTTPToolkit_${
dateFns.format(Date.now(), 'YYYY-MM-DD_HH-mm')
Expand Down
3 changes: 2 additions & 1 deletion src/components/view/view-event-list-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const ViewEventListFooter = styled(observer((props: {
onClear: () => void,
onFiltersConsidered: (filters: FilterSet | undefined) => void,
onScrollToEnd: () => void,
isMultiSelectEnabled: boolean,

allEvents: CollectedEvent[],
filteredEvents: CollectedEvent[],
Expand All @@ -87,7 +88,7 @@ export const ViewEventListFooter = styled(observer((props: {
<ButtonsContainer>
<PlayPauseButton />
<ScrollToEndButton onScrollToEnd={props.onScrollToEnd} />
<ExportAsHarButton events={props.filteredEvents} />
<ExportAsHarButton events={props.filteredEvents} isMultiSelectEnabled={props.isMultiSelectEnabled} />
<ImportHarButton />
<ClearAllButton
disabled={props.allEvents.length === 0}
Expand Down
90 changes: 82 additions & 8 deletions src/components/view/view-event-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ import { StatusCode } from '../common/status-code';
import { HEADER_FOOTER_HEIGHT } from './view-event-list-footer';
import { ViewEventContextMenuBuilder } from './view-context-menu-builder';

const USE_MULTI_SELECT_CHECKBOXES=false;// if this is enabled then a checkbox is shown when multi-select is enabled to allow controlling the checked rows that way rather than using the list directly
const MULTI_SELECT_ROW_CLASSNAME="multiSelected";

const SCROLL_BOTTOM_MARGIN = 5; // If you're in the last 5 pixels of the scroll area, we say you're at the bottom

const EmptyStateOverlay = styled(EmptyState)`
Expand All @@ -52,6 +55,8 @@ interface ViewEventListProps {
filteredEvents: CollectedEvent[];
selectedEvent: CollectedEvent | undefined;
isPaused: boolean;
isMultiSelectEnabled: boolean;
onMultiSelectToggled: () => void;

contextMenuBuilder: ViewEventContextMenuBuilder;

Expand Down Expand Up @@ -156,6 +161,15 @@ const Status = styled(Column)`
flex-shrink: 0;
flex-grow: 0;
`;
const MultiSelect = styled(Column)`
flex-basis: 20px;
${(p: { isMultiSelectEnabled: boolean }) => p.isMultiSelectEnabled && USE_MULTI_SELECT_CHECKBOXES ? "margin-right: 25px !important;" : "margin-right: 15px !important;" }
flex-shrink: 0;
margin-left: -20px !important;

title: "Multi-select events";
flex-grow: 0;
`;

const Source = styled(Column)`
flex-basis: 49px;
Expand Down Expand Up @@ -229,6 +243,10 @@ const EventListRow = styled.div`
user-select: none;
cursor: pointer;

&.multiSelected {
background-color: ${p => p.theme.highlightBackground};
color: ${p => p.theme.highlightColor};
}
&.selected {
background-color: ${p => p.theme.highlightBackground};
color: ${p => p.theme.highlightColor};
Expand Down Expand Up @@ -322,6 +340,7 @@ interface EventRowProps extends ListChildComponentProps {
selectedEvent: CollectedEvent | undefined;
events: CollectedEvent[];
contextMenuBuilder: ViewEventContextMenuBuilder;
isMultiSelectEnabled: boolean;
}
}

Expand Down Expand Up @@ -352,6 +371,7 @@ const EventRow = observer((props: EventRowProps) => {
return <ExchangeRow
index={index}
isSelected={isSelected}
isMultiSelectEnabled={props.data.isMultiSelectEnabled}
style={style}
exchange={event}
contextMenuBuilder={contextMenuBuilder}
Expand All @@ -376,15 +396,31 @@ const EventRow = observer((props: EventRowProps) => {
}
});

interface RowCheckboxProps {
checked:boolean;
whenChecked: React.ChangeEventHandler<HTMLInputElement>;
isMultiSelectEnabled: boolean;
}


const RowCheckbox = styled.input.attrs( (props : RowCheckboxProps) => ({
type: "checkbox", checked: props.checked, onChange: props.whenChecked

}))<RowCheckboxProps>`
${props => props.isMultiSelectEnabled && USE_MULTI_SELECT_CHECKBOXES ? `` : `width: 0 !important;`}
`;

const ExchangeRow = inject('uiStore')(observer(({
index,
isSelected,
style,
exchange,
contextMenuBuilder
contextMenuBuilder,
isMultiSelectEnabled
}: {
index: number,
isSelected: boolean,
isMultiSelectEnabled: boolean,
style: {},
exchange: HttpExchange,
contextMenuBuilder: ViewEventContextMenuBuilder
Expand All @@ -403,10 +439,11 @@ const ExchangeRow = inject('uiStore')(observer(({
data-event-id={exchange.id}
tabIndex={isSelected ? 0 : -1}
onContextMenu={contextMenuBuilder.getContextMenuCallback(exchange)}
className={isSelected ? 'selected' : ''}
className={isSelected ? 'selected' : exchange.mulitSelected ? MULTI_SELECT_ROW_CLASSNAME : ''}
style={style}
>
<RowPin pinned={pinned}/>
<RowCheckbox checked={exchange.mulitSelected} whenChecked={exchange.onMultiSelected} isMultiSelectEnabled={isMultiSelectEnabled} />
<RowMarker category={category} title={describeEventCategory(category)} />
<Method pinned={pinned}>{ request.method }</Method>
<Status>
Expand Down Expand Up @@ -492,7 +529,7 @@ const RTCConnectionRow = observer(({
data-event-id={event.id}
tabIndex={isSelected ? 0 : -1}

className={isSelected ? 'selected' : ''}
className={isSelected ? 'selected' : event.mulitSelected ? MULTI_SELECT_ROW_CLASSNAME : ''}
style={style}
>
<RowPin pinned={pinned}/>
Expand Down Expand Up @@ -536,7 +573,7 @@ const RTCStreamRow = observer(({
data-event-id={event.id}
tabIndex={isSelected ? 0 : -1}

className={isSelected ? 'selected' : ''}
className={isSelected ? 'selected' : event.mulitSelected ? MULTI_SELECT_ROW_CLASSNAME : ''}
style={style}
>
<RowPin pinned={pinned}/>
Expand Down Expand Up @@ -604,7 +641,7 @@ const BuiltInApiRow = observer((p: {
tabIndex={p.isSelected ? 0 : -1}

onContextMenu={p.contextMenuBuilder.getContextMenuCallback(p.exchange)}
className={p.isSelected ? 'selected' : ''}
className={p.isSelected ? 'selected' : p.exchange.mulitSelected ? MULTI_SELECT_ROW_CLASSNAME : ''}
style={p.style}
>
<RowPin pinned={pinned}/>
Expand Down Expand Up @@ -659,7 +696,7 @@ const TlsRow = observer((p: {
data-event-id={tlsEvent.id}
tabIndex={p.isSelected ? 0 : -1}

className={p.isSelected ? 'selected' : ''}
className={p.isSelected ? 'selected' : tlsEvent.mulitSelected ? MULTI_SELECT_ROW_CLASSNAME : ''}
style={p.style}
>
{
Expand All @@ -686,12 +723,14 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
return {
selectedEvent: this.props.selectedEvent,
events: this.props.filteredEvents,
isMultiSelectEnabled: this.props.isMultiSelectEnabled,
contextMenuBuilder: this.props.contextMenuBuilder
};
}

private listBodyRef = React.createRef<HTMLDivElement>();
private listRef = React.createRef<List>();
private AreMultipleEventsSelected = false;

private KeyBoundListWindow = observer(
React.forwardRef<HTMLDivElement>(
Expand All @@ -714,6 +753,7 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
return <ListContainer>
<TableHeader>
<MarkerHeader />
<MultiSelect isMultiSelectEnabled={this.props.isMultiSelectEnabled}><input type="Checkbox" onChange={(evt) => this.props.onMultiSelectToggled()} /></MultiSelect>
<Method>Method</Method>
<Status>Status</Status>
<Source>Source</Source>
Expand Down Expand Up @@ -876,20 +916,54 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
const eventIndex = parseInt(ariaRowIndex, 10) - 1;
const event = this.props.filteredEvents[eventIndex];
if (event !== this.props.selectedEvent) {
this.onEventSelected(eventIndex);
this.onEventSelected(eventIndex, mouseEvent);
} else {
// Clicking the selected row deselects it
this.onEventDeselected();
}
}

@action.bound
onEventSelected(index: number) {
onEventSelected(index: number, mouseEvent: React.MouseEvent) {
if (this.props.isMultiSelectEnabled){
const eventIndex = index;
const event = this.props.filteredEvents[eventIndex];
if ( (! USE_MULTI_SELECT_CHECKBOXES || mouseEvent.shiftKey) && ! mouseEvent.ctrlKey && this.AreMultipleEventsSelected){ //if using the checkboxes then only clear otehr checkboxes when shift key is hit
this.props.filteredEvents.forEach(evt => evt.mulitSelected = false);//to increase the perf here we should cache the selected events in a list, then we can uncheck them quickly and clear the list rather than doing this every click
this.AreMultipleEventsSelected=false;
}
if (! USE_MULTI_SELECT_CHECKBOXES){
if (! mouseEvent.ctrlKey){
if (this.props.selectedEvent){
this.props.selectedEvent.mulitSelected = false;
}
event.mulitSelected = true;
}
}


if (mouseEvent.ctrlKey){
event.mulitSelected = ! event.mulitSelected;
this.AreMultipleEventsSelected=true; //even if technically only one is selected we are safe to set this to true it just does the above reset first
}
if (mouseEvent.shiftKey){
this.AreMultipleEventsSelected = true; //even if technically only one is selected we are safe to set this to true it just does the above reset first
if (this.props.selectedEvent){
let curIndex = this.props.filteredEvents.indexOf(this.props.selectedEvent);
for(let x = curIndex < eventIndex ? curIndex : eventIndex; x <= (curIndex < eventIndex ? eventIndex : curIndex); x++){
this.props.filteredEvents[x].mulitSelected = true;
}
}
}
}
this.props.onSelected(this.props.filteredEvents[index]);
}

@action.bound
onEventDeselected() {
if (! USE_MULTI_SELECT_CHECKBOXES && this.props.selectedEvent){
this.props.selectedEvent.mulitSelected = false;
}
this.props.onSelected(undefined);
}

Expand Down
Loading