Skip to content

Commit

Permalink
Added support for time range columns in datatree-output-component
Browse files Browse the repository at this point in the history
Columns of data type TIME_RANGE provide time ranges in the trace
that can be selected and navigated to.

If a data provider provides a column of type TIME_RANGE then it
creates a context-sensitive menu item to select that time range
in the trace viewer.

Signed-off-by: Bernd Hufmann <bernd.hufmann@ericsson.com>
  • Loading branch information
bhufmann committed Mar 21, 2023
1 parent 432b74c commit 064b2bd
Show file tree
Hide file tree
Showing 16 changed files with 474 additions and 17 deletions.
25 changes: 25 additions & 0 deletions examples/browser/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* This file can be edited to customize webpack configuration.
* To reset delete this file and rerun theia build again.
*/
// @ts-check
const config = require('./gen-webpack.config.js');
const webpack = require("webpack");


/**
* Expose bundled modules on window.theia.moduleName namespace, e.g.
* window['theia']['@theia/core/lib/common/uri'].
* Such syntax can be used by external code, for instance, for testing.
config.module.rules.push({
test: /\.js$/,
loader: require.resolve('@theia/application-manager/lib/expose-loader')
}); */

config[0].plugins.push(new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development')
}
}));

module.exports = config;
24 changes: 24 additions & 0 deletions examples/electron/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* This file can be edited to customize webpack configuration.
* To reset delete this file and rerun theia build again.
*/
// @ts-check
const config = require('./gen-webpack.config.js');
const webpack = require("webpack");

/**
* Expose bundled modules on window.theia.moduleName namespace, e.g.
* window['theia']['@theia/core/lib/common/uri'].
* Such syntax can be used by external code, for instance, for testing.
config.module.rules.push({
test: /\.js$/,
loader: require.resolve('@theia/application-manager/lib/expose-loader')
}); */

config[0].plugins.push(new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development')
}
}));

module.exports = config;
1 change: 1 addition & 0 deletions packages/react-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"d3": "^7.1.1",
"lodash": "^4.17.15",
"react-chartjs-2": "^2.7.6",
"react-contexify": "^5.0.0",
"react-grid-layout": "1.2.0",
"react-modal": "^3.8.1",
"react-tooltip": "4.2.14",
Expand Down
123 changes: 112 additions & 11 deletions packages/react-components/src/components/datatree-output-component.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AbstractOutputComponent, AbstractOutputProps, AbstractOutputState } from './abstract-output-component';
import * as React from 'react';
import { Menu, Item, useContextMenu, ItemParams } from 'react-contexify';
import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper';
import { Entry } from 'tsp-typescript-client/lib/models/entry';
import { DataType } from 'tsp-typescript-client/lib/models/data-type';
import { ResponseStatus } from 'tsp-typescript-client/lib/models/response/responses';
import { EntryTree } from './utils/filter-tree/entry-tree';
import { getAllExpandedNodeIds } from './utils/filter-tree/utils';
import { TreeNode } from './utils/filter-tree/tree-node';
import ColumnHeader from './utils/filter-tree/column-header';
import debounce from 'lodash.debounce';
import { signalManager } from 'traceviewer-base/lib/signals/signal-manager';
import '../../style/react-contextify.css';

type DataTreeOutputProps = AbstractOutputProps & {
};
Expand All @@ -22,6 +25,8 @@ type DataTreeOuputState = AbstractOutputState & {
columns: ColumnHeader[];
};

const MENU_ID = 'datatree.context.menuId ';

export class DataTreeOutputComponent extends AbstractOutputComponent<AbstractOutputProps, DataTreeOuputState> {
treeRef: React.RefObject<HTMLDivElement> = React.createRef();

Expand All @@ -35,7 +40,7 @@ export class DataTreeOutputComponent extends AbstractOutputComponent<AbstractOut
xyTree: [],
collapsedNodes: [],
orderedNodes: [],
columns: [{title: 'Name', sortable: true}],
columns: [{ title: 'Name', sortable: true }],
optionsDropdownOpen: false,
additionalOptions: true
};
Expand All @@ -56,10 +61,10 @@ export class DataTreeOutputComponent extends AbstractOutputComponent<AbstractOut
const columns = [];
if (headers && headers.length > 0) {
headers.forEach(header => {
columns.push({title: header.name, sortable: true, resizable: true, tooltip: header.tooltip});
columns.push({ title: header.name, sortable: true, resizable: true, tooltip: header.tooltip, dataType: header.dataType });
});
} else {
columns.push({title: 'Name', sortable: true});
columns.push({ title: 'Name', sortable: true });
}
this.setState({
outputStatus: treeResponse.status,
Expand Down Expand Up @@ -87,15 +92,16 @@ export class DataTreeOutputComponent extends AbstractOutputComponent<AbstractOut
this.onToggleCollapse = this.onToggleCollapse.bind(this);
this.onOrderChange = this.onOrderChange.bind(this);
return this.state.xyTree.length
? <div
tabIndex={0}
id={this.props.traceId + this.props.outputDescriptor.id + 'focusContainer'}
className='scrollable' style={{ height: this.props.style.height, width: this.getMainAreaWidth() }}
>
? <div
tabIndex={0}
id={this.props.traceId + this.props.outputDescriptor.id + 'focusContainer'}
className='scrollable' style={{ height: this.props.style.height, width: this.getMainAreaWidth() }}
>
<EntryTree
entries={this.state.xyTree}
showCheckboxes={false}
collapsedNodes={this.state.collapsedNodes}
onContextMenu={this.onContextMenu}
onToggleCollapse={this.onToggleCollapse}
onOrderChange={this.onOrderChange}
headers={this.state.columns}
Expand All @@ -111,8 +117,10 @@ export class DataTreeOutputComponent extends AbstractOutputComponent<AbstractOut
<div ref={this.treeRef} className='output-component-tree disable-select'
style={{ height: this.props.style.height, width: this.props.outputWidth }}
>
{this.renderContextMenu()}
{this.renderTree()}
</div> :
</div>
:
<div tabIndex={0} id={this.props.traceId + this.props.outputDescriptor.id + 'focusContainer'} className='analysis-running-main-area'>
<i className='fa fa-refresh fa-spin' style={{ marginRight: '5px' }} />
<span>Analysis running</span>
Expand All @@ -121,6 +129,51 @@ export class DataTreeOutputComponent extends AbstractOutputComponent<AbstractOut
</React.Fragment>;
}

renderContextMenu(): React.ReactNode {
const timeRanges: string[] = [];
const cols = this.state.columns;
cols.forEach(col => {
if (col.dataType === DataType.TIME_RANGE) {
timeRanges.push(col.title);
}
});
return <React.Fragment> {
<Menu id={MENU_ID + this.props.outputDescriptor.id} theme={this.props.backgroundTheme} animation={'fade'} >
{(timeRanges && timeRanges.length > 0) ?
timeRanges.map(key => <Item key={key} id={key} onClick={this.handleItemClick}>Select {key}</Item>)
:
<></>
}
</Menu>
}
</React.Fragment>;
}

protected handleItemClick = (args: ItemParams): void => {
const tooltip: { [key: string]: string } = args.props.data;
const min = tooltip[args.event.currentTarget.id];
if (min !== undefined) {
let rx = /\[(\d*),.*/g;
let arr = rx.exec(min);
let start: bigint | undefined = undefined;
if (arr) {
start = BigInt(arr[1]) - this.props.unitController.offset;
}
rx = /.*,(\d*)\]/g;
arr = rx.exec(min);
let end: bigint | undefined = undefined;
if (arr) {
end = BigInt(arr[1]) - this.props.unitController.offset;
}
if (start !== undefined && end !== undefined) {
this.props.unitController.selectionRange = {
start,
end
};
}
}
};

setFocus(): void {
if (document.getElementById(this.props.traceId + this.props.outputDescriptor.id + 'focusContainer')) {
document.getElementById(this.props.traceId + this.props.outputDescriptor.id + 'focusContainer')?.focus();
Expand All @@ -129,6 +182,54 @@ export class DataTreeOutputComponent extends AbstractOutputComponent<AbstractOut
}
}

private onContextMenu = (event: React.MouseEvent<HTMLDivElement>, id: number): void => {
event.preventDefault();
event.stopPropagation();
this.doContextMenu(event, id);
};

private async doContextMenu(event: React.MouseEvent<HTMLDivElement>, id: number): Promise<void> {
if (this.state.xyTree) {
const timeProperties: { [key: string]: string } = {};
const entry = this.state.xyTree.find(e => e.id === id);
if (entry && this.state.columns && this.state.columns.length > 0) {
const cols = this.state.columns;
for (let i = 0; i < cols.length; i++) {
if (cols[i].dataType === DataType.TIME_RANGE) {
timeProperties[cols[i].title] = entry.labels[i];
}
}
}

if (Object.keys(timeProperties).length > 0) {
const { show } = useContextMenu({
id: MENU_ID + this.props.outputDescriptor.id,
});

show(event, {
props: {
data: timeProperties,
},
position: this.getMenuPosition(event)
});
}
}
}
getMenuPosition(event: React.MouseEvent<HTMLDivElement>): { x: number, y: number } {
const refNode = this.treeRef.current;
if (refNode) {
return {
// Compute position relative to treeRef
x: (event.clientX - refNode.getBoundingClientRect().left),
y: (event.clientY - refNode.getBoundingClientRect().top)
};
}
return {
x: 0,
y: 0
};
}

private onToggleCollapse(id: number, nodes: TreeNode[]) {
let newList = [...this.state.collapsedNodes];

Expand All @@ -140,11 +241,11 @@ export class DataTreeOutputComponent extends AbstractOutputComponent<AbstractOut
newList = newList.concat(id);
}
const orderedIds = getAllExpandedNodeIds(nodes, newList);
this.setState({collapsedNodes: newList, orderedNodes: orderedIds});
this.setState({ collapsedNodes: newList, orderedNodes: orderedIds });
}

private onOrderChange(ids: number[]) {
this.setState({orderedNodes: ids});
this.setState({ orderedNodes: ids });
}

protected async waitAnalysisCompletion(): Promise<void> {
Expand Down
Loading

0 comments on commit 064b2bd

Please sign in to comment.