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

Add tooltip support for timegraph states #283

Merged
merged 2 commits into from
Mar 19, 2021
Merged
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
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
119 changes: 119 additions & 0 deletions packages/react-components/src/components/tooltip-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
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}
arrowColor='transparent'
overridePosition={({ left, top }, currentEvent, currentTarget, refNode, place) => {
left += (place === 'left') ? -10 : (place === 'right') ? 10 : 0;
top += (place === 'top') ? -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
19 changes: 16 additions & 3 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 @@ -15322,9 +15330,9 @@ timed-out@^4.0.0, timed-out@^4.0.1:
integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=

timeline-chart@next:
version "0.2.0-next.65ced9e1"
resolved "https://registry.yarnpkg.com/timeline-chart/-/timeline-chart-0.2.0-next.65ced9e1.tgz#de03a3ad7aa38d8311ff518a4b3c41c40e74db7c"
integrity sha512-ok++vQVwciJezUJ81HWGIP59pPJvU7cpqAyBl1HhSpcOXA/HX2k584L3cXCP1Xfr/CuTyl7TqMWbY9DOXMqBSg==
version "0.2.0-next.b33a5cfb"
resolved "https://registry.yarnpkg.com/timeline-chart/-/timeline-chart-0.2.0-next.b33a5cfb.tgz#9a864bff76d3aa52cea2d8f6d1f7680f743feaa0"
integrity sha512-0txCF0aZmNRENmEZHX4spWIE8YAbXIfSXUfaNH08vLrhtNbqaMpPQ2OqenIUwrb9WtA0sgOozARknuBGdANw1w==
dependencies:
"@types/lodash.throttle" "^4.1.4"
glob "^7.1.6"
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