From 90ffab162ca4894f3488956356335b1d36f7404f Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Fri, 10 Oct 2025 20:37:58 +0200 Subject: [PATCH] Support generic object data provider Add DataOutputComponent with JSON Editor for the object and Next and Previous buttons for navigation. Update data on change of selectionRange Support DATA provider type in TraceContextComponent. Signed-off-by: Patrick Tasse --- .../react-components/package.json | 1 + .../src/components/data-output-component.tsx | 180 ++++++++++++++++++ .../components/trace-context-component.tsx | 9 + .../style/output-components-style.css | 7 + yarn.lock | 18 ++ 5 files changed, 215 insertions(+) create mode 100644 local-libs/traceviewer-libs/react-components/src/components/data-output-component.tsx diff --git a/local-libs/traceviewer-libs/react-components/package.json b/local-libs/traceviewer-libs/react-components/package.json index cae2c4b9..147a3757 100644 --- a/local-libs/traceviewer-libs/react-components/package.json +++ b/local-libs/traceviewer-libs/react-components/package.json @@ -28,6 +28,7 @@ "@vscode/codicons": "^0.0.29", "chart.js": "^2.8.0", "d3": "^7.1.1", + "json-edit-react": "1.28.2", "lodash": "^4.17.15", "react-chartjs-2": "^2.7.6", "react-contexify": "^5.0.0", diff --git a/local-libs/traceviewer-libs/react-components/src/components/data-output-component.tsx b/local-libs/traceviewer-libs/react-components/src/components/data-output-component.tsx new file mode 100644 index 00000000..3dee0068 --- /dev/null +++ b/local-libs/traceviewer-libs/react-components/src/components/data-output-component.tsx @@ -0,0 +1,180 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { AbstractOutputComponent, AbstractOutputProps, AbstractOutputState } from './abstract-output-component'; +import * as React from 'react'; +import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; +import { ResponseStatus } from 'tsp-typescript-client/lib/models/response/responses'; +import { ObjectModel } from 'tsp-typescript-client/lib/models/object'; +import { isEmpty } from 'lodash'; +import { JSONBigUtils } from 'tsp-typescript-client/lib/utils/jsonbig-utils'; +import { JsonEditor } from 'json-edit-react'; +import debounce from 'lodash.debounce'; +import '../../style/react-contextify.css'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSpinner } from '@fortawesome/free-solid-svg-icons'; + +type DataOutputProps = AbstractOutputProps & {}; + +type DataOuputState = AbstractOutputState & { + model: ObjectModel +}; + +const MENU_ID = 'Data.context.menuId '; + +export class DataOutputComponent extends AbstractOutputComponent { + dataRef: React.RefObject = React.createRef(); + + private _debouncedFetchData = debounce(() => this.fetchData(), 500); + + constructor(props: AbstractOutputProps) { + super(props); + this.state = { + outputStatus: ResponseStatus.RUNNING, + model: { object: {} }, + }; + this.addPinViewOptions(); + } + + componentDidMount(): void { + this.waitAnalysisCompletion(); + } + + async fetchData(navObject?: { [key: string]: any }, scroll?: () => void): Promise { + const useSelectionRange = this.props.outputDescriptor.capabilities?.selectionRange; + const parameters = useSelectionRange && this.props.selectionRange + ? QueryHelper.query({ ...navObject, 'count' : 500, 'selection_range' : { 'start': this.props.selectionRange.getStart(), 'end': this.props.selectionRange.getEnd() } } ) + : QueryHelper.query({ ...navObject, 'count' : 500 }); + const tspClientResponse = await this.props.tspClient.fetchObject( + this.props.traceId, + this.props.outputDescriptor.id, + parameters + ); + const modelResponse = tspClientResponse.getModel(); + if (tspClientResponse.isOk() && modelResponse) { + if (modelResponse.model) { + this.setState({ + outputStatus: modelResponse.status, + model: modelResponse.model + }, scroll); + } else { + this.setState({ + outputStatus: modelResponse.status + }); + } + return modelResponse.status; + } + this.setState({ + outputStatus: ResponseStatus.FAILED + }); + return ResponseStatus.FAILED; + } + + resultsAreEmpty(): boolean { + return isEmpty(this.state.model); + } + + renderMainArea(): React.ReactNode { + return ( + + {this.state.outputStatus === ResponseStatus.COMPLETED ? ( +
+
+ {this.renderPrevButton()} + {this.renderObject()} + {this.renderNextButton()} +
+
+ ) : ( +
+ + Analysis running +
+ )} +
+ ); + } + + private renderObject() { + const replacer = (_key: any, value: any) => { + return (typeof value === 'bigint') ? value.toString() + 'n' : value; + }; + const obj = JSON.parse(JSONBigUtils.stringify(this.state.model.object, null, '\t')); + return ( + + ); + } + private renderPrevButton() { + const navObject = { previous: this.state.model.previous }; + const scroll = () => this.dataRef.current?.scrollTo({ top: this.dataRef.current.scrollHeight, left: 0 }); + return ( + + {navObject.previous != undefined ? ( +


+ ) : ( + <> + )} +
+ ); + } + + private renderNextButton() { + const navObject = { next: this.state.model.next }; + const scroll = () => this.dataRef.current?.scrollTo({ top: 0, left: 0 }); + return ( + + {navObject.next != undefined ? ( +


+ ) : ( + <> + )} +
+ ); + } + + setFocus(): void { + if (document.getElementById(this.props.traceId + this.props.outputDescriptor.id + 'focusContainer')) { + document.getElementById(this.props.traceId + this.props.outputDescriptor.id + 'focusContainer')?.focus(); + } else { + document.getElementById(this.props.traceId + this.props.outputDescriptor.id)?.focus(); + } + } + + protected async waitAnalysisCompletion(): Promise { + let outputStatus = this.state.outputStatus; + const timeout = 500; + while (this.state && outputStatus === ResponseStatus.RUNNING) { + outputStatus = await this.fetchData(); + await new Promise(resolve => setTimeout(resolve, timeout)); + } + } + + componentWillUnmount(): void { + // fix Warning: Can't perform a React state update on an unmounted component + this.setState = (_state, _callback) => undefined; + } + + async componentDidUpdate(prevProps: DataOutputProps): Promise { + if (this.props.selectionRange && this.props.selectionRange !== prevProps.selectionRange) { + this._debouncedFetchData(); + } + } +} diff --git a/local-libs/traceviewer-libs/react-components/src/components/trace-context-component.tsx b/local-libs/traceviewer-libs/react-components/src/components/trace-context-component.tsx index 13e1fd5e..e4b91335 100644 --- a/local-libs/traceviewer-libs/react-components/src/components/trace-context-component.tsx +++ b/local-libs/traceviewer-libs/react-components/src/components/trace-context-component.tsx @@ -22,6 +22,7 @@ import { AbstractOutputProps } from './abstract-output-component'; import * as Messages from 'traceviewer-base/lib/message-manager'; import { signalManager } from 'traceviewer-base/lib/signals/signal-manager'; import { BIMath } from 'timeline-chart/lib/bigint-utils'; +import { DataOutputComponent } from './data-output-component'; import { DataTreeOutputComponent } from './datatree-output-component'; import { cloneDeep } from 'lodash'; import { UnitControllerHistoryHandler } from './utils/unit-controller-history-handler'; @@ -867,6 +868,14 @@ export class TraceContextComponent extends React.Component ); } + case ProviderType.DATA: + return ( + + ); default: return (