From 75aa1c01717dd85967aa66663f4f26a71190688c Mon Sep 17 00:00:00 2001 From: Neel Gondalia Date: Fri, 1 Dec 2023 16:59:05 -0500 Subject: [PATCH] Add support for multi row select and context menu in Timegraph Component changes made: - Support for adding context menu via signals - Support for making multiple selections in data tree of timegraph component - Added Signals to notify context menu selection and multi row selections This change will allow for a context menu to be added to the timegraph component based on the outputDescriptor id via a signal. The component also adds the ability to select multiple rows. The multi selections can be passed with the item clicked signal payload as well as through a rowSelectionsChanged signal. The end goal is to provide the timegraph views with the ability to make multi-row selections and perform some actions with the selections. Signed-off-by: Neel Gondalia ngondalia@blackberry.com --- ...ontext-menu-contributed-signal-payload.tsx | 41 +++ ...ntext-menu-item-clicked-signal-payload.tsx | 35 +++ .../row-selections-changed-signal-payload.tsx | 33 +++ packages/base/src/signals/signal-manager.ts | 21 +- .../components/timegraph-output-component.tsx | 268 +++++++++++++++++- .../utils/filter-tree/entry-tree.tsx | 5 +- .../utils/filter-tree/table-body.tsx | 2 + .../utils/filter-tree/table-row.tsx | 41 ++- .../components/utils/filter-tree/table.tsx | 2 + .../src/components/utils/filter-tree/tree.tsx | 4 + .../time-graph-navigation-shortcuts-table.tsx | 10 + .../style/output-components-style.css | 4 + .../style/react-contextify.css | 4 +- 13 files changed, 449 insertions(+), 21 deletions(-) create mode 100644 packages/base/src/signals/context-menu-contributed-signal-payload.tsx create mode 100644 packages/base/src/signals/context-menu-item-clicked-signal-payload.tsx create mode 100644 packages/base/src/signals/row-selections-changed-signal-payload.tsx diff --git a/packages/base/src/signals/context-menu-contributed-signal-payload.tsx b/packages/base/src/signals/context-menu-contributed-signal-payload.tsx new file mode 100644 index 000000000..e94bf8e2a --- /dev/null +++ b/packages/base/src/signals/context-menu-contributed-signal-payload.tsx @@ -0,0 +1,41 @@ +/*************************************************************************************** + * Copyright (c) 2024 BlackBerry Limited and contributors. + * + * Licensed under the MIT license. See LICENSE file in the project root for details. + ***************************************************************************************/ +export interface MenuItem { + id: string; + label: string; + // Parent Menu that this item belongs to - undefined indicates root menu item + parentMenuId?: string; +} + +export interface SubMenu { + id: string; + label: string; + items: MenuItem[]; + submenu: SubMenu | undefined; +} + +export interface ContextMenuItems { + submenus: SubMenu[]; + items: MenuItem[]; +} + +export class ContextMenuContributedSignalPayload { + private outputDescriptorId: string; + private menuItems: ContextMenuItems; + + constructor(descriptorId: string, menuItems: ContextMenuItems) { + this.outputDescriptorId = descriptorId; + this.menuItems = menuItems; + } + + public getOutputDescriptorId(): string { + return this.outputDescriptorId; + } + + public getMenuItems(): ContextMenuItems { + return this.menuItems; + } +} diff --git a/packages/base/src/signals/context-menu-item-clicked-signal-payload.tsx b/packages/base/src/signals/context-menu-item-clicked-signal-payload.tsx new file mode 100644 index 000000000..6a680b6de --- /dev/null +++ b/packages/base/src/signals/context-menu-item-clicked-signal-payload.tsx @@ -0,0 +1,35 @@ +/*************************************************************************************** + * Copyright (c) 2024 BlackBerry Limited and contributors. + * + * Licensed under the MIT license. See LICENSE file in the project root for details. + ***************************************************************************************/ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export class ContextMenuItemClickedSignalPayload { + private outputDescriptorId: string; + private itemId: string; + private parentMenuId: string | undefined; + private props: { [key: string]: any }; + + constructor(descriptorId: string, itemId: string, props: { [key: string]: any }, parentMenuId?: string) { + this.outputDescriptorId = descriptorId; + this.itemId = itemId; + this.props = props; + this.parentMenuId = parentMenuId; + } + + public getOutputDescriptorId(): string { + return this.outputDescriptorId; + } + + public getItemId(): string { + return this.itemId; + } + + public getProps(): { [key: string]: any } { + return this.props; + } + + public getParentMenuId(): string | undefined { + return this.parentMenuId; + } +} diff --git a/packages/base/src/signals/row-selections-changed-signal-payload.tsx b/packages/base/src/signals/row-selections-changed-signal-payload.tsx new file mode 100644 index 000000000..d15d437a6 --- /dev/null +++ b/packages/base/src/signals/row-selections-changed-signal-payload.tsx @@ -0,0 +1,33 @@ +/*************************************************************************************** + * Copyright (c) 2024 BlackBerry Limited and contributors. + * + * Licensed under the MIT license. See LICENSE file in the project root for details. + ***************************************************************************************/ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export class RowSelectionsChangedSignalPayload { + private traceId: string; + private outputDescriptorId: string; + private rows: { id: number; parentId?: number; metadata?: { [key: string]: any } }[]; + + constructor( + traceId: string, + descriptorId: string, + rows: { id: number; parentId?: number; metadata?: { [key: string]: any } }[] + ) { + this.outputDescriptorId = descriptorId; + this.traceId = traceId; + this.rows = rows; + } + + public getOutputDescriptorId(): string { + return this.outputDescriptorId; + } + + public getTraceId(): string { + return this.traceId; + } + + public getRows(): { id: number; parentId?: number; metadata?: { [key: string]: any } }[] { + return this.rows; + } +} diff --git a/packages/base/src/signals/signal-manager.ts b/packages/base/src/signals/signal-manager.ts index 2cd3da831..1ffd23344 100644 --- a/packages/base/src/signals/signal-manager.ts +++ b/packages/base/src/signals/signal-manager.ts @@ -5,6 +5,9 @@ import { Trace } from 'tsp-typescript-client/lib/models/trace'; import { OpenedTracesUpdatedSignalPayload } from './opened-traces-updated-signal-payload'; import { OutputAddedSignalPayload } from './output-added-signal-payload'; import { TimeRangeUpdatePayload } from './time-range-data-signal-payloads'; +import { ContextMenuContributedSignalPayload } from './context-menu-contributed-signal-payload'; +import { ContextMenuItemClickedSignalPayload } from './context-menu-item-clicked-signal-payload'; +import { RowSelectionsChangedSignalPayload } from './row-selections-changed-signal-payload'; export declare interface SignalManager { fireTraceOpenedSignal(trace: Trace): void; @@ -20,6 +23,7 @@ export declare interface SignalManager { fireThemeChangedSignal(theme: string): void; // TODO - Refactor or remove this signal. Similar signal to fireRequestSelectionRangeChange fireSelectionChangedSignal(payload: { [key: string]: string }): void; + fireRowSelectionsChanged(payload: RowSelectionsChangedSignalPayload): void; fireCloseTraceViewerTabSignal(traceUUID: string): void; fireTraceViewerTabActivatedSignal(experiment: Experiment): void; fireUpdateZoomSignal(hasZoomedIn: boolean): void; @@ -41,6 +45,8 @@ export declare interface SignalManager { fireSelectionRangeUpdated(payload: TimeRangeUpdatePayload): void; fireViewRangeUpdated(payload: TimeRangeUpdatePayload): void; fireRequestSelectionRangeChange(payload: TimeRangeUpdatePayload): void; + fireContributeContextMenu(payload: ContextMenuContributedSignalPayload): void; + fireContextMenuItemClicked(payload: ContextMenuItemClickedSignalPayload): void; } export const Signals = { @@ -57,6 +63,7 @@ export const Signals = { ITEM_PROPERTIES_UPDATED: 'item properties updated', THEME_CHANGED: 'theme changed', SELECTION_CHANGED: 'selection changed', + ROW_SELECTIONS_CHANGED: 'rows selected changed', CLOSE_TRACEVIEWERTAB: 'tab closed', TRACEVIEWERTAB_ACTIVATED: 'widget activated', UPDATE_ZOOM: 'update zoom', @@ -75,7 +82,9 @@ export const Signals = { VIEW_RANGE_UPDATED: 'view range updated', SELECTION_RANGE_UPDATED: 'selection range updated', REQUEST_SELECTION_RANGE_CHANGE: 'change selection range', - OUTPUT_DATA_CHANGED: 'output data changed' + OUTPUT_DATA_CHANGED: 'output data changed', + CONTRIBUTE_CONTEXT_MENU: 'contribute context menu', + CONTEXT_MENU_ITEM_CLICKED: 'context menu item clicked' }; export class SignalManager extends EventEmitter implements SignalManager { @@ -97,6 +106,10 @@ export class SignalManager extends EventEmitter implements SignalManager { fireExperimentSelectedSignal(experiment: Experiment | undefined): void { this.emit(Signals.EXPERIMENT_SELECTED, experiment); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fireRowSelectionsChanged(payload: RowSelectionsChangedSignalPayload): void { + this.emit(Signals.ROW_SELECTIONS_CHANGED, payload); + } fireExperimentUpdatedSignal(experiment: Experiment): void { this.emit(Signals.EXPERIMENT_UPDATED, experiment); } @@ -174,6 +187,12 @@ export class SignalManager extends EventEmitter implements SignalManager { fireRequestSelectionRangeChange(payload: TimeRangeUpdatePayload): void { this.emit(Signals.REQUEST_SELECTION_RANGE_CHANGE, payload); } + fireContributeContextMenu(payload: ContextMenuContributedSignalPayload): void { + this.emit(Signals.CONTRIBUTE_CONTEXT_MENU, payload); + } + fireContextMenuItemClicked(payload: ContextMenuItemClickedSignalPayload): void { + this.emit(Signals.CONTEXT_MENU_ITEM_CLICKED, payload); + } } let instance: SignalManager = new SignalManager(); diff --git a/packages/react-components/src/components/timegraph-output-component.tsx b/packages/react-components/src/components/timegraph-output-component.tsx index 4e2f5a59b..0a9596900 100644 --- a/packages/react-components/src/components/timegraph-output-component.tsx +++ b/packages/react-components/src/components/timegraph-output-component.tsx @@ -35,6 +35,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import TextField from '@mui/material/TextField'; import InputAdornment from '@mui/material/InputAdornment'; import { debounce } from 'lodash'; +import '../../style/react-contextify.css'; +import { Item, ItemParams, Menu, Separator, Submenu, useContextMenu } from 'react-contexify'; +import { + ContextMenuContributedSignalPayload, + ContextMenuItems, + MenuItem, + SubMenu +} from 'traceviewer-base/lib/signals/context-menu-contributed-signal-payload'; +import { ContextMenuItemClickedSignalPayload } from 'traceviewer-base/lib/signals/context-menu-item-clicked-signal-payload'; +import { RowSelectionsChangedSignalPayload } from 'traceviewer-base/lib/signals/row-selections-changed-signal-payload'; type TimegraphOutputProps = AbstractOutputProps & { addWidgetResizeHandler: (handler: () => void) => void; @@ -48,16 +58,18 @@ type TimegraphOutputState = AbstractTreeOutputState & { | { rows: TimelineChart.TimeGraphRowModel[]; range: TimelineChart.TimeGraphRange; resolution: number } | undefined; selectedRow?: number; + multiSelectedRows?: number[]; selectedMarkerRow?: number; collapsedNodes: number[]; collapsedMarkerNodes: number[]; columns: ColumnHeader[]; dataRows: TimelineChart.TimeGraphRowModel[]; searchString: string; + menuItems?: ContextMenuItems; }; const COARSE_RESOLUTION_FACTOR = 8; // resolution factor to use for first (coarse) update - +const MENU_ID = 'timegraph.menuId-'; export class TimegraphOutputComponent extends AbstractTreeOutputComponent { private totalHeight = 0; private rowController: TimeGraphRowController; @@ -83,6 +95,8 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent this.doHandleSelectionChangedSignal(payload); private onOutputDataChanged = (outputs: OutputDescriptor[]) => this.doHandleOutputDataChangedSignal(outputs); + private onContextMenuContributed = (payload: ContextMenuContributedSignalPayload) => + this.doHandleContextMenuContributed(payload); private pendingSelection: TimeGraphEntry | undefined; private _debouncedUpdateSearch = debounce(() => this.updateSearchFilter(), 500); @@ -101,6 +115,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent this.getTimegraphRowIds(), @@ -252,12 +269,14 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent { @@ -490,6 +517,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent + {this.renderContextMenu()} @@ -521,6 +552,135 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent, id: number): void { + event.preventDefault(); + event.stopPropagation(); + + if ( + !this.state.menuItems || + (this.state.menuItems.items.length <= 0 && this.state.menuItems.submenus.length <= 0) + ) { + return; + } + + // If the id that the context menu was triggered from is not in multi-selected-rows, we treat this as a row click + if (!this.state.multiSelectedRows?.includes(id)) { + this.onRowClick(id); + } + + const refNode = this.treeRef.current; + let calculatedX = 0; + let calculatedY = 0; + if (refNode) { + calculatedX = event.clientX - refNode.getBoundingClientRect().left; + calculatedY = event.clientY - refNode.getBoundingClientRect().top; + } + + const { show } = useContextMenu({ + id: MENU_ID + this.props.outputDescriptor.id + }); + + show(event, { + props: {}, + position: { x: calculatedX, y: calculatedY } + }); + } + + protected handleItemClick = (params: ItemParams): void => { + const props = { + selectedRows: this.getEntryModelsForRowIds(this.state.multiSelectedRows) + }; + const signalPayload: ContextMenuItemClickedSignalPayload = new ContextMenuItemClickedSignalPayload( + this.props.outputDescriptor.id, + params.data.itemId, + props, + params.data.parentMenuId + ); + signalManager().fireContextMenuItemClicked(signalPayload); + }; + + protected getEntryModelsForRowIds = ( + ids: number[] | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): { id: number; parentId?: number; metadata?: { [key: string]: any } }[] => { + if (!ids) { + return []; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const entryModels: { id: number; parentId?: number; metadata?: { [key: string]: any } }[] = []; + for (const id of ids) { + const element = this.state.timegraphTree.find(el => el.id === id); + if (element) { + entryModels.push({ id: element.id, parentId: element.parentId, metadata: element.metadata }); + } + } + return entryModels; + }; + + renderContextMenu(): React.ReactNode { + if ( + !this.state.menuItems || + (this.state.menuItems.items.length <= 0 && this.state.menuItems.submenus.length <= 0) + ) { + return <>; + } + return ( + + + {this.renderSubMenu(this.state.menuItems.submenus)} + {this.state.menuItems.submenus.length > 0 && } + {this.renderItems(this.state.menuItems.items)} + + + ); + } + + renderSubMenu(submenus: SubMenu[]): React.ReactNode { + return ( + + {submenus && submenus.length > 0 ? ( + submenus.map(menu => ( + + {menu.submenu && this.renderSubMenu([menu.submenu])} + {this.renderItems(menu.items)} + + )) + ) : ( + <> + )} + + ); + } + + renderItems(items: MenuItem[]): React.ReactNode { + return ( + + {items && items.length > 0 ? ( + items.map(item => ( + + {item.label} + + )) + ) : ( + <> + )} + + ); + } + renderYAxis(): React.ReactNode { return undefined; } @@ -977,6 +1137,29 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent menu.id === submenu.id) === -1) { + currentMenuItems.submenus.push(submenu); + } + } + for (const menuItem of menuItemsToAdd.items) { + if (currentMenuItems.items.findIndex(item => item.id === menuItem.id) === -1) { + currentMenuItems.items.push(menuItem); + } + } + } + this.setState({ menuItems: currentMenuItems }); + } + } + public onThemeChange = (): void => { // Simulate a click on the selected row when theme changes. // This changes the color of the selected row to new theme. @@ -1197,6 +1380,56 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent { + const tree = listToTree(this.state.timegraphTree, this.state.columns); + const rowIndex = getIndexOfNode(id, tree, this.state.collapsedNodes); + + if (isShiftClicked) { + // Perform shift action based on selectedRow value + if (this.state.selectedRow !== undefined) { + const treeNodeIds = getAllExpandedNodeIds(tree, this.state.collapsedNodes); + const lastSelectedRowIndex = getIndexOfNode(this.state.selectedRow, tree, this.state.collapsedNodes); + this.handleShiftClick(rowIndex, lastSelectedRowIndex, treeNodeIds); + } else { + // Do not have a previous selection therefore treat this as a normal row click + this.onRowClick(id); + } + } else { + const rows = this.state.multiSelectedRows ? this.state.multiSelectedRows.slice() : []; + // This indicates that the row is being deselected + if (rows.includes(id)) { + const index = rows.indexOf(id); + /** + * It is possible for the entry being deselected to be selectedRow. + * In this case we have to update the selectedRow state to point to another row + * We will try to set this to previously selected row that is part of the multi-selection, otherwise it should be set to undefined + */ + if (id === this.state.selectedRow) { + const prevRowId = rows.at(index - 1); + if (prevRowId && prevRowId !== id) { + const prevRowIndex = getIndexOfNode(prevRowId, tree, this.state.collapsedNodes); + this.chartLayer.selectAndReveal(prevRowIndex); + } else { + this.setState({ selectedRow: undefined }); + this.chartLayer.selectRow(undefined); + } + } + rows.splice(index, 1); + } else { + // No selectedRow - delegate to onRowClick as it is a first selection + if (this.state.selectedRow === undefined) { + this.onRowClick(id); + return; + } + rows?.push(id); + } + this.setState({ multiSelectedRows: rows }); + } }; public onMarkerRowClick = (id: number): void => { @@ -1208,9 +1441,38 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent this.setState({ selectedRow: row.id }); - public onMarkerSelectionChange = (row: TimelineChart.TimeGraphRowModel): void => + public onSelectionChange = (row: TimelineChart.TimeGraphRowModel): void => { + this.setState({ selectedRow: row.id }); + }; + + public onMarkerSelectionChange = (row: TimelineChart.TimeGraphRowModel): void => { this.setState({ selectedMarkerRow: row.id }); + }; + + private handleShiftClick = (currentIndex: number, lastSelectedRowIndex: number, treeNodeIds: number[]): void => { + const shiftClickedRows: number[] = []; + let startIndex = 0; + let endIndex = 0; + + if (lastSelectedRowIndex < currentIndex) { + startIndex = lastSelectedRowIndex; + endIndex = currentIndex; + } else { + startIndex = currentIndex; + endIndex = lastSelectedRowIndex; + } + + if (startIndex < 0 || startIndex >= treeNodeIds.length || endIndex >= treeNodeIds.length) { + return; + } + for (let i = startIndex; i <= endIndex; i++) { + const nodeId = treeNodeIds.at(i); + if (nodeId) { + shiftClickedRows.push(nodeId); + } + } + this.setState({ multiSelectedRows: shiftClickedRows }); + }; private selectAndReveal(item: TimeGraphEntry) { const rowIndex = getIndexOfNode( diff --git a/packages/react-components/src/components/utils/filter-tree/entry-tree.tsx b/packages/react-components/src/components/utils/filter-tree/entry-tree.tsx index 9093ff114..873aac74d 100644 --- a/packages/react-components/src/components/utils/filter-tree/entry-tree.tsx +++ b/packages/react-components/src/components/utils/filter-tree/entry-tree.tsx @@ -11,10 +11,12 @@ interface EntryTreeProps { showCheckboxes: boolean; showCloseIcons: boolean; selectedRow?: number; + multiSelectedRows?: number[]; collapsedNodes: number[]; showFilter: boolean; onToggleCheck: (ids: number[]) => void; onRowClick: (id: number) => void; + onMultipleRowClick?: (id: number, isShiftClicked?: boolean) => void; onContextMenu: (event: React.MouseEvent, id: number) => void; onClose: (id: number) => void; onToggleCollapse: (id: number, nodes: TreeNode[]) => void; @@ -43,7 +45,8 @@ export class EntryTree extends React.Component { this.props.checkedSeries !== nextProps.checkedSeries || this.props.entries !== nextProps.entries || this.props.collapsedNodes !== nextProps.collapsedNodes || - this.props.selectedRow !== nextProps.selectedRow; + this.props.selectedRow !== nextProps.selectedRow || + this.props.multiSelectedRows !== nextProps.multiSelectedRows; render(): JSX.Element { return ; diff --git a/packages/react-components/src/components/utils/filter-tree/table-body.tsx b/packages/react-components/src/components/utils/filter-tree/table-body.tsx index 2192f1796..1371c450e 100644 --- a/packages/react-components/src/components/utils/filter-tree/table-body.tsx +++ b/packages/react-components/src/components/utils/filter-tree/table-body.tsx @@ -5,12 +5,14 @@ import { TableRow } from './table-row'; interface TableBodyProps { nodes: TreeNode[]; selectedNode?: number; + multiSelectedNodes?: number[]; collapsedNodes: number[]; isCheckable: boolean; isClosable: boolean; getCheckedStatus: (id: number) => number; onToggleCollapse: (id: number) => void; onRowClick: (id: number) => void; + onMultipleRowClick?: (id: number, isShiftClicked?: boolean) => void; onClose: (id: number) => void; onToggleCheck: (id: number) => void; onContextMenu: (event: React.MouseEvent, id: number) => void; diff --git a/packages/react-components/src/components/utils/filter-tree/table-row.tsx b/packages/react-components/src/components/utils/filter-tree/table-row.tsx index 624490dff..b58ec2621 100644 --- a/packages/react-components/src/components/utils/filter-tree/table-row.tsx +++ b/packages/react-components/src/components/utils/filter-tree/table-row.tsx @@ -8,6 +8,7 @@ interface TableRowProps { node: TreeNode; level: number; selectedRow?: number; + multiSelectedRows?: number[]; collapsedNodes: number[]; isCheckable: boolean; isClosable: boolean; @@ -16,6 +17,7 @@ interface TableRowProps { onClose: (id: number) => void; onToggleCheck: (id: number) => void; onRowClick: (id: number) => void; + onMultipleRowClick?: (id: number, isShiftClicked?: boolean) => void; onContextMenu: (event: React.MouseEvent, id: number) => void; } @@ -89,9 +91,14 @@ export class TableRow extends React.Component { return undefined; }; - onClick = (): void => { - const { node, onRowClick } = this.props; - if (onRowClick) { + onClick = (e: React.MouseEvent): void => { + const { node, onRowClick, onMultipleRowClick } = this.props; + + if (onMultipleRowClick && e.ctrlKey) { + onMultipleRowClick(node.id, false); + } else if (onMultipleRowClick && e.shiftKey) { + onMultipleRowClick(node.id, true); + } else { onRowClick(node.id); } }; @@ -108,16 +115,22 @@ export class TableRow extends React.Component { return undefined; } const children = this.renderChildren(); - const { node, selectedRow } = this.props; - const className = selectedRow === node.id ? 'selected' : ''; - - return ( - - - {this.renderRow()} - - {children} - - ); + const { node, selectedRow, multiSelectedRows } = this.props; + let className = ''; + + if (selectedRow === node.id || multiSelectedRows?.includes(node.id)) { + className = 'selected'; + } + + { + return ( + + + {this.renderRow()} + + {children} + + ); + } } } diff --git a/packages/react-components/src/components/utils/filter-tree/table.tsx b/packages/react-components/src/components/utils/filter-tree/table.tsx index 436cca4f9..9c891f839 100644 --- a/packages/react-components/src/components/utils/filter-tree/table.tsx +++ b/packages/react-components/src/components/utils/filter-tree/table.tsx @@ -8,11 +8,13 @@ import ColumnHeader from './column-header'; interface TableProps { nodes: TreeNode[]; selectedRow?: number; + multiSelectedRows?: number[]; collapsedNodes: number[]; isCheckable: boolean; isClosable: boolean; sortConfig: SortConfig[]; onRowClick: (id: number) => void; + onMultipleRowClick?: (id: number, isShiftClicked?: boolean) => void; onContextMenu: (event: React.MouseEvent, id: number) => void; getCheckedStatus: (id: number) => number; onToggleCollapse: (id: number) => void; diff --git a/packages/react-components/src/components/utils/filter-tree/tree.tsx b/packages/react-components/src/components/utils/filter-tree/tree.tsx index a1f5bd6a4..cde574070 100644 --- a/packages/react-components/src/components/utils/filter-tree/tree.tsx +++ b/packages/react-components/src/components/utils/filter-tree/tree.tsx @@ -16,9 +16,11 @@ interface FilterTreeProps { checkedSeries: number[]; // Optional collapsedNodes: number[]; selectedRow?: number; + multiSelectedRows?: number[]; onToggleCheck: (ids: number[]) => void; // Optional onClose: (id: number) => void; onRowClick: (id: number) => void; + onMultipleRowClick?: (id: number, isShiftClicked?: boolean) => void; onContextMenu: (event: React.MouseEvent, id: number) => void; onToggleCollapse: (id: number, nodes: TreeNode[]) => void; onOrderChange: (ids: number[]) => void; @@ -269,6 +271,7 @@ export class FilterTree extends React.Component, + + Multi-Select + + Ctrl + Click + or + Shift + Click + + diff --git a/packages/react-components/style/output-components-style.css b/packages/react-components/style/output-components-style.css index fa7587325..e917d1d70 100644 --- a/packages/react-components/style/output-components-style.css +++ b/packages/react-components/style/output-components-style.css @@ -239,6 +239,10 @@ canvas { background-color: var(--trace-viewer-selection-background) !important; } +.table-tree tr.multiselected td { + background-color: var(--trace-viewer-list-hoverBackground) !important; +} + .resize-handle { float: right; position: absolute; diff --git a/packages/react-components/style/react-contextify.css b/packages/react-components/style/react-contextify.css index aa7ba9d16..566e9a3a7 100644 --- a/packages/react-components/style/react-contextify.css +++ b/packages/react-components/style/react-contextify.css @@ -10,7 +10,7 @@ box-shadow: 0px 10px 30px -5px rgba(0, 0, 0, 0.3); border-radius: 6px; padding: 6px 0; - min-width: 200px; + min-width: 150px; z-index: 100; } .react-contexify__submenu--is-open, .react-contexify__submenu--is-open > .react-contexify__item__content { @@ -231,4 +231,4 @@ animation: react-contexify__slideOut 0.3s; } - /*# sourceMappingURL=ReactContexify.css.map */ \ No newline at end of file + /*# sourceMappingURL=ReactContexify.css.map */