Skip to content

Commit

Permalink
Time Range Data widget
Browse files Browse the repository at this point in the history
Adds the Time Range Data Widget to the sidebar as specified in
ADR eclipse-cdt-cloud#8: Time Range Data Widget

The unit controller is linked to the widget by signals. When a new
active tab is loaded or changed, the trace-viewer Theia component
dispatches the new active unit controller via signal-manager. This
signal is picked up by the time-range-data-widget react component.
Because of this, the unit controller is now a public value in the
trace-context-component.

Signed-off-by: William Yang <william.yang@ericsson.com>
  • Loading branch information
williamsyang-work committed Mar 16, 2023
1 parent 84f8d2f commit 0310a56
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 2 deletions.
7 changes: 6 additions & 1 deletion packages/base/src/signals/signal-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Experiment } from 'tsp-typescript-client/lib/models/experiment';
import { Trace } from 'tsp-typescript-client/lib/models/trace';
import { OpenedTracesUpdatedSignalPayload } from './opened-traces-updated-signal-payload';
import { OutputAddedSignalPayload } from './output-added-signal-payload';
import { TimeGraphUnitController } from 'timeline-chart/lib/time-graph-unit-controller';
export declare interface SignalManager {
fireTraceOpenedSignal(trace: Trace): void;
fireTraceDeletedSignal(trace: Trace): void;
Expand Down Expand Up @@ -60,7 +61,8 @@ export const Signals = {
PIN_VIEW: 'view pinned',
UNPIN_VIEW: 'view unpinned',
OPEN_OVERVIEW_OUTPUT: 'open overview output',
OVERVIEW_OUTPUT_SELECTED: 'overview output selected'
OVERVIEW_OUTPUT_SELECTED: 'overview output selected',
NEW_ACTIVE_UNIT_CONTROLLER: 'new active unit controller',
};

export class SignalManager extends EventEmitter implements SignalManager {
Expand Down Expand Up @@ -141,6 +143,9 @@ export class SignalManager extends EventEmitter implements SignalManager {
fireOverviewOutputSelectedSignal(payload: { traceId: string, outputDescriptor: OutputDescriptor}): void {
this.emit(Signals.OVERVIEW_OUTPUT_SELECTED, payload);
}
fireNewActiveUnitController(unitController: TimeGraphUnitController): void {
this.emit(Signals.NEW_ACTIVE_UNIT_CONTROLLER, unitController);
}
}

let instance: SignalManager = new SignalManager();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private chartPersistedState: { output: OutputDescriptor, payload?: any} | undefined = undefined;

private unitController: TimeGraphUnitController;
public unitController: TimeGraphUnitController;
private historyHandler: UnitControllerHistoryHandler;
private tooltipComponent: React.RefObject<TooltipComponent>;
private tooltipXYComponent: React.RefObject<TooltipXYComponent>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import * as React from 'react';
import { TimelineChart } from 'timeline-chart/lib/time-graph-model';
import { TimeGraphUnitController } from 'timeline-chart/lib/time-graph-unit-controller';
import { signalManager, Signals } from 'traceviewer-base/lib/signals/signal-manager';

export interface ReactTimeRangeDataWidgetProps {
id: string,
title: string,
}

export interface ReactTimeRangeDataWidgetState {
unitController?: TimeGraphUnitController,
viewRange?: TimelineChart.TimeGraphRange,
selectionRange?: TimelineChart.TimeGraphRange,
offset?: bigint,
userInputSelectionStartIsValid: boolean,
userInputSelectionEndIsValid: boolean,
userInputSelectionStart?: bigint,
userInputSelectionEnd?: bigint,
inputting: boolean,
}

export class ReactTimeRangeDataWidget extends React.Component<ReactTimeRangeDataWidgetProps, ReactTimeRangeDataWidgetState> {

private selectionStartInput: React.RefObject<HTMLInputElement>;
private selectionEndInput: React.RefObject<HTMLInputElement>;

constructor(props: ReactTimeRangeDataWidgetProps) {
super(props);
this.selectionEndInput = React.createRef();
this.selectionStartInput = React.createRef();
this.state = {
inputting: false,
userInputSelectionStartIsValid: true,
userInputSelectionEndIsValid: true,
};
signalManager().on(Signals.NEW_ACTIVE_UNIT_CONTROLLER, this.onNewActiveUnitController);
}

componentWillUnmount = (): void => {
this.removeHandlers();
signalManager().off(Signals.NEW_ACTIVE_UNIT_CONTROLLER, this.onNewActiveUnitController);
};

addHandlers = (): void => {
this.state.unitController?.onSelectionRangeChange(this.onUnitControllerValueChange);
this.state.unitController?.onViewRangeChanged(this.onUnitControllerValueChange);
};

removeHandlers = (): void => {
this.state.unitController?.removeSelectionRangeChangedHandler(this.onUnitControllerValueChange);
this.state.unitController?.removeViewRangeChangedHandler(this.onUnitControllerValueChange);
};

onNewActiveUnitController = (unitController: TimeGraphUnitController): void => {
this.removeHandlers();
const { viewRange, selectionRange, offset } = unitController;
this.setState({ unitController, viewRange, selectionRange, offset }, this.addHandlers);
};

onUnitControllerValueChange = (): void => {
if (!this.state.unitController) {
return;
}
const { viewRange, selectionRange, offset } = this.state.unitController;
this.setState({ viewRange, selectionRange, offset, inputting: false }, this.setFormInputValuesToUnitControllersValue);
};

setFormInputValuesToUnitControllersValue = (): void => {
const { selectionRange } = this.state;
const { start , end } = this.getStartAndEnd(selectionRange?.start, selectionRange?.end);
if (this.selectionStartInput.current && this.selectionEndInput.current) {
this.selectionStartInput.current.value = start;
this.selectionEndInput.current.value = end;
}
this.setState({
userInputSelectionEndIsValid: true,
userInputSelectionStartIsValid: true,
userInputSelectionEnd: undefined,
userInputSelectionStart: undefined,
inputting: false,
});
};

onChange = (event: React.FormEvent<HTMLInputElement>, inputIndex: number): void => {
event.preventDefault();
if (!this.state.inputting) {
this.setState({ inputting: true });
}

// BigInt("") => 0 but we want that to be undefined.
const value = event.currentTarget.value === '' ? undefined : BigInt(event.currentTarget.value);

switch (inputIndex) {
case 0:
this.setState({ userInputSelectionStart: value });
return;
case 1:
this.setState({ userInputSelectionEnd: value });
return;
default:
throw Error('Input index is invalid!');
}

};

onSubmit = (event: React.FormEvent): void => {
this.verifyUserInput();
event.preventDefault();
};

onCancel = (): void => {
this.setFormInputValuesToUnitControllersValue();
};

/**
*
* Sometimes the unitController's selection range has a start that's larger than the end (they're reversed).
* This always sets the lesser number as the start.
* @param value1
* @param value2
* @returns { start: string, end: string }
*/
getStartAndEnd = (v1: bigint | string | undefined, v2: bigint | string | undefined): { start: string, end: string } => {
const { unitController, offset } = this.state;
if (!unitController || !offset || v1 === undefined || v2 === undefined) {
return { start: '', end: '' };
}

v1 = BigInt(v1);
v2 = BigInt(v2);

const reverse = v1 > v2;
const start = reverse ? v2 : v1;
const end = reverse ? v1 : v2;

// We display values in absolute time with the offset.
return {
start: (start + offset).toString(),
end: (end + offset).toString()
};
};

verifyUserInput = (): void => {
let { unitController, userInputSelectionStart, userInputSelectionEnd } = this.state;

// We need at least one value to change: start or end.
if (!unitController || (!userInputSelectionStart && !userInputSelectionEnd)) {
this.setFormInputValuesToUnitControllersValue();
return;
}
const { offset, absoluteRange, selectionRange } = unitController;

// If there is no pre-existing selection range and the user only inputs one value
// Make that both selection range start and end value
if (!selectionRange && (!userInputSelectionEnd || !userInputSelectionStart)) {
userInputSelectionStart = userInputSelectionStart || userInputSelectionEnd;
userInputSelectionEnd = userInputSelectionEnd || userInputSelectionStart;
}

// If there is no user input for start or end, set that value to the current unit controller value.
userInputSelectionStart = typeof userInputSelectionStart === 'bigint' ? userInputSelectionStart :
// Below is added to satisfy typescript compiler
!selectionRange ? offset :
// We also need to account for backwards selection start / end here.
selectionRange.start <= selectionRange.end ? selectionRange.start + offset :
selectionRange.end + offset;
userInputSelectionEnd = typeof userInputSelectionEnd === 'bigint' ? userInputSelectionEnd :
!selectionRange ? offset :
selectionRange.start <= selectionRange.end ? selectionRange.end + offset :
selectionRange.start + offset;

const isValid = (n: bigint): boolean => (n >= offset) && (n <= absoluteRange + offset);
const startValid = isValid(userInputSelectionStart);
const endValid = isValid(userInputSelectionEnd);

if (startValid && endValid) {
unitController.selectionRange = {
start: userInputSelectionStart - offset,
end: userInputSelectionEnd - offset
};
} else {
this.setState({
userInputSelectionStartIsValid: startValid,
userInputSelectionEndIsValid: endValid,
});
}
};

render(): React.ReactNode {

const {
viewRange,
selectionRange,
inputting,
userInputSelectionStartIsValid,
userInputSelectionEndIsValid,
} = this.state;

const sectionClassName = 'view-range-widget-section';
const errorClassName = `${sectionClassName} invalid-input`;

const { start: viewRangeStart, end: viewRangeEnd } = this.getStartAndEnd(viewRange?.start, viewRange?.end);
const { start: selectionRangeStart, end: selectionRangeEnd } = this.getStartAndEnd(selectionRange?.start, selectionRange?.end);

const startValid = inputting ? userInputSelectionStartIsValid : true;
const endValid = inputting ? userInputSelectionEndIsValid : true;

return (
<div className='trace-explorer-item-properties'>
<div className='trace-explorer-panel-content'>
<form onSubmit={this.onSubmit}>
{(!startValid || !endValid) && (
<div className={errorClassName}>
<label htmlFor="errorMessage">
<h4 className='outputs-element-name'><i>Invalid values</i></h4>
</label>
</div>
)}
<div className={sectionClassName}>
<label htmlFor="viewRangeStart">
<h4 className='outputs-element-name'>View Range Start:</h4>
{viewRangeStart}
</label>
</div>
<div className={sectionClassName}>
<label htmlFor="viewRangeEnd">
<h4 className='outputs-element-name'>View Range End:</h4>
{viewRangeEnd}
</label>
</div>
<div className={startValid ? sectionClassName : errorClassName}>
<label htmlFor="selectionRangeStart">
<h4 className='outputs-element-name'>{userInputSelectionStartIsValid ? 'Selection Range Start:' : '* Selection Range Start:'}</h4>
</label>
<input
ref={this.selectionStartInput}
type="number"
defaultValue={selectionRangeStart}
onChange={e => this.onChange(e, 0)}
/>
</div>
<div className={endValid ? sectionClassName : errorClassName}>
<label htmlFor="selectionRangeEnd">
<h4 className='outputs-element-name'>{endValid ? 'Selection Range End:' : '* Selection Range End:'}</h4>
</label>
<input
ref={this.selectionEndInput}
type="number"
defaultValue={selectionRangeEnd}
onChange={e => this.onChange(e, 1)}
/>
</div>
{inputting && (<div className={sectionClassName}>
<input type="submit" value="Submit"/><input type="button" onClick={this.onCancel} value="Cancel"/>
</div>)}
</form>
</div>
</div>
);
}
}
16 changes: 16 additions & 0 deletions packages/react-components/style/trace-explorer.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@
white-space: nowrap;
}

.trace-explorer-panel-content > form > .view-range-widget-section {
margin: 5px 0;
}

.trace-explorer-panel-content > form > .view-range-widget-section.invalid-input {
color: red;
}

.trace-explorer-panel-content > form > .view-range-widget-section > h4 {
margin-bottom: 3px;
}

.trace-explorer-panel-content > form > .view-range-widget-section > input[type=submit] {
margin: 3px;
}

.trace-explorer-panel-content>ul {
list-style-type: none;
padding-inline-start: 0px;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { inject, injectable, postConstruct } from 'inversify';
import { ReactWidget, Widget, Message, WidgetManager } from '@theia/core/lib/browser';
import * as React from 'react';
import { ReactTimeRangeDataWidget } from 'traceviewer-react-components/lib/trace-explorer//trace-explorer-time-range-data-widget';

@injectable()
export class TraceExplorerTimeRangeDataWidget extends ReactWidget {
static ID = 'trace-explorer-time-range-data';
static LABEL = 'Time Range Data';

@inject(WidgetManager) protected readonly widgetManager!: WidgetManager;

@postConstruct()
init(): void {
this.id = TraceExplorerTimeRangeDataWidget.ID;
this.title.label = TraceExplorerTimeRangeDataWidget.LABEL;
this.update();
}

render(): React.ReactNode {
return <div>
<ReactTimeRangeDataWidget
id={this.id}
title={this.title.label}
/>
</div>;
}

protected onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg);
this.update();
}

protected onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.update();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TraceExplorerItemPropertiesWidget } from './trace-explorer-sub-widgets/
import { TraceExplorerOpenedTracesWidget } from './trace-explorer-sub-widgets/theia-trace-explorer-opened-traces-widget';
import { TraceExplorerPlaceholderWidget } from './trace-explorer-sub-widgets/trace-explorer-placeholder-widget';
import { TraceExplorerServerStatusWidget } from './trace-explorer-sub-widgets/trace-explorer-server-status-widget';
import { TraceExplorerTimeRangeDataWidget } from './trace-explorer-sub-widgets/theia-trace-explorer-time-range-data-widget';
import { signalManager, Signals } from 'traceviewer-base/lib/signals/signal-manager';
import { OpenedTracesUpdatedSignalPayload } from 'traceviewer-base/src/signals/opened-traces-updated-signal-payload';
import { TraceServerConnectionStatusService } from '../trace-server-status';
Expand All @@ -20,6 +21,7 @@ export class TraceExplorerWidget extends BaseWidget {
@inject(TraceExplorerItemPropertiesWidget) protected readonly itemPropertiesWidget!: TraceExplorerItemPropertiesWidget;
@inject(TraceExplorerPlaceholderWidget) protected readonly placeholderWidget!: TraceExplorerPlaceholderWidget;
@inject(TraceExplorerServerStatusWidget) protected readonly serverStatusWidget!: TraceExplorerServerStatusWidget;
@inject(TraceExplorerTimeRangeDataWidget) protected readonly timeRangeDataWidget!: TraceExplorerTimeRangeDataWidget;
@inject(ViewContainer.Factory) protected readonly viewContainerFactory!: ViewContainer.Factory;
@inject(TraceServerConnectionStatusService) protected readonly connectionStatusService: TraceServerConnectionStatusService;

Expand All @@ -46,6 +48,7 @@ export class TraceExplorerWidget extends BaseWidget {
child.bind(TraceExplorerOpenedTracesWidget).toSelf();
child.bind(TraceExplorerPlaceholderWidget).toSelf();
child.bind(TraceExplorerServerStatusWidget).toSelf();
child.bind(TraceExplorerTimeRangeDataWidget).toSelf();
child.bind(TraceExplorerItemPropertiesWidget).toSelf();
child.bind(TraceExplorerWidget).toSelf().inSingletonScope();
return child;
Expand All @@ -63,6 +66,7 @@ export class TraceExplorerWidget extends BaseWidget {
});
this.traceViewsContainer.addWidget(this.openedTracesWidget);
this.traceViewsContainer.addWidget(this.viewsWidget);
this.traceViewsContainer.addWidget(this.timeRangeDataWidget);
this.traceViewsContainer.addWidget(this.itemPropertiesWidget);
this.toDispose.push(this.traceViewsContainer);
const layout = this.layout = new PanelLayout();
Expand Down
Loading

0 comments on commit 0310a56

Please # to comment.