diff --git a/viewer-prototype/package.json b/viewer-prototype/package.json index 643222093..c21c28996 100644 --- a/viewer-prototype/package.json +++ b/viewer-prototype/package.json @@ -21,11 +21,13 @@ "ag-grid-community": "^19.0.0", "ag-grid-react": "^19.0.0", "react-grid-layout": "^0.16.6", + "react-virtualized": "^9.21.0", "tsp-typescript-client": "https://github.com/theia-ide/tsp-typescript-client" }, "devDependencies": { "@types/chart.js": "^2.7.40", "@types/react-grid-layout": "^0.16.5", + "@types/react-virtualized": "^9.18.12", "rimraf": "latest", "typescript": "latest" }, diff --git a/viewer-prototype/src/browser/style/trace-explorer.css b/viewer-prototype/src/browser/style/trace-explorer.css new file mode 100644 index 000000000..188e755ab --- /dev/null +++ b/viewer-prototype/src/browser/style/trace-explorer.css @@ -0,0 +1,63 @@ +.trace-explorer-container { + display: grid; + grid-template-columns: 100%; + grid-template-rows: 1fr 1fr 1fr; + grid-row-gap: 10px; +} + +.trace-explorer-opened { + grid-row-start: 1; + display: grid; + grid-template-columns: 100%; + grid-template-rows: 25px 1fr; +} + +.trace-explorer-files { + grid-row-start: 2; + display: grid; + grid-template-columns: 100%; + grid-template-rows: 25px 1fr; +} + +.trace-explorer-analysis { + grid-row-start: 3; + display: grid; + grid-template-columns: 100%; + grid-template-rows: 25px 1fr; +} + +.trace-explorer-panel-title { + grid-row-start: 1; + background-color: rgba(97, 97, 97, 0.5); + color: white; + text-align: center; + line-height: 25px; +} + +.trace-explorer-panel-content { + grid-row-start: 2; + border: 1px solid rgba(97, 97, 97, 0.5); + color: white; + padding-inline-start: 5px; + overflow-x: scroll; + white-space: nowrap; +} + +.trace-explorer-panel-content>ul { + list-style-type: none; + padding-inline-start: 0px; +} + +.trace-list-container, .outputs-list-container { + color: white; + overflow-x: scroll; + white-space: nowrap; +} + +.trace-list-name, .outputs-list-name { + font-weight: bold; +} + +.trace-list-path, .outputs-list-description { + color: rgb(160, 160, 160); +} \ No newline at end of file diff --git a/viewer-prototype/src/browser/trace-explorer/trace-explorer-contribution.ts b/viewer-prototype/src/browser/trace-explorer/trace-explorer-contribution.ts new file mode 100644 index 000000000..369b770ab --- /dev/null +++ b/viewer-prototype/src/browser/trace-explorer/trace-explorer-contribution.ts @@ -0,0 +1,15 @@ +import { AbstractViewContribution } from "@theia/core/lib/browser/shell/view-contribution"; +import { TraceExplorerWidget, TRACE_EXPLORER_ID, TRACE_EXPLORER_LABEL } from "./trace-explorer-widget"; + +export class TraceExplorerContribution extends AbstractViewContribution { + constructor() { + super({ + widgetId: TRACE_EXPLORER_ID, + widgetName: TRACE_EXPLORER_LABEL, + defaultWidgetOptions: { + area: 'left' + }, + toggleCommandId: 'trace-explorer:toggle' + }); + } +} \ No newline at end of file diff --git a/viewer-prototype/src/browser/trace-explorer/trace-explorer-widget.tsx b/viewer-prototype/src/browser/trace-explorer/trace-explorer-widget.tsx new file mode 100644 index 000000000..7e7d9ffc9 --- /dev/null +++ b/viewer-prototype/src/browser/trace-explorer/trace-explorer-widget.tsx @@ -0,0 +1,150 @@ +import { injectable, inject } from 'inversify'; +import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; +import * as React from 'react'; +import { TraceManager } from '../../common/trace-manager'; +import { Trace } from 'tsp-typescript-client/lib/models/trace'; +import { List, ListRowProps } from 'react-virtualized'; +import { OutputDescriptor } from 'tsp-typescript-client/lib/models/output-descriptor'; + +export const TRACE_EXPLORER_ID = 'trace-explorer'; +export const TRACE_EXPLORER_LABEL = 'Trace Explorer'; + +@injectable() +export class TraceExplorerWidget extends ReactWidget { + private OPENED_TRACE_TITLE: string = 'Opened traces'; + private FILE_NAVIGATOR_TITLE: string = 'File navigator'; + private ANALYSIS_TITLE: string = 'Available analysis'; + + private openedTraces: Array = new Array(); + private availableOutputDescriptors: Array = new Array(); + + constructor( + @inject(TraceManager) private traceManager: TraceManager, + ) { + super(); + this.id = TRACE_EXPLORER_ID; + this.title.label = TRACE_EXPLORER_LABEL; + this.toDispose.push(traceManager.traceOpenedSignal(trace => this.onTraceOpened(trace))); + this.toDispose.push(traceManager.traceClosedSignal(trace => this.onTraceClosed(trace))); + this.initialize(); + } + + private onTraceOpened(openedTrace: Trace) { + this.updateOpenedTraces(); + this.updateAvailableAnalysis(openedTrace); + } + + private onTraceClosed(closedTrace: Trace) { + this.updateOpenedTraces(); + this.updateAvailableAnalysis(undefined); + } + + async initialize(): Promise { + this.updateOpenedTraces(); + this.updateAvailableAnalysis(undefined); + } + + protected render(): React.ReactNode { + this.updateOpenedTraces = this.updateOpenedTraces.bind(this); + this.updateAvailableAnalysis = this.updateAvailableAnalysis.bind(this); + this.traceRowRenderer = this.traceRowRenderer.bind(this); + this.outputsRowRenderer = this.outputsRowRenderer.bind(this); + return
+
+
+ {this.OPENED_TRACE_TITLE} +
+
+ +
+
+
+
+ {this.FILE_NAVIGATOR_TITLE} +
+
+ {'List of files'} +
+
+
+
+ {this.ANALYSIS_TITLE} +
+
+ +
+
+
; + } + + private traceRowRenderer(props: ListRowProps): React.ReactNode { + let traceName = ''; + let tracePath = ''; + if (this.openedTraces && this.openedTraces.length && props.index < this.openedTraces.length) { + traceName = this.openedTraces[props.index].name; + tracePath = this.openedTraces[props.index].path; + } + return
+
+ {traceName} +
+
+ {tracePath} +
+
; + } + + private outputsRowRenderer(props: ListRowProps): React.ReactNode { + let outputName = ''; + let outputDescription = ''; + if (this.availableOutputDescriptors && this.availableOutputDescriptors.length && props.index < this.availableOutputDescriptors.length) { + outputName = this.availableOutputDescriptors[props.index].name; + outputDescription = this.availableOutputDescriptors[props.index].description; + } + return
+
+ {outputName} +
+
+ {outputDescription} +
+
; + } + + private async updateOpenedTraces() { + this.openedTraces = this.traceManager.getOpenTraces(); + this.update(); + } + + private async updateAvailableAnalysis(trace: Trace | undefined) { + this.availableOutputDescriptors = new Array(); + if (trace) { + this.availableOutputDescriptors = await this.getOutputDescriptors(trace); + } else { + if (this.openedTraces.length) { + this.availableOutputDescriptors = await this.getOutputDescriptors(this.openedTraces[0]); + } + } + + this.update(); + } + + private async getOutputDescriptors(trace: Trace): Promise { + const outputDescriptors: OutputDescriptor[] = new Array(); + const descriptors = await this.traceManager.getAvailableOutputs(trace.name); + if (descriptors && descriptors.length) { + outputDescriptors.push(...descriptors); + } + return outputDescriptors; + } +} \ No newline at end of file diff --git a/viewer-prototype/src/browser/trace-viewer-frontend-module.ts b/viewer-prototype/src/browser/trace-viewer-frontend-module.ts index 2c8409671..bae5edccf 100644 --- a/viewer-prototype/src/browser/trace-viewer-frontend-module.ts +++ b/viewer-prototype/src/browser/trace-viewer-frontend-module.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { ContainerModule, Container } from 'inversify'; -import { WidgetFactory, OpenHandler, FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { WidgetFactory, OpenHandler, FrontendApplicationContribution, bindViewContribution } from '@theia/core/lib/browser'; import { TraceViewerWidget, TraceViewerWidgetOptions } from './trace-viewer-widget'; import { TraceViewerContribution } from './trace-viewer-contribution'; import { CommandContribution } from '@theia/core/lib/common'; @@ -25,8 +25,16 @@ import 'ag-grid-community/dist/styles/ag-theme-balham-dark.css'; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; import '../../src/browser/style/trace-viewer.css'; +import '../../src/browser/style/trace-explorer.css'; +import { TraceExplorerContribution } from './trace-explorer/trace-explorer-contribution'; +import { TRACE_EXPLORER_ID, TraceExplorerWidget } from './trace-explorer/trace-explorer-widget'; +import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; +import { TraceManager } from '../common/trace-manager'; export default new ContainerModule(bind => { + bind(TspClient).toDynamicValue(() => new TspClient('http://localhost:8080/tsp/api')).inSingletonScope(); + bind(TraceManager).toSelf().inSingletonScope(); + bind(TraceViewerWidget).toSelf(); bind(WidgetFactory).toDynamicValue(context => ({ id: TraceViewerWidget.ID, @@ -43,4 +51,30 @@ export default new ContainerModule(bind => { [CommandContribution, OpenHandler, FrontendApplicationContribution].forEach(serviceIdentifier => bind(serviceIdentifier).toService(TraceViewerContribution) ); + + bindViewContribution(bind, TraceExplorerContribution); + bind(TraceExplorerWidget).toSelf(); + // bind(FrontendApplicationContribution).toService(TraceExplorerContribution); + bind(WidgetFactory).toDynamicValue(context => ({ + id: TRACE_EXPLORER_ID, + createWidget: () => context.container.get(TraceExplorerWidget) + })); + + // bindFileNavigatorPreferences(bind); + // bind(FileNavigatorFilter).toSelf().inSingletonScope(); + + // bind(NavigatorContextKeyService).toSelf().inSingletonScope(); + + // bindViewContribution(bind, FileNavigatorContribution); + // bind(FrontendApplicationContribution).toService(FileNavigatorContribution); + + // bind(KeybindingContext).to(NavigatorActiveContext).inSingletonScope(); + + // bind(FileNavigatorWidget).toDynamicValue(ctx => + // createFileNavigatorWidget(ctx.container) + // ); + // bind(WidgetFactory).toDynamicValue(context => ({ + // id: FILE_NAVIGATOR_ID, + // createWidget: () => context.container.get(FileNavigatorWidget) + // })); }); diff --git a/viewer-prototype/src/browser/trace-viewer-widget.tsx b/viewer-prototype/src/browser/trace-viewer-widget.tsx index 9b2bf3495..6af3140c2 100644 --- a/viewer-prototype/src/browser/trace-viewer-widget.tsx +++ b/viewer-prototype/src/browser/trace-viewer-widget.tsx @@ -45,9 +45,6 @@ export class TraceViewerWidget extends ReactWidget { static LABEL = 'Trace Viewer'; protected readonly uri: Path; - // protected readonly resource: Resource; - private traceManager: TraceManager; - private tspClient: TspClient; private openedTrace: Trace | undefined; private traceInfo: Array = new Array(); private tableColumns: Array = new Array(); @@ -62,11 +59,11 @@ export class TraceViewerWidget extends ReactWidget { private XYTitle: string = ''; constructor( - @inject(TraceViewerWidgetOptions) protected readonly options: TraceViewerWidgetOptions + @inject(TraceViewerWidgetOptions) protected readonly options: TraceViewerWidgetOptions, + @inject(TraceManager) private traceManager: TraceManager, + @inject(TspClient) private tspClient: TspClient ) { super(); - this.traceManager = TraceManager.getInstance(); - this.tspClient = new TspClient('http://localhost:8080/tsp/api'); this.uri = new Path(this.options.traceURI); this.id = 'theia-traceOpen'; this.title.label = 'Trace: ' + this.uri.base; diff --git a/viewer-prototype/src/common/trace-manager.ts b/viewer-prototype/src/common/trace-manager.ts index 01d2f0e81..6cfec1237 100644 --- a/viewer-prototype/src/common/trace-manager.ts +++ b/viewer-prototype/src/common/trace-manager.ts @@ -15,36 +15,54 @@ ********************************************************************************/ import { Trace } from 'tsp-typescript-client/lib/models/trace'; -import { Path } from '@theia/core'; +import { Path, Emitter } from '@theia/core'; import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; import { Query } from 'tsp-typescript-client/lib/models/query/query'; +import { injectable, inject } from 'inversify'; +import { OutputDescriptor } from 'tsp-typescript-client/lib/models/output-descriptor'; +@injectable() export class TraceManager { - private static instance: TraceManager; - private tspClient: TspClient; + // Open signal + private traceOpenedEmitter = new Emitter(); + public traceOpenedSignal = this.traceOpenedEmitter.event; + + // Close signal + private traceClosedEmitter = new Emitter(); + public traceClosedSignal = this.traceClosedEmitter.event; private fOpenTraces: Map; - private constructor() { + private constructor( + @inject(TspClient) private tspClient: TspClient + ) { this.fOpenTraces = new Map(); - this.tspClient = new TspClient('http://localhost:8080/tsp/api'); - } - - static getInstance() { - if (!this.instance) { - this.instance = new TraceManager(); - } - return this.instance; } getOpenTraces() { - return this.fOpenTraces.values; + const openedTraces: Array = new Array(); + for(let entry of this.fOpenTraces) { + openedTraces.push(entry[1]); + } + return openedTraces; } getTrace(traceName: string) { return this.fOpenTraces.get(traceName); } + async getAvailableOutputs(traceName: string): Promise { + const trace = this.fOpenTraces.get(traceName); + if (trace) { + try { + return await this.tspClient.experimentOutputs(trace.UUID); + } catch (e) { + return undefined; + } + } + return undefined; + } + async openTrace(traceURI: Path, traceName?: string): Promise { let name = traceURI.name; if (traceName) { @@ -63,6 +81,7 @@ export class TraceManager { }, [])); // const trace: Trace = await RestRequest.post('http://localhost:8080/tsp/api/traces', params); this.addTrace(trace); + this.traceOpenedEmitter.fire(trace); return trace; } catch (e) { return undefined; @@ -96,7 +115,10 @@ export class TraceManager { // const requestURL = 'http://localhost:8080/tsp/api/traces' + '/' + traceUUID; await this.tspClient.deleteTrace(traceUUID); // await RestRequest.delete(requestURL); - this.removeTrace(traceToClose.name); + const deletedTrace = this.removeTrace(traceToClose.name); + if (deletedTrace) { + this.traceClosedEmitter.fire(deletedTrace); + } // const res = await fetch(requestURL, { // method: 'delete' @@ -113,7 +135,9 @@ export class TraceManager { this.fOpenTraces.set(trace.name, trace); } - private removeTrace(traceName: string) { + private removeTrace(traceName: string): Trace | undefined { + const deletedTrace = this.fOpenTraces.get(traceName); this.fOpenTraces.delete(traceName); + return deletedTrace; } } diff --git a/yarn.lock b/yarn.lock index 4d3da10df..d1160e20a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -591,6 +591,14 @@ dependencies: "@types/react" "*" +"@types/react-virtualized@^9.18.12": + version "9.18.12" + resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.18.12.tgz#541e65c5e0b4629d6a1c6f339171c7943e016ecb" + integrity sha512-Msdpt9zvYlb5Ul4PA339QUkJ0/z2O+gaFxed1rG+2rZjbe6XdYo7jWfJe206KBnjj84DwPPIbPFQCtoGuNwNTQ== + dependencies: + "@types/prop-types" "*" + "@types/react" "*" + "@types/react-virtualized@^9.18.3": version "9.18.11" resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.18.11.tgz#921d20bea4f18de533f89539f313f7688aededdf" @@ -7566,7 +7574,7 @@ react-resizable@1.x: prop-types "15.x" react-draggable "^2.2.6 || ^3.0.3" -react-virtualized@^9.20.0: +react-virtualized@^9.20.0, react-virtualized@^9.21.0: version "9.21.0" resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.0.tgz#8267c40ffb48db35b242a36dea85edcf280a6506" integrity sha512-duKD2HvO33mqld4EtQKm9H9H0p+xce1c++2D5xn59Ma7P8VT7CprfAe5hwjd1OGkyhqzOZiTMlTal7LxjH5yBQ==