diff --git a/packages/react-components/package.json b/packages/react-components/package.json index ea465f0b2..6456607ad 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -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", diff --git a/packages/react-components/src/components/abstract-output-component.tsx b/packages/react-components/src/components/abstract-output-component.tsx index 656486ada..425c2a07a 100644 --- a/packages/react-components/src/components/abstract-output-component.tsx +++ b/packages/react-components/src/components/abstract-output-component.tsx @@ -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; @@ -61,12 +63,13 @@ export abstract class AbstractOutputComponent

+ data-tip='' + data-for="tooltip-component">

{this.renderTitleBar()}
+ style={{ width: this.props.widthWPBugWorkaround - this.HANDLE_WIDTH, height:this.props.style.height }}> {this.renderMainArea()}
{this.props.children} diff --git a/packages/react-components/src/components/timegraph-output-component.tsx b/packages/react-components/src/components/timegraph-output-component.tsx index cc8bf012c..b44fc9790 100644 --- a/packages/react-components/src/components/timegraph-output-component.tsx +++ b/packages/react-components/src/components/timegraph-output-component.tsx @@ -45,6 +45,8 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent(); private selectedElement: TimeGraphRowElement | undefined; + private tooltipElement: TimeGraphRowElement | undefined; + private tooltipInfo: {[key: string]: string} | undefined; constructor(props: TimegraphOutputProps) { super(props); @@ -90,6 +92,14 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent { + this.props.tooltipComponent?.setElement(el, () => this.fetchTooltip(el)); + }, + mouseout: () => { + this.props.tooltipComponent?.setElement(undefined); + } + }); signalManager().on(Signals.SELECTION_CHANGED, ({ payload }) => this.onSelectionChanged(payload)); } @@ -204,6 +214,29 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent; } + 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
{this.getChartContainer()} diff --git a/packages/react-components/src/components/tooltip-component.tsx b/packages/react-components/src/components/tooltip-component.tsx new file mode 100644 index 000000000..1d2264e89 --- /dev/null +++ b/packages/react-components/src/components/tooltip-component.tsx @@ -0,0 +1,118 @@ +import * as React from 'react'; +import ReactTooltip from 'react-tooltip'; + +type MaybePromise = T | Promise; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface TooltipComponentState { + element?: T; + func?: ((element: T) => MaybePromise<{ [key: string]: string } | undefined>); + content?: string; +} + +export class TooltipComponent extends React.Component { + + private static readonly HOURGLASS_NOT_DONE = '⏳'; + + timerId?: NodeJS.Timeout; + + constructor(props: unknown) { + super(props); + this.state = { + element: undefined, + func: undefined, + content: undefined + }; + } + + render(): React.ReactNode { + return
{ + if (this.timerId) { + clearTimeout(this.timerId); + this.timerId = undefined; + } + }} + onMouseLeave={() => { + ReactTooltip.hide(); + this.setState({ content: undefined }); + }} + > + { + 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()} + /> +
; + } + + setElement(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 = ''; + if (tooltipInfo) { + Object.entries(tooltipInfo).forEach(([k, v]) => content += this.tooltipRow(k, v)); + } + content += '
'; + if (this.state.element === element) { + this.setState({ content }); + } + } + } + + private tooltipRow(key: string, value: string) { + return '' + key + '' + value + ''; + } +} diff --git a/packages/react-components/src/components/trace-context-component.tsx b/packages/react-components/src/components/trace-context-component.tsx index 8d1fae5a6..d44526702 100644 --- a/packages/react-components/src/components/trace-context-component.tsx +++ b/packages/react-components/src/components/trace-context-component.tsx @@ -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); @@ -58,7 +60,7 @@ export class TraceContextComponent extends React.Component; private traceContextContainer: React.RefObject; protected widgetResizeHandlers: (() => void)[] = []; @@ -108,6 +110,7 @@ export class TraceContextComponent extends React.Component { 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)); @@ -180,6 +183,11 @@ export class TraceContextComponent extends React.Component { + // 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) } })); @@ -211,6 +219,7 @@ export class TraceContextComponent extends React.Component + {this.props.outputs.length ? this.renderOutputs() : this.renderPlaceHolder()}
; } @@ -233,6 +242,7 @@ export class TraceContextComponent extends React.Component