Skip to content

Commit

Permalink
Add tooltip support for timegraph states
Browse files Browse the repository at this point in the history
Signed-off-by: Patrick Tasse <patrick.tasse@ericsson.com>
  • Loading branch information
PatrickTasse committed Mar 18, 2021
1 parent 59a4997 commit f82f0c3
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/react-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"react-chartjs-2": "^2.7.6",
"react-grid-layout": "^1.1.0",
"react-modal": "^3.8.1",
"react-tooltip": "4.2.14",
"react-virtualized": "^9.21.0",
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^0.86.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import { TimeGraphUnitController } from 'timeline-chart/lib/time-graph-unit-cont
import { TimeRange } from '@trace-viewer/base/lib/utils/time-range';
import { OutputComponentStyle } from './utils/output-component-style';
import { OutputStyleModel } from 'tsp-typescript-client/lib/models/styles';
import { TooltipComponent } from './tooltip-component';

export interface AbstractOutputProps {
tspClient: TspClient;
tooltipComponent: TooltipComponent | null;
traceId: string;
range: TimeRange;
nbEvents: number;
Expand Down Expand Up @@ -61,12 +63,13 @@ export abstract class AbstractOutputComponent<P extends AbstractOutputProps, S e
onMouseDown={this.props.onMouseDown}
onTouchStart={this.props.onTouchStart}
onTouchEnd={this.props.onTouchEnd}
>
data-tip=''
data-for="tooltip-component">
<div className='widget-handle' style={{ width: this.HANDLE_WIDTH, height:this.props.style.height }}>
{this.renderTitleBar()}
</div>
<div className='main-output-container' ref={this.mainAreaContainer}
style={{ width: this.props.widthWPBugWorkaround - this.HANDLE_WIDTH, height:this.props.style.height }}>
style={{ width: this.props.widthWPBugWorkaround - this.HANDLE_WIDTH, height:this.props.style.height }}>
{this.renderMainArea()}
</div>
{this.props.children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent<Timegr
private styleMap = new Map<string, TimeGraphRowElementStyle>();

private selectedElement: TimeGraphRowElement | undefined;
private tooltipElement: TimeGraphRowElement | undefined;
private tooltipInfo: {[key: string]: string} | undefined;

constructor(props: TimegraphOutputProps) {
super(props);
Expand Down Expand Up @@ -90,6 +92,14 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent<Timegr
}
this.onElementSelected(this.selectedElement);
});
this.chartLayer.registerRowElementMouseInteractions({
mouseover: el => {
this.props.tooltipComponent?.setElement(el, () => this.fetchTooltip(el));
},
mouseout: () => {
this.props.tooltipComponent?.setElement(undefined);
}
});
signalManager().on(Signals.SELECTION_CHANGED, ({ payload }) => this.onSelectionChanged(payload));
}

Expand Down Expand Up @@ -204,6 +214,29 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent<Timegr
</React.Fragment>;
}

private async fetchTooltip(element: TimeGraphRowElement): Promise<{ [key: string]: string } | undefined> {
const elementRange = element.model.range;
const offset = this.props.viewRange.getOffset();
let start: string | undefined;
let end: string | undefined;
if (this.props.unitController.numberTranslator) {
start = this.props.unitController.numberTranslator(elementRange.start);
end = this.props.unitController.numberTranslator(elementRange.end);
}
start = start ? start : (elementRange.start + (offset ? offset : 0)).toString();
end = end ? end : (elementRange.end + (offset ? offset : 0)).toString();
const time = Math.round(elementRange.start + (offset ? offset : 0));
const tooltipResponse = await this.props.tspClient.fetchTimeGraphToolTip(
this.props.traceId, this.props.outputDescriptor.id, time, element.row.model.id.toString());
return {
'Label': element.model.label,
'Start time': start,
'End time': end,
'Row': element.row.model.name,
...tooltipResponse.getModel()?.model
};
}

private renderTimeGraphContent() {
return <div id='main-timegraph-content' ref={this.horizontalContainer} style={{ height: this.props.style.height }} >
{this.getChartContainer()}
Expand Down
118 changes: 118 additions & 0 deletions packages/react-components/src/components/tooltip-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as React from 'react';
import ReactTooltip from 'react-tooltip';

type MaybePromise<T> = T | Promise<T>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface TooltipComponentState<T = any> {
element?: T;
func?: ((element: T) => MaybePromise<{ [key: string]: string } | undefined>);
content?: string;
}

export class TooltipComponent extends React.Component<unknown, TooltipComponentState> {

private static readonly HOURGLASS_NOT_DONE = '&#x23f3;';

timerId?: NodeJS.Timeout;

constructor(props: unknown) {
super(props);
this.state = {
element: undefined,
func: undefined,
content: undefined
};
}

render(): React.ReactNode {
return <div
onMouseEnter={() => {
if (this.timerId) {
clearTimeout(this.timerId);
this.timerId = undefined;
}
}}
onMouseLeave={() => {
ReactTooltip.hide();
this.setState({ content: undefined });
}}
>
<ReactTooltip
className="react-tooltip"
id="tooltip-component"
effect='float'
type='info'
place='bottom'
html={true}
delayShow={500}
delayUpdate={500}
afterShow={() => {
if (this.timerId) {
clearTimeout(this.timerId);
this.timerId = undefined;
}
if (this.state.content === undefined) {
this.fetchContent(this.state.element);
}
}}
clickable={true}
scrollHide={true}
overridePosition={({ left, top }, currentEvent, currentTarget, refNode, place) => {
left += (place === 'left') ? -10 : (place === 'right') ? 10 : 0;
top += (place === 'top') ? -10 : (place === 'bottom') ? 10 : 0;
return { left, top };
}}
getContent={() => this.getContent()}
/>
</div>;
}

setElement<T>(element: T, func?: ((element: T) => MaybePromise<{ [key: string]: string } | undefined>)): void {
if (element !== this.state.element && this.state.element) {
if (this.state.content) {
if (this.timerId === undefined) {
// allow 500 ms to move mouse over the tooltip
this.timerId = setTimeout(() => {
if (this.state.element !== element || this.state.element === undefined) {
ReactTooltip.hide();
this.setState({ content: undefined });
}
}, 500);
}
} else {
// content being fetched, hide the hourglass tooltip
ReactTooltip.hide();
}
}
this.setState({ element, func });
}

private getContent() {
if (this.state.content) {
return this.state.content;
}
if (this.state.element) {
return TooltipComponent.HOURGLASS_NOT_DONE;
}
return undefined;
}

private async fetchContent(element: unknown) {
if (this.state.element && this.state.func) {
const tooltipInfo = await this.state.func(element);
let content = '<table>';
if (tooltipInfo) {
Object.entries(tooltipInfo).forEach(([k, v]) => content += this.tooltipRow(k, v));
}
content += '</table>';
if (this.state.element === element) {
this.setState({ content });
}
}
}

private tooltipRow(key: string, value: string) {
return '<tr><td style="text-align:left">' + key + '</td><td style="text-align:left">' + value + '</td></tr>';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { NullOutputComponent } from './null-output-component';
import { AbstractOutputProps } from './abstract-output-component';
import * as Messages from '@trace-viewer/base/lib/message-manager';
import { signalManager, Signals } from '@trace-viewer/base/lib/signal-manager';
import ReactTooltip from 'react-tooltip';
import { TooltipComponent } from './tooltip-component';

const ResponsiveGridLayout = WidthProvider(Responsive);

Expand Down Expand Up @@ -58,7 +60,7 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
private readonly SCROLLBAR_PADDING: number = 12;

private unitController: TimeGraphUnitController;

private tooltipComponent: React.RefObject<TooltipComponent>;
private traceContextContainer: React.RefObject<HTMLDivElement>;

protected widgetResizeHandlers: (() => void)[] = [];
Expand Down Expand Up @@ -108,6 +110,7 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
};
this.unitController.onSelectionRangeChange(range => { this.handleTimeSelectionChange(range); });
this.unitController.onViewRangeChanged(viewRangeParam => { this.handleViewRangeChange(viewRangeParam); });
this.tooltipComponent = React.createRef();
this.traceContextContainer = React.createRef();
this.initialize();
signalManager().on(Signals.THEME_CHANGED, (theme: string) => this.updateBackgroundTheme(theme));
Expand Down Expand Up @@ -180,6 +183,11 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
this.props.messageManager.removeStatusMessage(this.TIME_SELECTION_STATUS_BAR_KEY);
}

async componentDidUpdate(): Promise<void> {
// Rebuild enables tooltip on newly added output component
ReactTooltip.rebuild();
}

private onResize() {
const newWidth = this.traceContextContainer.current ? this.traceContextContainer.current.clientWidth - this.SCROLLBAR_PADDING : this.DEFAULT_COMPONENT_WIDTH;
this.setState(prevState => ({ style: { ...prevState.style, width: newWidth, chartWidth: this.getChartWidth(newWidth) } }));
Expand Down Expand Up @@ -211,6 +219,7 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr

render(): JSX.Element {
return <div className='trace-context-container' ref={this.traceContextContainer}>
<TooltipComponent ref={this.tooltipComponent} />
{this.props.outputs.length ? this.renderOutputs() : this.renderPlaceHolder()}
</div>;
}
Expand All @@ -233,6 +242,7 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
const responseType = output.type;
const outputProps: AbstractOutputProps = {
tspClient: this.props.tspClient,
tooltipComponent: this.tooltipComponent.current,
traceId: this.state.experiment.UUID,
outputDescriptor: output,
range: this.state.currentRange,
Expand Down
1 change: 1 addition & 0 deletions packages/react-components/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"composite": true,
"strict": true,
"sourceMap": true,
Expand Down
13 changes: 13 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -13352,6 +13352,14 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.13.1:
react-is "^16.8.6"
scheduler "^0.19.1"

react-tooltip@4.2.14:
version "4.2.14"
resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.14.tgz#8e06b5926fdf6672e78d8ccadaa16bef40d131d7"
integrity sha512-hS2kAlpjyH5MXL9DaGKsdmEFCIEuMD2RZXkEJeNjmDe05dHpqj93o5JgpmczAgQFk099+JSsnHUDo7pIOuyMDQ==
dependencies:
prop-types "^15.7.2"
uuid "^7.0.3"

react-virtualized@^9.20.0, react-virtualized@^9.21.0:
version "9.22.3"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.3.tgz#f430f16beb0a42db420dbd4d340403c0de334421"
Expand Down Expand Up @@ -15914,6 +15922,11 @@ uuid@^3.0.1, uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==

uuid@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==

uuid@^8.0.0, uuid@^8.3.0:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
Expand Down

0 comments on commit f82f0c3

Please sign in to comment.