diff --git a/packages/react-components/src/components/timegraph-output-component.tsx b/packages/react-components/src/components/timegraph-output-component.tsx index 943aa7e2..9ad43bba 100644 --- a/packages/react-components/src/components/timegraph-output-component.tsx +++ b/packages/react-components/src/components/timegraph-output-component.tsx @@ -34,6 +34,7 @@ import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import TextField from '@mui/material/TextField'; import InputAdornment from '@mui/material/InputAdornment'; +import Chip from '@mui/material/Chip'; import { debounce } from 'lodash'; import '../../style/react-contextify.css'; import { Item, ItemParams, Menu, Separator, Submenu, useContextMenu } from 'react-contexify'; @@ -66,6 +67,7 @@ type TimegraphOutputState = AbstractTreeOutputState & { columns: ColumnHeader[]; dataRows: TimelineChart.TimeGraphRowModel[]; searchString: string; + filters: string[]; menuItems?: ContextMenuItems; }; @@ -100,7 +102,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent this.updateSearchFilter(), 500); + private _debouncedUpdateSearch = debounce(() => this.updateSearchFilters(), 500); private _debouncedUpdateChart = debounce(() => { this.chartLayer.updateChart(); }, 500); @@ -124,7 +126,8 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent 0) { + if (this.state.searchString?.length > 0 || this.state.filters.length > 0) { this._debouncedUpdateSearch(); } else { this.chartLayer.updateChart(); @@ -347,7 +350,10 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent { - if (this.state.searchString && this.state.searchString.length > 0) { - const filterExpressionsMap: { [key: number]: string[] } = {}; - filterExpressionsMap[1] = [this.state.searchString]; - this.chartLayer.updateChart(filterExpressionsMap); - } else { - this.chartLayer.updateChart(); - } - } - private onToggleCollapse(id: number) { let newList = [...this.state.collapsedNodes]; const exist = this.state.collapsedNodes.find(expandId => expandId === id); @@ -820,8 +816,16 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent this.onKeyDown(event)} + onKeyDown={this.handleKeyDown} /> + {this.state.filters.map((filter, index) => ( + this.removeFilter(filter)} + className="filter-chip" + /> + ))} ); @@ -835,6 +839,47 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent) => { + if ( + event.key === 'Enter' && + this.state.searchString.trim() && + event.target instanceof HTMLInputElement && + event.target.classList.contains('MuiInputBase-input') + ) { + this.addFilter(this.state.searchString.trim()); + this.setState({ searchString: '' }); + } + }; + + private addFilter = (filter: string) => { + this.setState(prevState => ({ filters: [...prevState.filters, filter] }), this.updateSearchFilters); + }; + + private removeFilter = (filter: string) => { + this.setState( + prevState => ({ filters: prevState.filters.filter(f => f !== filter) }), + this.updateSearchFilters + ); + }; + + private updateSearchFilters = () => { + const filterExpressionsMap: { [key: number]: string[] } = {}; + if (this.state.searchString) { + const DIMMED = 1; + filterExpressionsMap[DIMMED] = [this.state.searchString]; // For greying out + } + if (this.state.filters.length > 0) { + const FILTERED = 4; + filterExpressionsMap[FILTERED] = this.state.filters; // For filtering + } + + if (Object.keys(filterExpressionsMap).length > 0) { + this.chartLayer.updateChart(filterExpressionsMap); + } else { + this.chartLayer.updateChart(); + } + }; + private clearSearchBox() { this.setState({ searchString: '' }); } diff --git a/packages/react-components/style/output-components-style.css b/packages/react-components/style/output-components-style.css index e3d01313..60c8535b 100644 --- a/packages/react-components/style/output-components-style.css +++ b/packages/react-components/style/output-components-style.css @@ -442,14 +442,16 @@ canvas { .timgraph-search-bar { background: var(--trace-viewer-editor-background); padding-top: 5px; - padding-bottom: 10px; + padding-bottom: 15px; + height: 30px; display: flex; flex-direction: row; flex-wrap: wrap; + gap: 10px; } .timegraph-search-box { - height: 24px; + height: 30px; margin-left: 2.5px; margin-right: 2.5px; font-family: var(--trace-viewer-ui-font-family) !important; @@ -495,12 +497,6 @@ canvas { align-items: center; } -.input-container { - flex: 5; - padding: 5px; - display: flex; - position: relative; -} .input-container input { flex: 1; @@ -516,3 +512,21 @@ canvas { flex: 1; margin-right: 5px; } + +.filter-chip { + background-color: var(--theia-selection-background) !important; + color: var(--trace-viewer-ui-font-color0); + border-radius: 15px !important; + border-width: 1px !important; + display: flex; + align-items: center; + font-size: 12px; + height: 30px !important; +} + +.filter-chip .MuiChip-deleteIcon { + color: var(--trace-viewer-ui-font-color0); + font-size: 16px; + margin-left: 4px; + cursor: pointer; +}