From c20c16638d77aca7d6b5b39104e36367e6f3abe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Genevi=C3=A8ve=20Bastien?= Date: Mon, 9 Nov 2020 13:46:49 -0500 Subject: [PATCH 001/373] Split viewer-prototype in packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In prevision of the development of a vscode extension, the code in the viewer-prototype has been mostly moved to 2 new packages: * @trace-viewer/base: Queries the trace server for traces, experiments and possible outputs * @trace-viewer/react-components: Contains the various types of visualization for trace data, queries the trace server for output data to display Those 2 packages can eventually be published to npm. In the viewer-prototype directory only remains the theia specific module contributions. So the application still works as before using theia, it's just that most of the classes used by the trace-viewer have been moved to new npm packages. Signed-off-by: Geneviève Bastien --- base/.eslintrc.js | 26 + base/package.json | 35 + base/src/experiment-manager.ts | 158 ++ base/src/message-manager.ts | 39 + base/src/signal-manager.ts | 32 + base/src/trace-manager.ts | 152 ++ base/src/utils/time-range.ts | 53 + base/src/utils/value-hash.ts | 23 + base/tsconfig.json | 16 + react-components/.eslintrc.js | 39 + react-components/jest.config.json | 23 + react-components/jestSetup.ts | 4 + react-components/package.json | 66 + .../components/abstract-output-component.tsx | 102 ++ .../abstract-tree-output-component.tsx | 52 + .../data-providers/style-provider.ts | 101 ++ .../data-providers/tsp-data-provider.ts | 168 ++ .../src/components/null-output-component.tsx | 27 + .../src/components/table-output-component.tsx | 165 ++ .../components/timegraph-output-component.tsx | 361 ++++ .../components/trace-context-component.tsx | 262 +++ .../__snapshots__/entry-tree.test.tsx.snap | 1486 +++++++++++++++++ .../__snapshots__/table-row.test.tsx.snap | 586 +++++++ .../__tests__/entry-tree.test.tsx | 297 ++++ .../filtrer-tree/__tests__/table-row.test.tsx | 243 +++ .../filtrer-tree/__tests__/utils.test.tsx | 66 + .../utils/filtrer-tree/checkbox-component.tsx | 37 + .../utils/filtrer-tree/column-header.tsx | 5 + .../utils/filtrer-tree/entry-tree.tsx | 44 + .../components/utils/filtrer-tree/filter.tsx | 21 + .../components/utils/filtrer-tree/icons.tsx | 27 + .../components/utils/filtrer-tree/message.tsx | 21 + .../components/utils/filtrer-tree/sort.tsx | 71 + .../utils/filtrer-tree/table-body.tsx | 38 + .../utils/filtrer-tree/table-cell.tsx | 23 + .../utils/filtrer-tree/table-header.tsx | 47 + .../utils/filtrer-tree/table-row.tsx | 90 + .../components/utils/filtrer-tree/table.tsx | 73 + .../utils/filtrer-tree/tree-node.tsx | 7 + .../components/utils/filtrer-tree/tree.tsx | 277 +++ .../components/utils/filtrer-tree/utils.tsx | 57 + .../utils/output-component-style.ts | 14 + .../components/utils/time-axis-component.tsx | 45 + .../utils/time-navigator-component.tsx | 33 + .../utils/timegraph-container-component.tsx | 45 + .../src/components/xy-output-component.tsx | 301 ++++ react-components/src/index.tsx | 3 + .../src/style/chart-line-solid.svg | 1 + .../src/style/output-components-style.css | 96 ++ react-components/src/style/status-bar.css | 22 + .../src/style/trace-context-style.css | 22 + react-components/src/style/trace-explorer.css | 165 ++ react-components/src/style/trace-viewer.css | 16 + react-components/tsconfig.json | 25 + 54 files changed, 6208 insertions(+) create mode 100644 base/.eslintrc.js create mode 100644 base/package.json create mode 100644 base/src/experiment-manager.ts create mode 100644 base/src/message-manager.ts create mode 100644 base/src/signal-manager.ts create mode 100644 base/src/trace-manager.ts create mode 100644 base/src/utils/time-range.ts create mode 100644 base/src/utils/value-hash.ts create mode 100644 base/tsconfig.json create mode 100644 react-components/.eslintrc.js create mode 100644 react-components/jest.config.json create mode 100644 react-components/jestSetup.ts create mode 100644 react-components/package.json create mode 100644 react-components/src/components/abstract-output-component.tsx create mode 100644 react-components/src/components/abstract-tree-output-component.tsx create mode 100644 react-components/src/components/data-providers/style-provider.ts create mode 100644 react-components/src/components/data-providers/tsp-data-provider.ts create mode 100644 react-components/src/components/null-output-component.tsx create mode 100644 react-components/src/components/table-output-component.tsx create mode 100644 react-components/src/components/timegraph-output-component.tsx create mode 100644 react-components/src/components/trace-context-component.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/__tests__/__snapshots__/entry-tree.test.tsx.snap create mode 100644 react-components/src/components/utils/filtrer-tree/__tests__/__snapshots__/table-row.test.tsx.snap create mode 100644 react-components/src/components/utils/filtrer-tree/__tests__/entry-tree.test.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/__tests__/table-row.test.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/__tests__/utils.test.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/checkbox-component.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/column-header.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/entry-tree.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/filter.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/icons.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/message.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/sort.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/table-body.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/table-cell.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/table-header.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/table-row.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/table.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/tree-node.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/tree.tsx create mode 100644 react-components/src/components/utils/filtrer-tree/utils.tsx create mode 100644 react-components/src/components/utils/output-component-style.ts create mode 100644 react-components/src/components/utils/time-axis-component.tsx create mode 100644 react-components/src/components/utils/time-navigator-component.tsx create mode 100644 react-components/src/components/utils/timegraph-container-component.tsx create mode 100644 react-components/src/components/xy-output-component.tsx create mode 100644 react-components/src/index.tsx create mode 100644 react-components/src/style/chart-line-solid.svg create mode 100644 react-components/src/style/output-components-style.css create mode 100644 react-components/src/style/status-bar.css create mode 100644 react-components/src/style/trace-context-style.css create mode 100644 react-components/src/style/trace-explorer.css create mode 100644 react-components/src/style/trace-viewer.css create mode 100644 react-components/tsconfig.json diff --git a/base/.eslintrc.js b/base/.eslintrc.js new file mode 100644 index 00000000..a6115b86 --- /dev/null +++ b/base/.eslintrc.js @@ -0,0 +1,26 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parser: "@typescript-eslint/parser", // Specifies the ESLint parser + parserOptions: { + ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features + sourceType: "module", // Allows for the use of imports + tsconfigRootDir: __dirname, + project: 'tsconfig.json', + projectFolderIgnoreList: [ + '/lib/' + ] + }, + extends: [ + 'plugin:@typescript-eslint/recommended', + '../../configs/base.eslintrc.json', + '../../configs/warnings.eslintrc.json', + '../../configs/errors.eslintrc.json' + ], + ignorePatterns: [ + 'node_modules', + 'lib', + '.eslintrc.js', + 'plugins' + ] +}; diff --git a/base/package.json b/base/package.json new file mode 100644 index 00000000..c1c60661 --- /dev/null +++ b/base/package.json @@ -0,0 +1,35 @@ +{ + "name": "@trace-viewer/base", + "version": "0.0.0", + "description": "Trace Compass base package, contains trace management utilities", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/theia-ide/theia-trace-extension" + }, + "files": [ + "lib", + "src" + ], + "dependencies": { + "tsp-typescript-client": "next" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^3.4.0", + "@typescript-eslint/parser": "^3.4.0", + "eslint": "^7.3.0", + "eslint-plugin-import": "^2.21.2", + "eslint-plugin-no-null": "^1.0.2", + "eslint-plugin-react": "^7.20.0", + "rimraf": "latest", + "typescript": "latest" + }, + "scripts": { + "build": "tsc", + "clean": "rimraf lib", + "lint": "eslint .", + "prepare": "yarn clean && yarn build", + "test": "echo 'test'", + "watch": "tsc -w" + } +} \ No newline at end of file diff --git a/base/src/experiment-manager.ts b/base/src/experiment-manager.ts new file mode 100644 index 00000000..3260d37a --- /dev/null +++ b/base/src/experiment-manager.ts @@ -0,0 +1,158 @@ +import { Trace } from 'tsp-typescript-client/lib/models/trace'; +import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; +import { Query } from 'tsp-typescript-client/lib/models/query/query'; +import { OutputDescriptor } from 'tsp-typescript-client/lib/models/output-descriptor'; +import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; +import { TspClientResponse } from 'tsp-typescript-client/lib/protocol/tsp-client-response'; +import { signalManager, Signals } from './signal-manager'; + +export class ExperimentManager { + + private fOpenExperiments: Map = new Map(); + private fTspClient: TspClient; + + constructor( + tspClient: TspClient + ) { + this.fTspClient = tspClient; + } + + /** + * Get an array of opened experiments + * @returns Array of experiment + */ + async getOpenedExperiments(): Promise { + const openedExperiments: Array = []; + // Look on the server for opened experiments + const experimentResponse = await this.fTspClient.fetchExperiments(); + if (experimentResponse.isOk()) { + openedExperiments.push(...experimentResponse.getModel()); + } + return openedExperiments; + } + + /** + * Get a specific experiment information + * @param experimentUUID experiment UUID + */ + async getExperiment(experimentUUID: string): Promise { + // Check if the experiment is in "cache" + let experiment = this.fOpenExperiments.get(experimentUUID); + + // If the experiment is undefined, check on the server + if (!experiment) { + const experimentResponse = await this.fTspClient.fetchExperiment(experimentUUID); + if (experimentResponse.isOk()) { + experiment = experimentResponse.getModel(); + } + } + return experiment; + } + + /** + * Get an array of OutputDescriptor for a given experiment + * @param experimentUUID experiment UUID + */ + async getAvailableOutputs(experimentUUID: string): Promise { + // Check if the experiment is opened + const experiment = this.fOpenExperiments.get(experimentUUID); + if (experiment) { + const outputsResponse = await this.fTspClient.experimentOutputs(experiment.UUID); + return outputsResponse.getModel(); + } + return undefined; + } + + /** + * Open a given experiment on the server + * @param experimentURI experiment URI to open + * @param experimentName Optional name for the experiment. If not specified the URI name is used + * @returns The opened experiment + */ + async openExperiment(experimentName: string, traces: Array): Promise { + const name = experimentName; + + const traceURIs = new Array(); + for (let i = 0; i < traces.length; i++) { + traceURIs.push(traces[i].UUID); + } + + const experimentResponse = await this.fTspClient.createExperiment(new Query({ + 'name': name, + 'traces': traceURIs + })); + const opendExperiment = experimentResponse.getModel(); + if (opendExperiment && experimentResponse.isOk()) { + this.addExperiment(opendExperiment); + signalManager().emit(Signals.EXPERIMENT_OPENED, {experiment: opendExperiment}); + return opendExperiment; + } else if (opendExperiment && experimentResponse.getStatusCode() === 409) { + // Repost with a suffix as long as there are conflicts + const handleConflict = async function (tspClient: TspClient, tryNb: number): Promise> { + const suffix = '(' + tryNb + ')'; + return tspClient.createExperiment(new Query({ + 'name': name + suffix, + 'traces': traceURIs + })); + }; + let conflictResolutionResponse = experimentResponse; + let i = 1; + while (conflictResolutionResponse.getStatusCode() === 409) { + conflictResolutionResponse = await handleConflict(this.fTspClient, i); + i++; + } + const experiment = conflictResolutionResponse.getModel(); + if (experiment && conflictResolutionResponse.isOk()) { + this.addExperiment(experiment); + signalManager().emit(Signals.EXPERIMENT_OPENED, {experiment: experiment}); + return experiment; + } + } + // TODO Handle any other experiment open errors + return undefined; + } + + /** + * Update the experiment with the latest info from the server. + * @param experimentName experiment name to update + * @returns The updated experiment or undefined if the experiment was not open previously + */ + async updateExperiment(experimentUUID: string): Promise { + const currentExperiment = this.fOpenExperiments.get(experimentUUID); + if (currentExperiment) { + const experimentResponse = await this.fTspClient.fetchExperiment(currentExperiment.UUID); + const experiment = experimentResponse.getModel(); + if (experiment && experimentResponse.isOk) { + this.fOpenExperiments.set(experimentUUID, experiment); + return experiment; + } + } + + return undefined; + } + + /** + * Close the given on the server + * @param experimentUUID experiment UUID + */ + async closeExperiment(experimentUUID: string): Promise { + const experimentToClose = this.fOpenExperiments.get(experimentUUID); + if (experimentToClose) { + await this.fTspClient.deleteExperiment(experimentUUID); + const deletedExperiment = this.removeExperiment(experimentUUID); + if (deletedExperiment) { + signalManager().emit(Signals.EXPERIMENT_CLOSED, {experiment: deletedExperiment}); + } + } + } + + private addExperiment(experiment: Experiment) { + this.fOpenExperiments.set(experiment.UUID, experiment); + } + + private removeExperiment(experimentUUID: string): Experiment | undefined { + const deletedExperiment = this.fOpenExperiments.get(experimentUUID); + this.fOpenExperiments.delete(experimentUUID); + return deletedExperiment; + } +} diff --git a/base/src/message-manager.ts b/base/src/message-manager.ts new file mode 100644 index 00000000..7778b013 --- /dev/null +++ b/base/src/message-manager.ts @@ -0,0 +1,39 @@ +export enum MessageCategory { + TRACE_CONTEXT, + SERVER_MESSAGE, + SERVER_STATUS, +} + +export enum MessageSeverity { + ERROR, + WARNING, + INFO, + DEBUG +} + +export interface StatusMessage { + text: string; + category?: MessageCategory; + severity?: MessageSeverity; +} + +export declare interface MessageManager { + + addStatusMessage(messageKey: string, message: StatusMessage): void; + removeStatusMessage(messageKey: string): void; + +} + +export class MessageManager implements MessageManager { + + addStatusMessage(messageKey: string, {text, + category = MessageCategory.SERVER_MESSAGE, + severity = MessageSeverity.INFO }: StatusMessage): void { + console.log('New status message', messageKey, text, category, severity); + } + + removeStatusMessage(messageKey: string): void { + console.log('Removing status message status message', messageKey); + } + +} diff --git a/base/src/signal-manager.ts b/base/src/signal-manager.ts new file mode 100644 index 00000000..6be9afe7 --- /dev/null +++ b/base/src/signal-manager.ts @@ -0,0 +1,32 @@ +import { EventEmitter } from 'events'; + +export declare interface SignalManager { + + fireTooltipSignal(tooltip: { [key: string]: string }): void; + +} + +export const Signals = { + TRACE_OPENED : 'trace opened', + TRACE_CLOSED : 'trace closed', + EXPERIMENT_OPENED: 'experiment opened', + EXPERIMENT_CLOSED: 'experiment closed', + EXPERIMENT_SELECTED: 'experiment selected', + TOOLTIP_UPDATED: 'tooltip updated' +}; + +export class SignalManager extends EventEmitter implements SignalManager { + + fireTooltipSignal(tooltip: { [key: string]: string; }): void { + this.emit(Signals.TOOLTIP_UPDATED, {tooltip}); + } + +} + +let instance: SignalManager = new SignalManager(); + +export const setSignalManagerInstance = (sm: SignalManager) => { + instance = sm; +}; + +export const signalManager = (): SignalManager => instance; diff --git a/base/src/trace-manager.ts b/base/src/trace-manager.ts new file mode 100644 index 00000000..287232d1 --- /dev/null +++ b/base/src/trace-manager.ts @@ -0,0 +1,152 @@ + +import { Trace } from 'tsp-typescript-client/lib/models/trace'; +import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; +import { Query } from 'tsp-typescript-client/lib/models/query/query'; +import { OutputDescriptor } from 'tsp-typescript-client/lib/models/output-descriptor'; +import { TspClientResponse } from 'tsp-typescript-client/lib/protocol/tsp-client-response'; +import { signalManager, Signals } from './signal-manager'; + +export class TraceManager { + + private fOpenTraces: Map = new Map(); + private fTspClient: TspClient; + + constructor( + tspClient: TspClient + ) { + this.fTspClient = tspClient; + } + + /** + * Get an array of opened traces + * @returns Array of Trace + */ + async getOpenedTraces(): Promise { + const openedTraces: Array = []; + // Look on the server for opened trace + const tracesResponse = await this.fTspClient.fetchTraces(); + if (tracesResponse.isOk()) { + openedTraces.push(...tracesResponse.getModel()); + } + return openedTraces; + } + + /** + * Get a specific trace information + * @param traceUUID Trace UUID + */ + async getTrace(traceUUID: string): Promise { + // Check if the trace is in "cache" + let trace = this.fOpenTraces.get(traceUUID); + + // If the trace is undefined, check on the server + if (!trace) { + const traceResponse = await this.fTspClient.fetchTrace(traceUUID); + if (traceResponse.isOk()) { + trace = traceResponse.getModel(); + } + } + return trace; + } + + /** + * Get an array of OutputDescriptor for a given trace + * @param traceUUID Trace UUID + */ + async getAvailableOutputs(traceUUID: string): Promise { + // Check if the trace is opened + const trace = this.fOpenTraces.get(traceUUID); + if (trace) { + const outputsResponse = await this.fTspClient.experimentOutputs(trace.UUID); + return outputsResponse.getModel(); + } + return undefined; + } + + /** + * Open a given trace on the server + * @param traceURI Trace URI to open + * @param traceName Optional name for the trace. If not specified the URI name is used + * @returns The opened trace + */ + async openTrace(traceURI: string, traceName?: string): Promise { + const traceResponse = await this.fTspClient.openTrace(new Query({ + 'name': traceName, + 'uri': traceURI + })); + + const openedTrace = traceResponse.getModel(); + if (openedTrace && traceResponse.isOk()) { + this.addTrace(openedTrace); + signalManager().emit(Signals.TRACE_OPENED, {trace: openedTrace}); + return openedTrace; + } else if (openedTrace && traceResponse.getStatusCode() === 409) { + // Repost with a suffix as long as there are conflicts + const handleConflict = async function (tspClient: TspClient, tryNb: number): Promise> { + const suffix = '(' + tryNb + ')'; + return tspClient.openTrace(new Query({ + 'name': name + suffix, + 'uri': traceURI + })); + }; + let conflictResolutionResponse = traceResponse; + let i = 1; + while (conflictResolutionResponse.getStatusCode() === 409) { + conflictResolutionResponse = await handleConflict(this.fTspClient, i); + i++; + } + const trace = conflictResolutionResponse.getModel(); + if (trace && conflictResolutionResponse.isOk()) { + this.addTrace(trace); + signalManager().emit(Signals.TRACE_OPENED, {trace: openedTrace}); + return trace; + } + } + // TODO Handle trace open errors + return undefined; + } + + /** + * Update the trace with the latest info from the server. + * @param traceName Trace name to update + * @returns The updated trace or undefined if the trace was not open previously + */ + async updateTrace(traceUUID: string): Promise { + const currentTrace = this.fOpenTraces.get(traceUUID); + if (currentTrace) { + const traceResponse = await this.fTspClient.fetchTrace(currentTrace.UUID); + const trace = traceResponse.getModel(); + if (trace && traceResponse.isOk) { + this.fOpenTraces.set(traceUUID, trace); + return trace; + } + } + + return undefined; + } + + /** + * Close the given on the server + * @param traceUUID Trace UUID + */ + async closeTrace(traceUUID: string): Promise { + const traceToClose = this.fOpenTraces.get(traceUUID); + if (traceToClose) { + await this.fTspClient.deleteTrace(traceUUID); + const deletedTrace = this.removeTrace(traceUUID); + if (deletedTrace) { + signalManager().emit(Signals.TRACE_CLOSED, {trace: deletedTrace}); + } + } + } + + private addTrace(trace: Trace) { + this.fOpenTraces.set(trace.UUID, trace); + } + + private removeTrace(traceUUID: string): Trace | undefined { + const deletedTrace = this.fOpenTraces.get(traceUUID); + this.fOpenTraces.delete(traceUUID); + return deletedTrace; + } +} diff --git a/base/src/utils/time-range.ts b/base/src/utils/time-range.ts new file mode 100644 index 00000000..dae6c91d --- /dev/null +++ b/base/src/utils/time-range.ts @@ -0,0 +1,53 @@ +export class TimeRange { + private start: number; + private end: number; + private offset: number | undefined; + + /** + * Constructor. + * @param start Range start time + * @param end Range end time + * @param offset Time offset, if this is defined the start and end time should be relative to this value + */ + constructor(start: number, end: number, offset?: number) { + this.start = start; + this.end = end; + this.offset = offset; + } + + /** + * Get the range start time. + * If an offset is present the return value is start + offset. + */ + public getstart(): number { + if (this.offset !== undefined) { + return this.start + this.offset; + } + return this.start; + } + + /** + * Get the range end time. + * If an offset is present the return value is end + offset. + */ + public getEnd(): number { + if (this.offset !== undefined) { + return this.end + this.offset; + } + return this.end; + } + + /** + * Get range duration + */ + public getDuration(): number { + return this.end - this.start; + } + + /** + * Return the time offset + */ + public getOffset(): number | undefined { + return this.offset; + } +} diff --git a/base/src/utils/value-hash.ts b/base/src/utils/value-hash.ts new file mode 100644 index 00000000..2d393680 --- /dev/null +++ b/base/src/utils/value-hash.ts @@ -0,0 +1,23 @@ +/** + * Transforms a string value to a numerical value, either parsing the string as + * a number or by running some kind of hash function on the string. This + * function for a same string will always return the same result. + * + * @param str the string value to hash + */ +const hash = (str: string): number => { + const int = parseInt(str); + if (!isNaN(int)) { + return int; + } + // Based on https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript + let hashCode = 0; + for (let i = 0; i < str.length; i++) { + const chr = str.charCodeAt(i); + hashCode = ((hashCode << 5) - hashCode) + chr; + hashCode |= 0; // Convert to 32bit integer + } + return hashCode; +}; + +export default hash; diff --git a/base/tsconfig.json b/base/tsconfig.json new file mode 100644 index 00000000..5a096972 --- /dev/null +++ b/base/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "strict": true, + "sourceMap": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "module": "commonjs", + "target": "ES2020", + "outDir": "lib", + "declaration": true, + "skipLibCheck": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/react-components/.eslintrc.js b/react-components/.eslintrc.js new file mode 100644 index 00000000..af429789 --- /dev/null +++ b/react-components/.eslintrc.js @@ -0,0 +1,39 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parser: "@typescript-eslint/parser", // Specifies the ESLint parser + parserOptions: { + ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features + sourceType: "module", // Allows for the use of imports + ecmaFeatures: { + jsx: true // Allows for the parsing of JSX + } + }, + settings: { + react: { + version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use + } + }, + extends: [ + 'plugin:react/recommended', + 'plugin:@typescript-eslint/recommended', + '../../configs/base.eslintrc.json', + '../../configs/warnings.eslintrc.json', + '../../configs/errors.eslintrc.json' + ], + ignorePatterns: [ + 'node_modules', + 'lib', + '.eslintrc.js', + 'plugins', + '**/*/__tests__', + 'jestSetup.ts' + ], + parserOptions: { + tsconfigRootDir: __dirname, + project: 'tsconfig.json', + projectFolderIgnoreList: [ + '/lib/' + ] + } +}; diff --git a/react-components/jest.config.json b/react-components/jest.config.json new file mode 100644 index 00000000..4eee85e6 --- /dev/null +++ b/react-components/jest.config.json @@ -0,0 +1,23 @@ +{ + "globals": { + "ts-jest": { + "tsconfig": "tsconfig.json" + } + }, + "transform": { + "^.+\\.tsx?$": "ts-jest" + }, + "testEnvironment": "jsdom", + "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json", + "node" + ], + "setupFilesAfterEnv": [ + "./jestSetup.ts" + ] +} diff --git a/react-components/jestSetup.ts b/react-components/jestSetup.ts new file mode 100644 index 00000000..acd71d49 --- /dev/null +++ b/react-components/jestSetup.ts @@ -0,0 +1,4 @@ +import Enzyme from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +Enzyme.configure({ adapter: new Adapter() }); \ No newline at end of file diff --git a/react-components/package.json b/react-components/package.json new file mode 100644 index 00000000..37ced015 --- /dev/null +++ b/react-components/package.json @@ -0,0 +1,66 @@ +{ + "name": "@trace-viewer/react-components", + "version": "0.0.0", + "description": "Trace Compass react components", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/theia-ide/theia-trace-extension" + }, + "files": [ + "lib", + "src" + ], + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^1.2.17", + "@fortawesome/free-solid-svg-icons": "^5.8.1", + "@fortawesome/react-fontawesome": "^0.1.4", + "@trace-viewer/base": "0.0.0", + "ag-grid-community": "^20.2.0", + "ag-grid-react": "^20.2.0", + "chart.js": "^2.8.0", + "lodash": "^4.17.15", + "react-chartjs-2": "^2.7.6", + "react-grid-layout": "^1.1.0", + "react-modal": "^3.8.1", + "react-virtualized": "^9.21.0", + "semantic-ui-css": "^2.4.1", + "semantic-ui-react": "^0.86.0", + "timeline-chart": "next", + "tsp-typescript-client": "next" + }, + "devDependencies": { + "@testing-library/react": "^10.4.6", + "@types/chart.js": "^2.7.52", + "@types/enzyme": "^3.10.5", + "@types/enzyme-adapter-react-16": "^1.0.6", + "@types/jest": "^26.0.4", + "@types/lodash": "^4.14.142", + "@types/react-grid-layout": "^0.16.7", + "@types/react-modal": "^3.8.2", + "@types/react-test-renderer": "^16.9.2", + "@types/react-virtualized": "^9.21.1", + "@typescript-eslint/eslint-plugin": "^3.4.0", + "@typescript-eslint/parser": "^3.4.0", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.2", + "eslint": "^7.3.0", + "eslint-plugin-import": "^2.21.2", + "eslint-plugin-no-null": "^1.0.2", + "eslint-plugin-react": "^7.20.0", + "jest": "^26.1.0", + "react-test-renderer": "^16.13.1", + "rimraf": "latest", + "ts-jest": "^26.3.0", + "typescript": "latest" + }, + "scripts": { + "assets": "mkdir -p ./lib/style && cp -r ./src/style/* ./lib/style", + "build": "tsc && yarn run assets", + "clean": "rimraf lib", + "lint": "eslint .", + "prepare": "yarn clean && yarn build", + "test": "jest --config jest.config.json", + "watch": "tsc -w" + } +} diff --git a/react-components/src/components/abstract-output-component.tsx b/react-components/src/components/abstract-output-component.tsx new file mode 100644 index 00000000..5286a805 --- /dev/null +++ b/react-components/src/components/abstract-output-component.tsx @@ -0,0 +1,102 @@ +import * as React from 'react'; +import { OutputDescriptor } from 'tsp-typescript-client/lib/models/output-descriptor'; +import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { TimeGraphUnitController } from 'timeline-chart/lib/time-graph-unit-controller'; +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'; + +export interface AbstractOutputProps { + tspClient: TspClient; + traceId: string; + range: TimeRange; + viewRange: TimeRange; + selectionRange: TimeRange | undefined; + resolution?: number; + outputDescriptor: OutputDescriptor; + style: OutputComponentStyle; + // WidthProvider (react-grid-layout version 0.16.7) has a bug. + // Workaround for it needs width to be explicitly passed + // https://github.com/STRML/react-grid-layout/issues/961 + widthWPBugWorkaround: number; + onOutputRemove: (outputId: string) => void; + // TODO Not sure + unitController: TimeGraphUnitController; + onSelectionRangeChange?: () => void; + onViewRangeChange?: () => void; + className?: string; + onMouseUp?: VoidFunction; + onMouseDown?: VoidFunction; + onTouchStart?: VoidFunction; + onTouchEnd?: VoidFunction; +} + +export interface AbstractOutputState { + outputStatus: string; + styleModel?: OutputStyleModel +} + +export abstract class AbstractOutputComponent

extends React.Component { + + private mainAreaContainer: React.RefObject; + private readonly HANDLE_WIDTH = 30; + + constructor(props: P) { + super(props); + this.mainAreaContainer = React.createRef(); + this.closeComponent = this.closeComponent.bind(this); + this.renderTitleBar = this.renderTitleBar.bind(this); + } + + render(): JSX.Element { + const localStyle = Object.assign({},this.props.style); + localStyle.width = this.props.widthWPBugWorkaround; + return

+
+ {this.renderTitleBar()} +
+
+ {this.renderMainArea()} +
+ {this.props.children} +
; + } + + private renderTitleBar(): React.ReactNode { + const outputName = this.props.outputDescriptor.name; + return + +
+ {outputName} +
+
; + } + + private closeComponent() { + this.props.onOutputRemove(this.props.outputDescriptor.id); + } + + public getMainAreaWidth(): number { + if (this.mainAreaContainer.current) { + return this.mainAreaContainer.current.clientWidth; + } + return 1000; + } + + public getHandleWidth(): number { + return this.HANDLE_WIDTH; + } + + abstract renderMainArea(): React.ReactNode; +} diff --git a/react-components/src/components/abstract-tree-output-component.tsx b/react-components/src/components/abstract-tree-output-component.tsx new file mode 100644 index 00000000..bf7309ba --- /dev/null +++ b/react-components/src/components/abstract-tree-output-component.tsx @@ -0,0 +1,52 @@ +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'; + +export abstract class AbstractTreeOutputComponent

extends AbstractOutputComponent { + renderMainArea(): React.ReactNode { + const treeWidth = this.props.widthWPBugWorkaround - this.getHandleWidth() - this.props.style.chartWidth; + return +

{ this.synchronizeTreeScroll(); }} + style={{ width: treeWidth, height: this.props.style.height }} + > + {this.renderTree()} +
+
+ {this.renderChart()} +
+ ; + } + + treeRef: React.RefObject = React.createRef(); + + abstract renderTree(): React.ReactNode; + + abstract renderChart(): React.ReactNode; + + abstract synchronizeTreeScroll(): void; + + protected async waitAnalysisCompletion(): Promise { + const traceUUID = this.props.traceId; + const tspClient = this.props.tspClient; + const outPutId = this.props.outputDescriptor.id; + + // TODO Use the output descriptor to find out if the analysis is completed + const xyTreeParameters = QueryHelper.selectionTimeQuery( + QueryHelper.splitRangeIntoEqualParts(this.props.range.getstart(), this.props.range.getEnd(), 1120), []); + let xyTreeResponse = (await tspClient.fetchXYTree(traceUUID, outPutId, xyTreeParameters)).getModel(); + while (xyTreeResponse.status === ResponseStatus.RUNNING) { + xyTreeResponse = (await tspClient.fetchXYTree(traceUUID, outPutId, xyTreeParameters)).getModel(); + } + this.setState({ + outputStatus: xyTreeResponse.status + }); + } + + componentWillUnmount(): void { + // fix Warning: Can't perform a React state update on an unmounted component + this.setState = (_state, _callback) => undefined; + + } +} diff --git a/react-components/src/components/data-providers/style-provider.ts b/react-components/src/components/data-providers/style-provider.ts new file mode 100644 index 00000000..34bd0cde --- /dev/null +++ b/react-components/src/components/data-providers/style-provider.ts @@ -0,0 +1,101 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; +import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; +import { OutputElementStyle } from 'tsp-typescript-client/lib/models/styles'; + +export class StyleProvider { + private tspClient: TspClient; + private traceId: string; + private outputId: string; + + private tmpStyleObject: { [key: string]: { [key: string]: { [key: string]: any } } }; + + private styles: { [key: string]: OutputElementStyle } | undefined; + + constructor(outputId: string, traceId: string, tspClient: TspClient) { + this.outputId = outputId; + this.tspClient = tspClient; + this.traceId = traceId; + const threadStyleObject = { + '0': { + color: '646464', + height: 0.33 + }, + '2': { + color: '00C800', + height: 1 + }, + '3': { + color: '0000C8', + height: 1 + }, + '4': { + color: 'C80064', + height: 0.75 + }, + '1': { + color: 'C8C800', + height: 0.5 + }, + '5': { + color: 'C86400', + height: 0.5 + }, + '6': { + color: 'C8C8C8', + height: 0.5 + } + }; + + const resourceStyleObject = { + '0': { + color: 'C8C8C8', + height: 0.66 + }, + '2': { + color: '00C800', + height: 1 + }, + '4': { + color: '0000C8', + height: 1 + }, + '16': { + color: 'C80064', + height: 0.75 + }, + '8': { + color: 'C89664', + height: 1 + }, + '1': { + color: 'C8C800', + height: 1 + } + }; + this.tmpStyleObject = { + 'org.eclipse.tracecompass.internal.analysis.os.linux.core.threadstatus.ThreadStatusDataProvider': threadStyleObject, + 'org.eclipse.tracecompass.internal.analysis.os.linux.core.threadstatus.ResourcesStatusDataProvider': resourceStyleObject + }; + } + + /** + * Get the style for a specific output + * @param forceUpdate Force the update of the current cached styles from the server + */ + public async getStyles(forceUpdate?: boolean): Promise<{ [key: string]: OutputElementStyle }> { + if (!this.styles || forceUpdate) { + const styleResponse = await this.tspClient.fetchStyles(this.traceId, this.outputId, QueryHelper.query()); + const styleModel = styleResponse.getModel().model; + const styles = styleModel.styles; + this.styles = styles; + return styles; + } + return this.styles; + } + + public getStylesTmp(_forceUpdate?: boolean): { [key: string]: { [key: string]: any } } { + const styles = this.tmpStyleObject[this.outputId]; + return styles ? styles : {}; + } +} diff --git a/react-components/src/components/data-providers/tsp-data-provider.ts b/react-components/src/components/data-providers/tsp-data-provider.ts new file mode 100644 index 00000000..62daef78 --- /dev/null +++ b/react-components/src/components/data-providers/tsp-data-provider.ts @@ -0,0 +1,168 @@ +import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; +import { TimeGraphEntry, TimeGraphRow, TimeGraphState } from 'tsp-typescript-client/lib/models/timegraph'; +import { TimelineChart } from 'timeline-chart/lib/time-graph-model'; +import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; +import { OutputElementStyle } from 'tsp-typescript-client/lib/models/styles'; + +export class TspDataProvider { + + protected canvasDisplayWidth: number | undefined; + + private client: TspClient; + private outputId: string; + private traceUUID: string; + private timeGraphEntries: TimeGraphEntry[]; + private timeGraphRows: TimeGraphRow[]; + + public totalRange: number; + + constructor(client: TspClient, traceUUID: string, outputId: string, canvasDisplayWidth?: number) { + this.timeGraphEntries = []; + this.timeGraphRows = []; + this.canvasDisplayWidth = canvasDisplayWidth; + this.client = client; + this.outputId = outputId; + this.traceUUID = traceUUID; + this.totalRange = 0; + } + + async getData(ids: number[], entries: TimeGraphEntry[], viewRange?: TimelineChart.TimeGraphRange, resolution?: number): Promise { + this.timeGraphEntries = [...entries]; + if (!this.timeGraphEntries.length) { + return { + id: 'model', + totalLength: this.totalRange, + arrows: [], + rows: [], + data: {} + }; + } + + this.totalRange = this.timeGraphEntries[0].end - this.timeGraphEntries[0].start; // 1332170682540133097 - starttime + let statesParameters = QueryHelper.selectionTimeQuery(QueryHelper.splitRangeIntoEqualParts(1332170682440133097, 1332170682540133097, 1120), ids); + if (viewRange && resolution) { + const start = viewRange.start + this.timeGraphEntries[0].start; + const end = viewRange.end + this.timeGraphEntries[0].start; + statesParameters = QueryHelper.selectionTimeQuery(QueryHelper.splitRangeIntoEqualParts(Math.trunc(start), Math.trunc(end), resolution), ids); + } + const stateResponse = (await this.client.fetchTimeGraphStates(this.traceUUID, + this.outputId, statesParameters)).getModel(); + + this.timeGraphRows = stateResponse.model.rows; + this.timeGraphRowsOrdering(ids); + + // the start time which is normalized to logical 0 in timeline chart. + const chartStart = this.timeGraphEntries[0].start; + const rows: TimelineChart.TimeGraphRowModel[] = []; + this.timeGraphRows.forEach((row: TimeGraphRow) => { + const rowId: number = row.entryId; + const entry = this.timeGraphEntries.find(tgEntry => tgEntry.id === rowId); + if (entry) { + rows.push(this.getRowModel(row, chartStart, rowId, entry)); + } + }); + + return { + id: 'model', + totalLength: this.totalRange, + arrows: [], + rows, + data: { + originalStart: chartStart + } + }; + } + + private timeGraphRowsOrdering(orderedIds: number[]) { + const newTimeGraphRows: TimeGraphRow[] = []; + orderedIds.forEach(id => { + const timeGraphRow = this.timeGraphRows.find(row => row.entryId === id); + if (timeGraphRow) { + newTimeGraphRows.push(timeGraphRow); + } else { + const emptyRow: TimeGraphRow = { states: [{ start: 0, end: 0, label: '', tags: 0 }], entryId: id }; + newTimeGraphRows.push(emptyRow); + } + }); + + this.timeGraphRows = newTimeGraphRows; + } + + private getDefaultForGapStyle() { + // Default color and height for the GAP state + return { + parentKey: '', + values: { + color: 0xCACACA, + height: 1.0 + } + }; + + } + + private getRowModel(row: TimeGraphRow, chartStart: number, rowId: number, entry: TimeGraphEntry) { + + let gapStyle: OutputElementStyle; + if (!entry.style) { + gapStyle = this.getDefaultForGapStyle(); + } else { + gapStyle = entry.style; + } + const states: TimelineChart.TimeGraphRowElementModel[] = []; + let prevPossibleState = entry.start; + let nextPossibleState = entry.end; + row.states.forEach((state: TimeGraphState, idx: number) => { + const end = state.end - chartStart; + if (state.style) { + states.push({ + id: row.entryId + '-' + idx, + label: state.label || '', + range: { + start: state.start - chartStart, + end + }, + data: { + style: state.style + } + }); + this.totalRange = this.totalRange < end ? end : this.totalRange; + } else { + const nextIndex = idx + 1; + const nextState = row.states[nextIndex]; + if (nextState && nextState.start > state.end + 1) { + // Add gap state + states.push({ + // TODO: We should probably remove id from state. We don't use it anywhere. + id: row.entryId + '-' + idx, + label: '', + range: { + start: end, + end: nextState.start - chartStart + }, + data: { + style: gapStyle + } + }); + } + } + if (idx === 0) { + prevPossibleState = state.start - chartStart; + } + if (idx === row.states.length - 1) { + nextPossibleState = state.end - chartStart; + } + }); + + return { + id: rowId, + name: entry.labels[0], // 'row' + rowId, + range: { + start: entry.start - chartStart, + end: entry.end - chartStart + }, + states, + prevPossibleState, + nextPossibleState + }; + } +} diff --git a/react-components/src/components/null-output-component.tsx b/react-components/src/components/null-output-component.tsx new file mode 100644 index 00000000..f8109707 --- /dev/null +++ b/react-components/src/components/null-output-component.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { AbstractOutputComponent, AbstractOutputProps, AbstractOutputState } from './abstract-output-component'; + +type NullOutputState = AbstractOutputState & { +}; + +type NullOutputProps = AbstractOutputProps & { +}; + +export class NullOutputComponent extends AbstractOutputComponent { + constructor(props: NullOutputProps) { + super(props); + } + renderMainArea(): React.ReactNode { + const treeWidth = this.props.widthWPBugWorkaround - this.getHandleWidth() - this.props.style.chartWidth; + return +
+ {''} +
+
+ {'Not implemented yet!'} +
+
; + } + } diff --git a/react-components/src/components/table-output-component.tsx b/react-components/src/components/table-output-component.tsx new file mode 100644 index 00000000..8f0b1ba3 --- /dev/null +++ b/react-components/src/components/table-output-component.tsx @@ -0,0 +1,165 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { AbstractOutputComponent, AbstractOutputProps, AbstractOutputState } from './abstract-output-component'; +import * as React from 'react'; +import { AgGridReact } from 'ag-grid-react'; +import { ColDef, IDatasource, GridReadyEvent } from 'ag-grid-community'; +import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; +import { cloneDeep } from 'lodash'; + +type TableOuputState = AbstractOutputState & { + tableColumns: ColDef[]; +}; + +type TableOutputProps = AbstractOutputProps & { + cacheBlockSize?: number; + maxBlocksInCache?: number; + columnWidth?: number; + blockLoadDebounce?: number; + tableHeight?: string; + tableWidth?: string; +}; + +export class TableOutputComponent extends AbstractOutputComponent { + private debugMode = false; + private columnIds: Array = []; + private fetchColumns = true; + private columnArray = new Array(); + private showIndexColumn = false; + private components: any; + + static defaultProps: Partial = { + cacheBlockSize: 200, + maxBlocksInCache: 5, + columnWidth: 200, + blockLoadDebounce: 250, + tableHeight: '300px', + tableWidth: '100%' + }; + + constructor(props: TableOutputProps) { + super(props); + + this.components = { + loadingRenderer: (params: any) => { + if (params.value !== undefined) { + return params.value; + } else { + return ''; + } + } + }; + } + + renderMainArea(): React.ReactNode { + return
+ + +
; + } + + private async fetchTableLines(fetchIndex: number, linesToFetch: number) { + const traceUUID = this.props.traceId; + const tspClient = this.props.tspClient; + const outPutId = this.props.outputDescriptor.id; + + const lineResponse = (await tspClient.fetchTableLines(traceUUID, outPutId, QueryHelper.tableQuery(this.columnIds, fetchIndex, linesToFetch))).getModel(); + const model = lineResponse.model; + const lines = model.lines; + const linesArray = new Array(); + lines.forEach(line => { + const obj: any = {}; + const cells = line.cells; + const ids = model.columnIds; + + if (this.showIndexColumn) { + obj[0] = line.index.toString(); + } + + for (let i = 0; i < cells.length; i++) { + const id = this.showIndexColumn ? ids[i] + 1 : ids[i]; + obj[id] = cells[i].content; + } + linesArray.push(obj); + }); + + return linesArray; + } + + private onGridReady = async (event: GridReadyEvent) => { + const dataSource: IDatasource = { + getRows: async params => { + if (this.fetchColumns) { + this.fetchColumns = false; + await this.init(); + } + const rowsThisPage = await this.fetchTableLines(params.startRow, params.endRow - params.startRow); + for (let i = 0; i < rowsThisPage.length; i++) { + const item = rowsThisPage[i]; + const itemCopy = cloneDeep(item); + rowsThisPage[i] = itemCopy; + } + + if (this.props.cacheBlockSize && (rowsThisPage.length < this.props.cacheBlockSize)) { + params.successCallback(rowsThisPage, params.startRow + rowsThisPage.length); + } else { + params.successCallback(rowsThisPage); + } + } + }; + event.api.setDatasource(dataSource); + }; + + private async init() { + const traceUUID = this.props.traceId; + const tspClient = this.props.tspClient; + const outPutId = this.props.outputDescriptor.id; + + // Fetch columns + const columnsResponse = (await tspClient.fetchTableColumns(traceUUID, outPutId, QueryHelper.timeQuery([0, 1]))).getModel(); + const columnEntries = columnsResponse.model; + const colIds: Array = []; + const columnsArray = new Array(); + + if (this.showIndexColumn) { + columnsArray.push({ + headerName: 'Index', + field: '0', + width: this.props.columnWidth, + cellRenderer: 'loadingRenderer' + }); + colIds.push(0); + } + + columnEntries.forEach(columnHeader => { + const id = this.showIndexColumn ? ++columnHeader.id : columnHeader.id; + colIds.push(id); + columnsArray.push({ + headerName: columnHeader.name, + field: columnHeader.id.toString(), + width: this.props.columnWidth + + }); + }); + + if (!this.showIndexColumn) { + columnsArray[0].cellRenderer = 'loadingRenderer'; + } + + this.columnIds = colIds; + this.columnArray = columnsArray; + + this.setState({ + tableColumns: this.columnArray + }); + } +} diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx new file mode 100644 index 00000000..63571df4 --- /dev/null +++ b/react-components/src/components/timegraph-output-component.tsx @@ -0,0 +1,361 @@ +import * as React from 'react'; +import { TimeGraphRowElement, TimeGraphRowElementStyle } from 'timeline-chart/lib/components/time-graph-row-element'; +import { TimeGraphChart, TimeGraphChartProviders } from 'timeline-chart/lib/layer/time-graph-chart'; +import { TimeGraphChartCursors } from 'timeline-chart/lib/layer/time-graph-chart-cursors'; +import { TimeGraphChartGrid } from 'timeline-chart/lib/layer/time-graph-chart-grid'; +import { TimeGraphChartSelectionRange } from 'timeline-chart/lib/layer/time-graph-chart-selection-range'; +import { TimeGraphVerticalScrollbar } from 'timeline-chart/lib/layer/time-graph-vertical-scrollbar'; +import { TimelineChart } from 'timeline-chart/lib/time-graph-model'; +import { TimeGraphRowController } from 'timeline-chart/lib/time-graph-row-controller'; +import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; +import { ResponseStatus } from 'tsp-typescript-client/lib/models/response/responses'; +import { TimeGraphEntry } from 'tsp-typescript-client/lib/models/timegraph'; +import { signalManager } from '@trace-viewer/base/lib/signal-manager'; +import { AbstractOutputProps, AbstractOutputState } from './abstract-output-component'; +import { AbstractTreeOutputComponent } from './abstract-tree-output-component'; +import { StyleProvider } from './data-providers/style-provider'; +import { TspDataProvider } from './data-providers/tsp-data-provider'; +import { ReactTimeGraphContainer } from './utils/timegraph-container-component'; +import { OutputElementStyle } from 'tsp-typescript-client/lib/models/styles'; +import { EntryTree } from './utils/filtrer-tree/entry-tree'; +import { listToTree, getAllExpandedNodeIds } from './utils/filtrer-tree/utils'; +import hash from '@trace-viewer/base/lib/utils/value-hash'; +import ColumnHeader from './utils/filtrer-tree/column-header'; + +type TimegraphOutputProps = AbstractOutputProps & { + addWidgetResizeHandler: (handler: () => void) => void; +}; + +type TimegraphOutputState = AbstractOutputState & { + timegraphTree: TimeGraphEntry[]; + collapsedNodes: number[]; + columns: ColumnHeader[]; +}; + +export class TimegraphOutputComponent extends AbstractTreeOutputComponent { + private totalHeight = 0; + private rowController: TimeGraphRowController; + private chartLayer: TimeGraphChart; + private vscrollLayer: TimeGraphVerticalScrollbar; + private horizontalContainer: React.RefObject; + + private tspDataProvider: TspDataProvider; + private styleMap = new Map(); + + private selectedElement: TimeGraphRowElement | undefined; + + constructor(props: TimegraphOutputProps) { + super(props); + this.state = { + outputStatus: ResponseStatus.RUNNING, + timegraphTree: [], + collapsedNodes: [], + columns: [] + }; + this.onToggleCollapse = this.onToggleCollapse.bind(this); + this.tspDataProvider = new TspDataProvider(this.props.tspClient, this.props.traceId, this.props.outputDescriptor.id); + this.rowController = new TimeGraphRowController(this.props.style.rowHeight, this.totalHeight); + this.horizontalContainer = React.createRef(); + const providers: TimeGraphChartProviders = { + dataProvider: async (range: TimelineChart.TimeGraphRange, resolution: number) => this.fetchTimegraphData(range, resolution), + rowElementStyleProvider: (model: TimelineChart.TimeGraphRowElementModel) => this.getElementStyle(model), + rowStyleProvider: (row: TimelineChart.TimeGraphRowModel) => ({ + backgroundColor: 0x979797,// 0xaaaaff, + backgroundOpacity: row.selected ? 0.1 : 0, + lineColor: 0xdddddd, // hasStates ? 0xdddddd : 0xaa4444, // row.data && row.data.hasStates + lineThickness: 1, // hasStates ? 1 : 3 // row.data && row.data.hasStates + }) + }; + this.chartLayer = new TimeGraphChart('timeGraphChart', providers, this.rowController); + this.vscrollLayer = new TimeGraphVerticalScrollbar('timeGraphVerticalScrollbar', this.rowController); + + this.rowController.onVerticalOffsetChangedHandler(() => { + if (this.treeRef.current) { + this.treeRef.current.scrollTop = this.rowController.verticalOffset; + } + }); + + this.chartLayer.onSelectedRowElementChanged(model => { + if (model) { + const el = this.chartLayer.getElementById(model.id); + if (el) { + this.selectedElement = el; + } + } else { + this.selectedElement = undefined; + } + this.onElementSelected(this.selectedElement); + }); + } + + synchronizeTreeScroll(): void { + if (this.treeRef.current) { + this.rowController.verticalOffset = this.treeRef.current.scrollTop; + } + } + + async componentDidMount(): Promise { + this.waitAnalysisCompletion(); + } + + async componentDidUpdate(_prevProps: TimegraphOutputProps, _prevState: TimegraphOutputState): Promise { + if (this.state.outputStatus !== ResponseStatus.COMPLETED || !this.state.timegraphTree.length) { + const treeParameters = QueryHelper.timeQuery([0, 1]); + const treeResponse = (await this.props.tspClient.fetchTimeGraphTree(this.props.traceId, + this.props.outputDescriptor.id, treeParameters)).getModel(); + const nbEntries = treeResponse.model.entries.length; + this.totalHeight = nbEntries * this.props.style.rowHeight; + this.rowController.totalHeight = this.totalHeight; + const columns: ColumnHeader[] = []; + if (treeResponse.model.headers && treeResponse.model.headers.length > 0) { + treeResponse.model.headers.forEach(header => { + columns.push({title: header.name, sortable: true, tooltip: header.tooltip}); + }); + } else { + columns.push({title: 'Name', sortable: true}); + } + // TODO Style should not be retreive in the "initialization" part or at least async + const styleResponse = (await this.props.tspClient.fetchStyles(this.props.traceId, this.props.outputDescriptor.id, QueryHelper.query())).getModel(); + this.setState({ + // outputStatus: ResponseStatus.COMPLETED, + timegraphTree: treeResponse.model.entries, + styleModel: styleResponse.model, + columns + }); + this.chartLayer.updateChart(); + } + + if (this.state.collapsedNodes !== _prevState.collapsedNodes) { + this.chartLayer.updateChart(); + } + } + + private onToggleCollapse(id: number) { + let newList = [...this.state.collapsedNodes]; + const exist = this.state.collapsedNodes.find(expandId => expandId === id); + if (exist !== undefined) { + newList = newList.filter(collapsed => id !== collapsed); + } else { + newList = newList.concat(id); + } + this.setState({ collapsedNodes: newList }); + } + + renderTree(): React.ReactNode { + // TODO Show header, when we can have entries in-line with timeline-chart + return ; + } + + renderChart(): React.ReactNode { + return + {this.state.outputStatus === ResponseStatus.COMPLETED ? +
{ ev.preventDefault(); ev.stopPropagation(); }} style={{ height: this.props.style.height }} > + {this.renderTimeGraphContent()} +
: + 'Analysis running...'} +
; + } + + private renderTimeGraphContent() { + return
+ {this.getChartContainer()} +
; + } + + private getChartContainer() { + const grid = new TimeGraphChartGrid('timeGraphGrid', this.props.style.rowHeight, this.props.style.lineColor); + + const cursors = new TimeGraphChartCursors('chart-cursors', this.chartLayer, this.rowController, { color: this.props.style.cursorColor }); + const selectionRange = new TimeGraphChartSelectionRange('chart-selection-range', { color: this.props.style.cursorColor }); + return + ; + } + + protected getVerticalScrollbar(): JSX.Element { + return ; + } + + private async onElementSelected(element: TimeGraphRowElement | undefined) { + if (element && this.props.viewRange) { + const elementRange = element.model.range; + const offset = this.props.viewRange.getOffset(); + const time = Math.round((elementRange.start + ((elementRange.end - elementRange.start) / 2)) + (offset ? offset : 0)); + const tooltipResponse = await this.props.tspClient.fetchTimeGraphToolTip(this.props.traceId, this.props.outputDescriptor.id, time, element.row.model.id.toString()); + const responseModel = tooltipResponse.getModel(); + if (responseModel) { + const tooltipObject = { + 'Label': element.model.label, + 'Start time': (elementRange.start + (offset ? offset : 0)).toString(), + 'End time': (elementRange.end + (offset ? offset : 0)).toString(), + ...responseModel.model, + 'Row': element.row.model.name + }; + signalManager().fireTooltipSignal(tooltipObject); + } + } + } + + private async fetchTimegraphData(range: TimelineChart.TimeGraphRange, resolution: number) { + const treeNodes = listToTree(this.state.timegraphTree, this.state.columns); + const orderedTreeIds = getAllExpandedNodeIds(treeNodes, this.state.collapsedNodes); + const length = range.end - range.start; + const overlap = ((length * 5) - length) / 2; + const start = range.start - overlap > 0 ? range.start - overlap : 0; + const end = range.end + overlap < this.props.unitController.absoluteRange ? range.end + overlap : this.props.unitController.absoluteRange; + const newRange: TimelineChart.TimeGraphRange = { start, end }; + const newResolution: number = resolution * 0.8; + const timeGraphData: TimelineChart.TimeGraphModel = await this.tspDataProvider.getData(orderedTreeIds, this.state.timegraphTree, newRange, this.props.style.chartWidth); + return { + rows: timeGraphData ? timeGraphData.rows : [], + range: newRange, + resolution: newResolution + }; + } + + private getElementStyle(element: TimelineChart.TimeGraphRowElementModel) { + const styleModel = this.state.styleModel; + if (styleModel) { + const metadata = element.data; + if (metadata && metadata.style) { + const elementStyle: OutputElementStyle = metadata.style; + const modelStyle = styleModel.styles[elementStyle.parentKey]; + if (modelStyle) { + const currentStyle = Object.assign({}, modelStyle.values, elementStyle.values); + if (currentStyle) { + const color = this.hexStringToNumber(currentStyle['background-color']); + let height = this.props.style.rowHeight * 0.8; + if (currentStyle['height']) { + height = currentStyle['height'] * this.props.style.rowHeight; + } + return { + color: color, + height: height, + borderWidth: element.selected ? 2 : 0, + borderColor: 0xeef20c + }; + } + } + } + } + return this.getDefaultElementStyle(element); + } + + private hexStringToNumber(hexString: string): number { + return parseInt(hexString.replace(/^#/, ''), 16); + } + + private getDefaultElementStyle(element: TimelineChart.TimeGraphRowElementModel) { + const styleProvider = new StyleProvider(this.props.outputDescriptor.id, this.props.traceId, this.props.tspClient); + const styles = styleProvider.getStylesTmp(); + const backupStyles: TimeGraphRowElementStyle[] = [ + { + color: 0x3891A6, + height: this.props.style.rowHeight * 0.8 + }, { + color: 0x4C5B5C, + height: this.props.style.rowHeight * 0.7 + }, { + color: 0xFDE74C, + height: this.props.style.rowHeight * 0.6 + }, { + color: 0xDB5461, + height: this.props.style.rowHeight * 0.5 + }, { + color: 0xE3655B, + height: this.props.style.rowHeight * 0.4 + }, { + color: 0xEA8F87, + height: this.props.style.rowHeight * 0.9 + }, { + color: 0xDE636F, + height: this.props.style.rowHeight * 0.3 + }, + ]; + + let style: TimeGraphRowElementStyle | undefined = backupStyles[0]; + const val = element.label; + const modelData = element.data; + if (modelData) { + const outputStyle = modelData.style; + if (!outputStyle) { + return { + color: 0xCACACA, + height: this.props.style.rowHeight * 0.5, + borderWidth: element.selected ? 2 : 0, + borderColor: 0xeef20c + }; + } + + const stateStyle = outputStyle as OutputElementStyle; + const elementStyle = styles[stateStyle.parentKey]; + if (elementStyle) { + return { + color: parseInt(elementStyle.color, 16), + height: this.props.style.rowHeight * elementStyle.height, + borderWidth: element.selected ? 2 : 0, + borderColor: 0xeef20c + }; + } + + style = this.styleMap.get(stateStyle.parentKey); + if (style === undefined) { + style = backupStyles[(Math.abs(hash(stateStyle.parentKey)) as number % backupStyles.length)]; + this.styleMap.set(stateStyle.parentKey, style); + } + return { + color: style.color, + height: style.height, + borderWidth: element.selected ? 2 : 0, + borderColor: 0xeef20c + }; + } + + style = this.styleMap.get(val); + if (!style) { + style = backupStyles[(this.styleMap.size % backupStyles.length)]; + this.styleMap.set(val, style); + } + return { + color: style.color, + height: style.height, + borderWidth: element.selected ? 2 : 0, + borderColor: 0xeef20c + }; + } + +} diff --git a/react-components/src/components/trace-context-component.tsx b/react-components/src/components/trace-context-component.tsx new file mode 100644 index 00000000..23dac9b4 --- /dev/null +++ b/react-components/src/components/trace-context-component.tsx @@ -0,0 +1,262 @@ +import * as React from 'react'; +import '../style/trace-viewer.css'; +import '../style/trace-context-style.css'; +import '../style/output-components-style.css'; +import '../style/trace-explorer.css'; +import '../style/status-bar.css'; +import { Layout, Responsive, WidthProvider } from 'react-grid-layout'; +import { TimelineChart } from 'timeline-chart/lib/time-graph-model'; +import { TimeGraphUnitController } from 'timeline-chart/lib/time-graph-unit-controller'; +import { OutputDescriptor } from 'tsp-typescript-client/lib/models/output-descriptor'; +import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; +import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; +import { TimeRange } from '@trace-viewer/base/lib/utils/time-range'; +import { TableOutputComponent } from './table-output-component'; +import { TimegraphOutputComponent } from './timegraph-output-component'; +import { OutputComponentStyle } from './utils/output-component-style'; +import { TimeAxisComponent } from './utils/time-axis-component'; +import { TimeNavigatorComponent } from './utils/time-navigator-component'; +import { XYOutputComponent } from './xy-output-component'; +import { NullOutputComponent } from './null-output-component'; +import { AbstractOutputProps } from './abstract-output-component'; +import * as Messages from '@trace-viewer/base/lib/message-manager'; + +const ResponsiveGridLayout = WidthProvider(Responsive); + +interface TraceContextProps { + tspClient: TspClient; + experiment: Experiment; + outputs: OutputDescriptor[]; + onOutputRemove: (outputId: string) => void; + // Introduce dependency on Theia maybe it should be just a callback + messageManager: Messages.MessageManager; + addResizeHandler: (handler: () => void) => void; +} + +interface TraceContextState { + timeOffset: number; + currentRange: TimeRange; + currentViewRange: TimeRange; + currentTimeSelection: TimeRange | undefined; + experiment: Experiment + traceIndexing: boolean; + style: OutputComponentStyle; +} + +export class TraceContextComponent extends React.Component { + private readonly INDEXING_RUNNING_STATUS: string = 'RUNNING'; + private readonly INDEXING_STATUS_BAR_KEY = 'indexing-status'; + private readonly TIME_SELECTION_STATUS_BAR_KEY = 'time-selection-range'; + private readonly COMPONENT_WIDTH_PROPORTION: number = 0.85; + private readonly DEFAULT_COMPONENT_WIDTH: number = 1500; + private readonly DEFAULT_CHART_WIDTH: number = Math.floor(this.DEFAULT_COMPONENT_WIDTH * this.COMPONENT_WIDTH_PROPORTION); + private readonly DEFAULT_COMPONENT_HEIGHT: number = 10; + private readonly DEFAULT_COMPONENT_ROWHEIGHT: number = 20; + private readonly SCROLLBAR_PADDING: number = 12; + + private unitController: TimeGraphUnitController; + + private traceContextContainer: React.RefObject; + + protected widgetResizeHandlers: (() => void)[] = []; + protected readonly addWidgetResizeHandler = (h: () => void): void => { + this.widgetResizeHandlers.push(h); + }; + + constructor(props: TraceContextProps) { + super(props); + let traceRange = new TimeRange(0, 0); + let viewRange = new TimeRange(0, 0); + if (this.props.experiment) { + const experiment = this.props.experiment; + traceRange = new TimeRange(experiment.start - this.props.experiment.start, experiment.end - this.props.experiment.start, this.props.experiment.start); + viewRange = new TimeRange(experiment.start - this.props.experiment.start, experiment.end - this.props.experiment.start, this.props.experiment.start); + } + this.state = { + timeOffset: this.props.experiment.start, + currentRange: traceRange, + currentViewRange: viewRange, + currentTimeSelection: undefined, + experiment: this.props.experiment, + traceIndexing: this.props.experiment.indexingStatus === this.INDEXING_RUNNING_STATUS, + style: { + width: this.DEFAULT_COMPONENT_WIDTH, // 1245, + chartWidth: this.DEFAULT_CHART_WIDTH, + height: this.DEFAULT_COMPONENT_HEIGHT, + rowHeight: this.DEFAULT_COMPONENT_ROWHEIGHT, + naviBackgroundColor: 0x3f3f3f, + chartBackgroundColor: 0x3f3f3f, + cursorColor: 0x259fd8, + lineColor: 0xbbbbbb + } + }; + const absoluteRange = traceRange.getDuration(); + this.unitController = new TimeGraphUnitController(absoluteRange, { start: 0, end: absoluteRange }); + this.unitController.numberTranslator = (theNumber: number) => { + const originalStart = traceRange.getstart(); + theNumber += originalStart; + const zeroPad = (num: number) => String(num).padStart(3, '0'); + const seconds = Math.floor(theNumber / 1000000000); + const millis = zeroPad(Math.floor(theNumber / 1000000) % 1000); + const micros = zeroPad(Math.floor(theNumber / 1000) % 1000); + const nanos = zeroPad(Math.floor(theNumber) % 1000); + return seconds + '.' + millis + ' ' + micros + ' ' + nanos; + }; + this.unitController.onSelectionRangeChange(range => { this.handleTimeSelectionChange(range); }); + this.unitController.onViewRangeChanged(viewRangeParam => { this.handleViewRangeChange(viewRangeParam); }); + this.traceContextContainer = React.createRef(); + this.initialize(); + } + + private async initialize() { + await this.updateTrace(); + this.unitController.absoluteRange = this.state.experiment.end - this.state.timeOffset; + this.unitController.viewRange = { start: 0, end: this.state.experiment.end - this.state.timeOffset }; + } + + private async updateTrace() { + if (this.state.traceIndexing) { + let updatedExperiment = (await this.props.tspClient.fetchExperiment(this.props.experiment.UUID)).getModel(); + let isIndexing = updatedExperiment.indexingStatus === this.INDEXING_RUNNING_STATUS; + while (isIndexing) { + updatedExperiment = (await this.props.tspClient.fetchExperiment(this.props.experiment.UUID)).getModel(); + isIndexing = updatedExperiment.indexingStatus === this.INDEXING_RUNNING_STATUS; + this.setState({ + timeOffset: updatedExperiment.start, + experiment: updatedExperiment, + traceIndexing: isIndexing, + currentRange: new TimeRange(updatedExperiment.start - updatedExperiment.start, updatedExperiment.end - updatedExperiment.start, updatedExperiment.start) + }); + + // Update status bar + this.props.messageManager.addStatusMessage(this.INDEXING_STATUS_BAR_KEY, { + text: `Indexing ${this.props.experiment.name}: ${this.state.experiment.nbEvents}`, + category: Messages.MessageCategory.SERVER_MESSAGE + }); + await this.sleep(500); + } + } + this.props.messageManager.removeStatusMessage(this.INDEXING_STATUS_BAR_KEY); + } + + private async sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + componentDidMount(): void { + this.onResize = this.onResize.bind(this); + this.props.addResizeHandler(this.onResize); + this.onResize(); + } + + componentWillUnmount(): void { + this.props.messageManager.removeStatusMessage(this.INDEXING_STATUS_BAR_KEY); + this.props.messageManager.removeStatusMessage(this.TIME_SELECTION_STATUS_BAR_KEY); + } + + 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) } })); + this.widgetResizeHandlers.forEach(h => h()); + } + + private getChartWidth(totalWidth: number): number { + return Math.floor(totalWidth * this.COMPONENT_WIDTH_PROPORTION); + } + + private handleTimeSelectionChange(range: TimelineChart.TimeGraphRange) { + const t1 = Math.trunc(range.start + this.state.timeOffset); + const t2 = Math.trunc(range.end + this.state.timeOffset); + + this.props.messageManager.addStatusMessage(this.TIME_SELECTION_STATUS_BAR_KEY, { + text: `T1: ${t1} T2: ${t2} Delta: ${t2 - t1}`, + category: Messages.MessageCategory.TRACE_CONTEXT + }); + this.setState(prevState => ({ + currentTimeSelection: new TimeRange(range.start, range.end, prevState.timeOffset) + })); + } + + private handleViewRangeChange(viewRange: TimelineChart.TimeGraphRange) { + this.setState(prevState => ({ + currentViewRange: new TimeRange(viewRange.start, viewRange.end, prevState.timeOffset) + })); + } + + render(): JSX.Element { + return
+ {this.props.outputs.length ? this.renderOutputs() : this.renderPlaceHolder()} +
; + } + + private renderOutputs() { + const layouts = this.generateGridLayout(); + const outputs = this.props.outputs; + return +
+ +
+ { + // Syntax to use ReactGridLayout with Custom Components, while passing resized dimensions to children: + // https://github.com/STRML/react-grid-layout/issues/299#issuecomment-524959229 + } + + {outputs.map(output => { + const responseType = output.type; + const outputProps: AbstractOutputProps = { + tspClient: this.props.tspClient, + traceId: this.state.experiment.UUID, + outputDescriptor: output, + range: this.state.currentRange, + viewRange: this.state.currentViewRange, + selectionRange: this.state.currentTimeSelection, + style: this.state.style, + onOutputRemove: this.props.onOutputRemove, + unitController: this.unitController, + widthWPBugWorkaround: this.state.style.width + }; + switch (responseType) { + case 'TIME_GRAPH': + return ; + case 'TREE_TIME_XY': + return ; + case 'TABLE': + return ; + default: + return ; + } + })} + +
+ +
+
; + } + + private renderPlaceHolder() { + return
+ {'Add outputs by clicking on an analysis in the trace explorer view'} +
; + } + + private generateGridLayout(): Layout[] { + const outputs = this.props.outputs; + const layouts: Layout[] = []; + if (outputs.length) { + outputs.forEach((output, index) => { + const itemLayout = { + i: output.id, + x: 0, + y: index, + w: 1, + h: this.DEFAULT_COMPONENT_HEIGHT + }; + layouts.push(itemLayout); + }); + } + return layouts; + } +} diff --git a/react-components/src/components/utils/filtrer-tree/__tests__/__snapshots__/entry-tree.test.tsx.snap b/react-components/src/components/utils/filtrer-tree/__tests__/__snapshots__/entry-tree.test.tsx.snap new file mode 100644 index 00000000..14316ed1 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/__tests__/__snapshots__/entry-tree.test.tsx.snap @@ -0,0 +1,1486 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Empty tree 1`] = ` + + + +`; + +exports[`Entry with children All unchecked 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Sortable column + + + + + Unsortable column +
+ + + + + + + parent + + parent second column +
+ + + + + child1 + + child1 second column +
+ + + + + + + child2 + + child2 second column +
+ + + + + grandchild1 + + grandchild1 second column +
+ + + + + grandchild2 + + grandchild2 second column +
+
+`; + +exports[`Entry with children Check one grandchild 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Sortable column + + + + + Unsortable column +
+ + + + + + + parent + + parent second column +
+ + + + + child1 + + child1 second column +
+ + + + + + + child2 + + child2 second column +
+ + + + + grandchild1 + + grandchild1 second column +
+ + + + + grandchild2 + + grandchild2 second column +
+
+`; + +exports[`Entry with children Collapse one child 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + +
+ Sortable column + + + + + Unsortable column +
+ + + + + + + parent + + parent second column +
+ + + + + child1 + + child1 second column +
+ + + + + + + child2 + + child2 second column +
+
+`; + +exports[`Entry with children With filter element 1`] = ` +Array [ +
+ +
, +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Sortable column + + + + + Unsortable column +
+ + + + + + + parent + + parent second column +
+ + + + + child1 + + child1 second column +
+ + + + + + + child2 + + child2 second column +
+ + + + + grandchild1 + + grandchild1 second column +
+ + + + + grandchild2 + + grandchild2 second column +
+
, +] +`; + +exports[`one level of entries 1`] = ` +
+ + + + + + + + + + + + + + + + + +
+ Sortable column + + + + + Unsortable column +
+ + + entry1 + + +
+ + + entry2 + + entry2 column2 +
+
+`; + +exports[`one level of entries 2`] = ` +
+ + + + + + + + + + + + + + + + + +
+ Sortable column + + + + + Unsortable column +
+ + + + + entry1 + + +
+ + + + + entry2 + + entry2 column2 +
+
+`; + +exports[`one level of entries 3`] = ` +
+ + + + + + + + + + + +
+ + + + + entry1 + + +
+ + + + + entry2 + + entry2 column2 +
+
+`; diff --git a/react-components/src/components/utils/filtrer-tree/__tests__/__snapshots__/table-row.test.tsx.snap b/react-components/src/components/utils/filtrer-tree/__tests__/__snapshots__/table-row.test.tsx.snap new file mode 100644 index 00000000..06bccb23 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/__tests__/__snapshots__/table-row.test.tsx.snap @@ -0,0 +1,586 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Checked status 1`] = ` + + + + + + + cell1 - text + + +`; + +exports[`Checked status 2`] = ` + + + + + + + cell1 - text + + +`; + +exports[`Checked status 3`] = ` + + + + + + + cell1 - text + + +`; + +exports[`Levels 1`] = ` + + + + + + + cell1 - text + + +`; + +exports[`Levels 2`] = ` + + + + + + + cell1 - text + + +`; + +exports[`Multiple labels With children and labels 1`] = ` +Array [ + + + + + + + + + parent 1 text + + + parent 2 text + + , + + + + + + + child1 + + + child 1 text + + , + + + + + + + child2 + + + child 2 text + + , +] +`; + +exports[`Uncheckable 1`] = ` + + + + + cell1 - text + + +`; + +exports[`with children With children 1`] = ` +Array [ + + + + + + + + + parent text + + , + + + + + + + child 1 text + + , + + + + + + + child 2 text + + , +] +`; + +exports[`with children With children collapsed 1`] = ` + + + + + + + + + parent text + + +`; diff --git a/react-components/src/components/utils/filtrer-tree/__tests__/entry-tree.test.tsx b/react-components/src/components/utils/filtrer-tree/__tests__/entry-tree.test.tsx new file mode 100644 index 00000000..00e15ef4 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/__tests__/entry-tree.test.tsx @@ -0,0 +1,297 @@ +import * as React from 'react'; +import { EntryTree } from '../entry-tree'; +import { create } from 'react-test-renderer'; +import { render, fireEvent } from "@testing-library/react"; +import { within } from "@testing-library/dom"; + +const mockOnChecked = jest.fn(); +const mockOnCollapse = jest.fn(); + +test('Empty tree', () => { + const tree = create() + .toJSON(); + expect(tree).toMatchSnapshot(); +}); + +test('one level of entries', () => { + const entry1 = { + id: 0, + parentId: -1, + labels: ['entry1'] + + }; + const entry2 = { + id: 1, + parentId: -1, + labels: ['entry2', 'entry2 column2'] + + }; + const treeWithoutCheckboxes = create() + .toJSON(); + expect(treeWithoutCheckboxes).toMatchSnapshot(); + + const treeWithCheckboxes = create() + expect(treeWithCheckboxes).toMatchSnapshot(); + + const treeWithoutHeaders = create() + expect(treeWithoutHeaders).toMatchSnapshot(); +}); + +describe('Entry with children', () => { + const parent = { + id: 0, + parentId: -1, + labels: ['parent', 'parent second column'] + }; + const child1 = { + id: 1, + parentId: parent.id, + labels: ['child1', 'child1 second column'] + }; + const child2 = { + id: 2, + parentId: parent.id, + labels: ['child2', 'child2 second column'] + }; + const grandchild1 = { + id: 3, + parentId: child2.id, + labels: ['grandchild1', 'grandchild1 second column'] + }; + const grandchild2 = { + id: 4, + parentId: child2.id, + labels: ['grandchild2', 'grandchild2 second column'] + }; + + test('All unchecked', () => { + const tree = create() + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + + test('Check one grandchild', () => { + const tree = create() + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + + test('Collapse one child', () => { + const tree = create() + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + + test('Check series', () => { + mockOnChecked.mockClear(); + let { getByText } = render() + // Check a parent and make sure all its children were selected too + const element = getByText(child2.labels[0]); + const image = within(element).getAllByRole("img", {hidden: true})[1]; + fireEvent.click(image as HTMLElement); + expect(mockOnChecked).toHaveBeenCalledTimes(1); + expect(mockOnChecked).toHaveBeenCalledWith([child2.id, grandchild1.id, grandchild2.id]); + mockOnChecked.mockClear(); + + // Check a child and make sure all its children were selected too + const childElement = getByText(grandchild1.labels[0]); + const image2 = within(childElement).getByRole("img", {hidden: true}); + fireEvent.click(image2 as HTMLElement); + expect(mockOnChecked).toHaveBeenCalledTimes(1); + expect(mockOnChecked).toHaveBeenCalledWith([grandchild1.id]); + mockOnChecked.mockClear(); + + // Check again the parent element, should check all items + fireEvent.click(image as HTMLElement); + expect(mockOnChecked).toHaveBeenCalledTimes(1); + expect(mockOnChecked).toHaveBeenCalledWith([child2.id, grandchild1.id, grandchild2.id]); + + }); + + test('Check series with previous selected', () => { + mockOnChecked.mockClear(); + let { getByText } = render() + // Check a parent and make sure all its children were selected too + const element = getByText(child2.labels[0]); + const image = within(element).getAllByRole("img", {hidden: true})[1]; + fireEvent.click(image as HTMLElement); + expect(mockOnChecked).toHaveBeenCalledTimes(1) + expect(mockOnChecked).toHaveBeenCalledWith([child2.id, grandchild1.id]) + }); + + test('Collapse items', () => { + mockOnCollapse.mockClear() + const { getByText } = render() + // Click on the collapse icon of the element and make sure the function is called + const element = getByText(child2.labels[0]); + const collapseImage = within(element).getAllByRole("img", {hidden: true})[0]; + fireEvent.click(collapseImage as HTMLElement); + expect(mockOnCollapse).toHaveBeenCalledTimes(1) + expect(mockOnCollapse).toHaveBeenCalledWith(child2.id, expect.anything()) + }); + + test('With filter element', () => { + const tree = create() + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + + test('Filter items behavior', () => { + const { getByPlaceholderText, queryByText } = render() + // Enter a filter and make sure only visible elements are present + const filterEl = getByPlaceholderText("Filter"); + fireEvent.change(filterEl, {target: { value: grandchild2.labels[0]}}); + + let treeEl = queryByText(parent.labels[0]); + expect(treeEl).toBeTruthy(); + + treeEl = queryByText(child1.labels[0]); + expect(treeEl).toBeFalsy(); + + treeEl = queryByText(child2.labels[0]); + expect(treeEl).toBeTruthy(); + + treeEl = queryByText(grandchild1.labels[0]); + expect(treeEl).toBeFalsy(); + + treeEl = queryByText(grandchild2.labels[0]); + expect(treeEl).toBeTruthy(); + + // Remove the filter + const filterEl2 = getByPlaceholderText("Filter"); + fireEvent.change(filterEl2, {target: { value: ""}}); + + treeEl = queryByText(parent.labels[0]); + expect(treeEl).toBeTruthy(); + + treeEl = queryByText(child1.labels[0]); + expect(treeEl).toBeTruthy(); + + treeEl = queryByText(child2.labels[0]); + expect(treeEl).toBeTruthy(); + + treeEl = queryByText(grandchild1.labels[0]); + expect(treeEl).toBeTruthy(); + + treeEl = queryByText(grandchild2.labels[0]); + expect(treeEl).toBeTruthy(); + }); +}); diff --git a/react-components/src/components/utils/filtrer-tree/__tests__/table-row.test.tsx b/react-components/src/components/utils/filtrer-tree/__tests__/table-row.test.tsx new file mode 100644 index 00000000..c195dff8 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/__tests__/table-row.test.tsx @@ -0,0 +1,243 @@ +import * as React from 'react'; +import { TreeNode } from '../tree-node'; +import { TableRow } from '../table-row'; +import { create } from 'react-test-renderer'; +import { render, fireEvent } from "@testing-library/react"; +import { within } from "@testing-library/dom"; + +const mockOnChecked = jest.fn(); +const mockOnCollapse = jest.fn(); + +const cell1Text = "cell1 - text"; +const testTreeNode = { + id: 5, + parentId: -1, + labels: [cell1Text], + children: [], + isRoot: true +} as TreeNode; + +test('Checked status', () => { + const treeNodeUnchecked = create( 0} + level={0} + isCheckable={true} + collapsedNodes={[]} + onToggleCollapse={mockOnCollapse} + onToggleCheck={mockOnChecked} + />) + .toJSON(); + expect(treeNodeUnchecked).toMatchSnapshot(); + + const treeNodeChecked = create( 1} + level={0} + isCheckable={true} + collapsedNodes={[]} + onToggleCollapse={mockOnCollapse} + onToggleCheck={mockOnChecked} + />) + .toJSON(); + expect(treeNodeChecked).toMatchSnapshot(); + + const treeNodePartialCheck = create( 2} + level={0} + isCheckable={true} + collapsedNodes={[]} + onToggleCollapse={mockOnCollapse} + onToggleCheck={mockOnChecked} + />) + .toJSON(); + expect(treeNodePartialCheck).toMatchSnapshot(); +}); + +test('Uncheckable', () => { + const uncheckableTree = create( 0} + collapsedNodes={[]} + onToggleCollapse={mockOnCollapse} + onToggleCheck={mockOnChecked} + />) + .toJSON(); + expect(uncheckableTree).toMatchSnapshot(); + +}); + +test('Levels', () => { + const lessPadding = create( 0} + level={1} + isCheckable={true} + collapsedNodes={[]} + onToggleCollapse={mockOnCollapse} + onToggleCheck={mockOnChecked} + />) + .toJSON(); + expect(lessPadding).toMatchSnapshot(); + + const morePadding = create( 0} + level={10} + isCheckable={true} + collapsedNodes={[]} + onToggleCollapse={mockOnCollapse} + onToggleCheck={mockOnChecked} + />) + .toJSON(); + expect(morePadding).toMatchSnapshot(); + +}); + +test('Toggle check', async () => { + mockOnChecked.mockClear(); + let { getByText } = render( 0} + level={0} + isCheckable={true} + collapsedNodes={[]} + onToggleCollapse={mockOnCollapse} + onToggleCheck={mockOnChecked} + />) + const element = getByText(cell1Text); + let {getByRole} = within(element); + const image = getByRole("img", {hidden: true}); + fireEvent.click(image as HTMLElement); + expect(mockOnChecked).toHaveBeenCalledTimes(1); + expect(mockOnChecked).toHaveBeenCalledWith(testTreeNode.id); + + fireEvent.click(image as HTMLElement); + expect(mockOnChecked).toHaveBeenCalledTimes(2); + expect(mockOnChecked).toHaveBeenCalledWith(testTreeNode.id); +}); + +describe('with children', () => { + + const parentText = "parent text"; + const child1 = { + id: 2, + parentId: 1, + labels: ['child 1 text'], + children: [], + isRoot: false + } as TreeNode; + const child2 = { + id: 3, + parentId: 1, + labels: ['child 2 text'], + children: [], + isRoot: false + } as TreeNode; + const parentNode = { + id: 1, + parentId: -1, + labels: [parentText], + children: [child1, child2], + isRoot: true + } as TreeNode; + + test('With children', () => { + + let nodeWithChildren = create( 0} + level={0} + isCheckable={true} + collapsedNodes={[]} + onToggleCollapse={mockOnCollapse} + onToggleCheck={mockOnChecked} + />) + .toJSON(); + expect(nodeWithChildren).toMatchSnapshot(); + }) + + test('With children collapsed', () => { + + let nodeWithChildren = create( 0} + level={0} + isCheckable={true} + collapsedNodes={[parentNode.id]} + onToggleCollapse={mockOnCollapse} + onToggleCheck={mockOnChecked} + />) + .toJSON(); + expect(nodeWithChildren).toMatchSnapshot(); + }) + + test('Toggle collapse', () => { + mockOnCollapse.mockClear(); + + let { getByText } = render( 0} + level={0} + isCheckable={true} + collapsedNodes={[]} + onToggleCollapse={mockOnCollapse} + onToggleCheck={mockOnChecked} + />) + const element = getByText(parentText); + const collapsedEl = within(element).getAllByRole("img", {hidden: true})[0]; + fireEvent.click(collapsedEl as HTMLElement); + expect(mockOnCollapse).toHaveBeenCalledTimes(1) + expect(mockOnCollapse).toHaveBeenCalledWith(parentNode.id) + }); + +}); + +describe('Multiple labels', () => { + + const parentText = "parent 1 text"; + const parentText2 = "parent 2 text"; + const child1 = { + id: 2, + parentId: 1, + labels: ['child1', 'child 1 text'], + children: [], + isRoot: false + } as TreeNode; + const child2 = { + id: 3, + parentId: 1, + labels: ['child2', 'child 2 text'], + children: [], + isRoot: false + } as TreeNode; + const parentNode = { + id: 1, + parentId: -1, + labels: [parentText, parentText2], + children: [child1, child2], + isRoot: true + } as TreeNode; + + test('With children and labels', () => { + + let nodeWithChildren = create( 0} + level={0} + isCheckable={true} + collapsedNodes={[]} + onToggleCollapse={mockOnCollapse} + onToggleCheck={mockOnChecked} + />) + .toJSON(); + expect(nodeWithChildren).toMatchSnapshot(); + }) + +}); + + + diff --git a/react-components/src/components/utils/filtrer-tree/__tests__/utils.test.tsx b/react-components/src/components/utils/filtrer-tree/__tests__/utils.test.tsx new file mode 100644 index 00000000..65d80c5e --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/__tests__/utils.test.tsx @@ -0,0 +1,66 @@ +import { listToTree } from "../utils"; + +describe('listToTree', () => { + const parent = { + id: 0, + parentId: -1, + labels: ['parent', 'parent second column'] + }; + const child1 = { + id: 1, + parentId: parent.id, + labels: ['child1', 'child1 second column'] + }; + const child2 = { + id: 2, + parentId: parent.id, + labels: ['child2', 'child2 second column'] + }; + const grandchild1 = { + id: 3, + parentId: child2.id, + labels: ['grandchild1', 'grandchild1 second column'] + }; + const grandchild2 = { + id: 4, + parentId: child2.id, + labels: ['grandchild2', 'grandchild2 second column'] + }; + + test('Basic case, empty headers', () => { + const expectedTree = ({ + labels: parent.labels, + isRoot: true, + id: parent.id, + parentId: parent.parentId, + children: [{ + labels: child1.labels, + isRoot: false, + id: child1.id, + parentId: child1.parentId, + children: [] + }, { + labels: child2.labels, + isRoot: false, + id: child2.id, + parentId: child2.parentId, + children: [{ + labels: grandchild1.labels, + isRoot: false, + id: grandchild1.id, + parentId: grandchild1.parentId, + children: [] + }, { + labels: grandchild2.labels, + isRoot: false, + id: grandchild2.id, + parentId: grandchild2.parentId, + children: [] + }] + }] + }); + const tree = listToTree([child1, parent, child2, grandchild1, grandchild2], []); + expect(tree).toMatchObject([expectedTree]); + }); + +}); \ No newline at end of file diff --git a/react-components/src/components/utils/filtrer-tree/checkbox-component.tsx b/react-components/src/components/utils/filtrer-tree/checkbox-component.tsx new file mode 100644 index 00000000..23c03931 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/checkbox-component.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import icons from './icons'; + +interface CheckboxProps { + id: number; + checkedStatus: number; + onToggleCheck: (id: number) => void; +} + +export class CheckboxComponent extends React.Component { + constructor(props: CheckboxProps) { + super(props); + } + + private handleClick = (): void => { + this.props.onToggleCheck(this.props.id); + }; + + renderCheckbox = (checkedStatus: number): React.ReactNode => { + switch (checkedStatus) { + case 0: + return icons.unchecked; + case 1: + return icons.checked; + case 2: + return icons.halfChecked; + default: + return icons.unchecked; + } + }; + + render(): JSX.Element { + return + {this.renderCheckbox(this.props.checkedStatus)} + ; + } +} diff --git a/react-components/src/components/utils/filtrer-tree/column-header.tsx b/react-components/src/components/utils/filtrer-tree/column-header.tsx new file mode 100644 index 00000000..b7678cd5 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/column-header.tsx @@ -0,0 +1,5 @@ +export default interface ColumnHeader { + title: string, + tooltip?: string, + sortable?: boolean +} diff --git a/react-components/src/components/utils/filtrer-tree/entry-tree.tsx b/react-components/src/components/utils/filtrer-tree/entry-tree.tsx new file mode 100644 index 00000000..5d4cdcdb --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/entry-tree.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { Entry } from 'tsp-typescript-client/lib/models/entry'; +import { listToTree } from './utils'; +import { FilterTree } from './tree'; +import { TreeNode } from './tree-node'; +import ColumnHeader from './column-header'; + +interface EntryTreeProps { + entries: Entry[]; + checkedSeries: number[]; + showCheckboxes: boolean; + collapsedNodes: number[]; + showFilter: boolean; + onToggleCheck: (ids: number[]) => void; + onToggleCollapse: (id: number, nodes: TreeNode[]) => void; + onOrderChange: (ids: number[]) => void; + showHeader: boolean; + headers: ColumnHeader[]; + className: string; +} + +export class EntryTree extends React.Component { + static defaultProps: Partial = { + showFilter: true, + onOrderChange: () => { /* Nothing to do */ }, + showHeader: true, + className: 'table-tree', + headers: [{title: 'Name', sortable: true}] + }; + + constructor(props: EntryTreeProps) { + super(props); + } + + shouldComponentUpdate = (nextProps: EntryTreeProps): boolean => + (this.props.checkedSeries !== nextProps.checkedSeries || this.props.entries !== nextProps.entries || this.props.collapsedNodes !== nextProps.collapsedNodes); + + render(): JSX.Element { + return ; + } +} diff --git a/react-components/src/components/utils/filtrer-tree/filter.tsx b/react-components/src/components/utils/filtrer-tree/filter.tsx new file mode 100644 index 00000000..ec158f81 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/filter.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; + +interface FilterProps { + onChange: (e: React.ChangeEvent) => void; +} + +export class Filter extends React.Component { + constructor(props: FilterProps) { + super(props); + } + + render(): JSX.Element { + return
+ +
; + } +} diff --git a/react-components/src/components/utils/filtrer-tree/icons.tsx b/react-components/src/components/utils/filtrer-tree/icons.tsx new file mode 100644 index 00000000..5ee0d991 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/icons.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faChevronDown, faChevronRight, faCheckSquare, faSquare, faMinusSquare, faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'; + +interface iconsShape { + expand: React.ReactNode, + collapse: React.ReactNode, + unchecked: React.ReactNode, + checked: React.ReactNode, + halfChecked: React.ReactNode, + sort: React.ReactNode, + sortDown: React.ReactNode, + sortUp: React.ReactNode +} + +const icons: iconsShape = { + expand: , + collapse: , + unchecked: , + checked: , + halfChecked: , + sort: , + sortDown: , + sortUp: +}; + +export default icons; diff --git a/react-components/src/components/utils/filtrer-tree/message.tsx b/react-components/src/components/utils/filtrer-tree/message.tsx new file mode 100644 index 00000000..a7a5f9c9 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/message.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; + +interface MessageProps { + error: string; +} + +export class Message extends React.Component { + constructor(props: MessageProps) { + super(props); + } + + static defaultProps = { + error: '' + }; + + render(): JSX.Element { + return + {this.props.error} + ; + } +} diff --git a/react-components/src/components/utils/filtrer-tree/sort.tsx b/react-components/src/components/utils/filtrer-tree/sort.tsx new file mode 100644 index 00000000..c5422955 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/sort.tsx @@ -0,0 +1,71 @@ +import icons from './icons'; +import { TreeNode } from './tree-node'; + +interface SortState { + asc: React.ReactNode, + desc: React.ReactNode, + default: React.ReactNode; +} + +export const sortState: SortState = { + asc: icons.sortUp, + desc: icons.sortDown, + default: icons.sort +}; + +export interface SortConfig { + column: string; + columnIndex: number; + sortState: React.ReactNode; +} + +export const nextSortState = (currentState: React.ReactNode): React.ReactNode => { + if (currentState === sortState.default || currentState === sortState.asc) { + return sortState.desc; + } else if (currentState === sortState.desc) { + return sortState.asc; + } else { + return sortState.default; + } +}; + +export const sortNodes = (nodes: TreeNode[], sortConfig: SortConfig[]): TreeNode[] => { + const sortedNodes = [...nodes]; + const orderToSort = sortConfig.find((config: SortConfig) => config.sortState !== sortState.default); + if (orderToSort) { + sortedNodes.sort((node1: TreeNode, node2: TreeNode) => { + const index = orderToSort.columnIndex; + const order = (orderToSort.sortState === sortState.asc) ? 'asc' : 'desc'; + const value1 = node1.labels[index]; + const value2 = node2.labels[index]; + let result = 0; + if (!value1 && value2) { + result = -1; + } else if (value1 && !value2) { + result = 1; + } else if (!value1 && !value2) { + result = 0; + } else { + if (typeof value1 === 'string' && typeof value2 === 'string') { + const comp = (value1 as string).localeCompare(value2); + result = (order === 'asc') ? -comp : comp; + } else { + if (value1 < value2) { + result = (order === 'asc') ? -1 : 1; + } else if (value1 > value2) { + result = (order === 'asc') ? 1 : -1; + } else { + result = 0; + } + } + } + return result; + }); + sortedNodes.forEach((node: TreeNode) => { + if (node.children.length) { + node.children = sortNodes(node.children, sortConfig); + } + }); + } + return sortedNodes; +}; diff --git a/react-components/src/components/utils/filtrer-tree/table-body.tsx b/react-components/src/components/utils/filtrer-tree/table-body.tsx new file mode 100644 index 00000000..1339ee38 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/table-body.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { TreeNode } from './tree-node'; +import { TableRow } from './table-row'; + +interface TableBodyProps { + nodes: TreeNode[]; + collapsedNodes: number[]; + isCheckable: boolean; + getCheckedStatus: (id: number) => number; + onToggleCollapse: (id: number) => void; + onToggleCheck: (id: number) => void; +} + +export class TableBody extends React.Component { + constructor(props: TableBodyProps) { + super(props); + } + + createRow = (node: TreeNode): React.ReactNode => + ; + + renderRows = (): React.ReactNode => this.props.nodes.map((node: TreeNode) => this.createRow(node)); + + render(): React.ReactNode | undefined { + if (!this.props.nodes) {return undefined;} + + return ( + + {this.renderRows()} + + ); + } +} diff --git a/react-components/src/components/utils/filtrer-tree/table-cell.tsx b/react-components/src/components/utils/filtrer-tree/table-cell.tsx new file mode 100644 index 00000000..9d5a7db3 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/table-cell.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { TreeNode } from './tree-node'; + +interface TableCellProps { + node: TreeNode; + index: number; +} + +export class TableCell extends React.Component { + constructor(props: TableCellProps) { + super(props); + } + + render(): React.ReactNode { + const content = this.props.node.labels[this.props.index]; + return ( + + {this.props.children} + {content} + + ); + } +} diff --git a/react-components/src/components/utils/filtrer-tree/table-header.tsx b/react-components/src/components/utils/filtrer-tree/table-header.tsx new file mode 100644 index 00000000..7bafecc8 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/table-header.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { SortConfig } from './sort'; +import ColumnHeader from './column-header'; + +interface TableHeaderProps { + columns: ColumnHeader[]; + sortableColumns: string[]; + sortConfig: SortConfig[]; + onSort: (sortColumn: string) => void; +} + +export class TableHeader extends React.Component { + constructor(props: TableHeaderProps) { + super(props); + } + + handleSortChange = (sortColumn: string): void => { + this.props.onSort(sortColumn); + }; + + toCapitalCase = (name: string): string => (name.charAt(0).toUpperCase() + name.slice(1)); + + renderSortIcon = (column: string): React.ReactNode | undefined => { + if (this.props.sortableColumns.includes(column)) { + const state = this.props.sortConfig.find((config: SortConfig) => config.column === column); + return state + ? {state.sortState} + : undefined; + } + return undefined; + }; + + renderHeader = (): React.ReactNode => this.props.columns.map((column: ColumnHeader, index) => + this.handleSortChange(column.title)}> + {this.toCapitalCase(column.title)} + {this.renderSortIcon(column.title)} + + ); + + render(): React.ReactNode { + return + + {this.renderHeader()} + + ; + } +} diff --git a/react-components/src/components/utils/filtrer-tree/table-row.tsx b/react-components/src/components/utils/filtrer-tree/table-row.tsx new file mode 100644 index 00000000..402eaeed --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/table-row.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { TreeNode } from './tree-node'; +import { TableCell } from './table-cell'; +import { CheckboxComponent } from './checkbox-component'; +import icons from './icons'; + +interface TableRowProps { + node: TreeNode; + level: number; + collapsedNodes: number[]; + isCheckable: boolean; + getCheckedStatus: (id: number) => number; + onToggleCollapse: (id: number) => void; + onToggleCheck: (id: number) => void; +} + +export class TableRow extends React.Component { + constructor(props: TableRowProps) { + super(props); + } + + isCollapsed = (): boolean => this.props.collapsedNodes.includes(this.props.node.id); + + private handleCollapse = (): void => { + this.props.onToggleCollapse(this.props.node.id); + }; + + renderToggleCollapse = (): React.ReactNode => { + const marginLeft = this.props.level * 15 + 'px'; + return ( + (this.props.node.children.length === 0) + ? + : + {(this.isCollapsed() ? icons.expand : icons.collapse)} + + ); + }; + + renderCheckbox = (): React.ReactNode => { + const checkedStatus = this.props.getCheckedStatus(this.props.node.id); + return this.props.isCheckable + ? + : ; + }; + + renderRow = (): React.ReactNode => this.props.node.labels.map((_label: string, index) => { + let toggleCollapse: React.ReactNode; + let toggleCheck: React.ReactNode; + if (index === 0) { + toggleCollapse = this.renderToggleCollapse(); + toggleCheck = this.renderCheckbox(); + } + + return + {toggleCollapse} + {toggleCheck} + ; + }); + + renderChildren = (): React.ReactNode | undefined => { + if (this.props.node.children.length && !this.isCollapsed()) { + return this.props.node.children.map((child: TreeNode) => + + ); + } + return undefined; + }; + + render(): React.ReactNode | undefined { + if (!this.props.node) {return undefined;} + const children = this.renderChildren(); + + return ( + + {this.renderRow()} + {children} + + ); + } +} diff --git a/react-components/src/components/utils/filtrer-tree/table.tsx b/react-components/src/components/utils/filtrer-tree/table.tsx new file mode 100644 index 00000000..c7197e43 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/table.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { TreeNode } from './tree-node'; +import { TableHeader } from './table-header'; +import { TableBody } from './table-body'; +import { SortConfig, sortState, nextSortState, sortNodes } from './sort'; +import ColumnHeader from './column-header'; + +interface TableProps { + nodes: TreeNode[]; + collapsedNodes: number[]; + isCheckable: boolean; + sortConfig: SortConfig[]; + getCheckedStatus: (id: number) => number; + onToggleCollapse: (id: number) => void; + onToggleCheck: (id: number) => void; + onSort: (sortedNodes: TreeNode[]) => void; + onSortConfigChange: (sortConfig: SortConfig[]) => void; + showHeader: boolean; + headers: ColumnHeader[]; + className: string; +} + +export class Table extends React.Component { + + private sortableColumns: string[]; + + constructor(props: TableProps) { + super(props); + const sortableCols: string[] = []; + const config: SortConfig[] = []; + this.props.headers.forEach((header: ColumnHeader, columnIndex) => { + if (header.sortable) { + config.push({column: header.title, columnIndex: columnIndex, sortState: sortState.default}); + sortableCols.push(header.title); + } + }); + this.props.onSortConfigChange(config); + this.sortableColumns = sortableCols; + } + + onSortChange = (sortColumn: string): void => { + let newSortConfigs: SortConfig[] = [...this.props.sortConfig]; + newSortConfigs = newSortConfigs.map((config: SortConfig) => { + if (config.column === sortColumn) { + return {...config, sortState: nextSortState(config.sortState)}; + } else { + return {...config, sortState: sortState.default}; + } + }); + const newSortedNodes = sortNodes(this.props.nodes, newSortConfigs); + this.props.onSortConfigChange(newSortConfigs); + this.props.onSort(newSortedNodes); + }; + + render(): JSX.Element { + return ( +
+ + {this.props.showHeader && } + +
+
+ ); + } +} diff --git a/react-components/src/components/utils/filtrer-tree/tree-node.tsx b/react-components/src/components/utils/filtrer-tree/tree-node.tsx new file mode 100644 index 00000000..7cd98a19 --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/tree-node.tsx @@ -0,0 +1,7 @@ +export interface TreeNode { + id: number; + parentId: number; + labels: string[]; + children: Array; + isRoot: boolean; +} diff --git a/react-components/src/components/utils/filtrer-tree/tree.tsx b/react-components/src/components/utils/filtrer-tree/tree.tsx new file mode 100644 index 00000000..a085595b --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/tree.tsx @@ -0,0 +1,277 @@ +import * as React from 'react'; +import { TreeNode } from './tree-node'; +import { Message } from './message'; +import { Filter } from './filter'; +import { Table } from './table'; +import { getAllExpandedNodeIds } from './utils'; +import { SortConfig, sortNodes } from './sort'; +import ColumnHeader from './column-header'; + +interface FilterTreeProps { + nodes: TreeNode[]; + showCheckboxes: boolean; + showFilter: boolean; // Optional + checkedSeries: number[]; // Optional + collapsedNodes: number[]; + onToggleCheck: (ids: number[]) => void; // Optional + onToggleCollapse: (id: number, nodes: TreeNode[]) => void; + onOrderChange: (ids: number[]) => void; + showHeader: boolean; + headers: ColumnHeader[]; + className: string; +} + +interface FilterTreeState { + filteredNodes: TreeNode[]; + sortConfig: SortConfig[]; +} + +export class FilterTree extends React.Component { + static defaultProps: Partial = { + checkedSeries: [], + showFilter: true, + onToggleCheck: () => { /* Nothing to do */ }, + onOrderChange: () => { /* Nothing to do */ }, + }; + + constructor(props: FilterTreeProps) { + super(props); + this.state = { + filteredNodes: this.props.nodes, + sortConfig: [] + }; + } + + getRootNodes = (): TreeNode[] => { + const nodes = [...this.props.nodes]; + return nodes.filter((node: TreeNode) => node.isRoot === true); + }; + + getNode = (treeNodes: TreeNode[], id: number): TreeNode | undefined => { + const nodes: TreeNode[] = [...treeNodes]; + if (!nodes) { + return undefined; + } + let currentNode: TreeNode; + while (nodes.length) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + currentNode = nodes.pop()!; + if (currentNode.id === id) { + return currentNode; + } else { + if (currentNode.children && currentNode.children.length) { + currentNode.children.forEach((child: TreeNode) => { + nodes.push(child); + }); + } + } + } + return undefined; + }; + + handleCollapse = (id: number): void => { + const nodes = sortNodes(this.state.filteredNodes, this.state.sortConfig); + this.props.onToggleCollapse(id, nodes); + }; + + handleOrderChange = (nodes: TreeNode[]): void => { + const ids = getAllExpandedNodeIds(nodes, this.props.collapsedNodes); + this.props.onOrderChange(ids); + }; + + handleSortConfigChange = (sortConfig: SortConfig[]): void => { + this.setState({sortConfig: sortConfig}); + }; + + getAllChildrenIds = (node: TreeNode, ids: number[]): number[] => { + ids.push(node.id); + if (node.children.length) { + node.children.forEach((child: TreeNode) => { + this.getAllChildrenIds(child, ids); + }); + } + return ids; + }; + + getParentIdsToCheck = (parentId: number, ids: number[], toCheck: boolean): number[] => { + ids.push(parentId); + const parentNode = this.getNode(this.props.nodes, parentId); + if (parentNode) { + if (toCheck && this.areAllSiblingsChecked(parentNode)) { + this.getParentIdsToCheck(parentNode.parentId, ids, toCheck); + } else if (!toCheck && this.isNodeChecked(parentNode.parentId)) { + this.getParentIdsToCheck(parentNode.parentId, ids, toCheck); + } + } + return ids; + }; + + isNodeChecked = (id: number): boolean => this.props.checkedSeries.includes(id); + + areAllSiblingsChecked = (node: TreeNode): boolean => { + const parentNode = this.getNode(this.props.nodes, node.parentId); + if (parentNode) { + return parentNode.children.every((child: TreeNode) => this.isNodeChecked(child.id) || node.id === child.id); + } + return false; + }; + + handleCheck = (id: number): void => { + let checkedIds: number[] = []; + const checkedNode = this.getNode(this.props.nodes, id); + if (checkedNode) { + const childrenIds = this.getAllChildrenIds(checkedNode, []); + const visibleChildrenIds = childrenIds.filter((childId: number) =>this.getNode(this.state.filteredNodes, childId) !== undefined); + if (!this.isNodeChecked(id)) { + if (checkedNode.children.length) { + const childIdsToCheck = visibleChildrenIds.filter(childId => !this.isNodeChecked(childId)); + checkedIds = checkedIds.concat(childIdsToCheck); + } else { + checkedIds = checkedIds.concat(id); + } + if (this.areAllSiblingsChecked(checkedNode) && !this.isNodeChecked(checkedNode.parentId)) { + const parentsToCheck = this.getParentIdsToCheck(checkedNode.parentId, [], true); + checkedIds = checkedIds.concat(parentsToCheck); + } + } else { + if (checkedNode.children.length) { + checkedIds = checkedIds.concat(visibleChildrenIds); + } else { + checkedIds = checkedIds.concat(id); + } + if (this.isNodeChecked(checkedNode.parentId)) { + const parentsToCheck = this.getParentIdsToCheck(checkedNode.parentId, [], false); + checkedIds = checkedIds.concat(parentsToCheck); + } + } + this.props.onToggleCheck(checkedIds); + } + }; + + // returns 0 for unchecked, 1 for checked, 2 for half checked + getCheckedStatus = (id: number): number => { + const node = this.getNode(this.props.nodes, id); + if (node) { + if (node.children.length === 0) { + return this.isNodeChecked(id) ? 1 : 0; + } else { + if (this.isEveryChildChecked(node)) { + return 1; + } + if (this.isSomeChildChecked(node)) { + return 2; + } + } + } + return 0; + }; + + isEveryChildChecked = (node: TreeNode): boolean => { + const visibleNodes = node.children.filter((child: TreeNode) =>this.getNode(this.state.filteredNodes, child.id) !== undefined); + let allChildrenChecked = false; + if (visibleNodes.length) { + allChildrenChecked = visibleNodes.every((child: TreeNode) => { + let isChecked = this.isNodeChecked(child.id); + if (child.children.length) { + isChecked = isChecked && this.isEveryChildChecked(child); + } + return isChecked; + }); + } + const leaves = this.getAllLeavesId(this.state.filteredNodes, []); + const allLeavesChecked = leaves.every((id: number) => this.isNodeChecked(id)); + return allChildrenChecked || allLeavesChecked; + }; + + getAllLeavesId = (nodes: TreeNode[], ids: number[]): number[] => { + nodes.forEach((node: TreeNode) => { + if (node.children.length) { + this.getAllLeavesId(node.children, ids); + } else { + ids.push(node.id); + } + }); + return ids; + }; + + isSomeChildChecked = (node: TreeNode): boolean => node.children.some((child: TreeNode) => { + let isChecked = this.isNodeChecked(child.id); + if (child.children.length) { + isChecked = isChecked || this.isSomeChildChecked(child); + } + return isChecked; + }); + + isCollapsed = (id: number): boolean => this.props.collapsedNodes.includes(id); + + handleFilterChanged = (filter: string): void => { + let filteredTree: TreeNode[] = []; + const matchedIds: number[] = []; + const rootNodes = this.getRootNodes(); + rootNodes.forEach((node: TreeNode) => this.getMatchingIds(node, filter, matchedIds)); + filteredTree = this.filterTree(this.props.nodes, matchedIds); + this.setState({filteredNodes: filteredTree}); + this.handleOrderChange(filteredTree); + }; + + getMatchingIds = (node: TreeNode, filter: string, foundIds: number[]): boolean => { + let isMatching = node.labels[0].indexOf(filter) > -1; + if (node.children && node.children.length) { + node.children.forEach((child: TreeNode) => { + const hasMatchingChild = this.getMatchingIds(child, filter, foundIds); + isMatching = isMatching || hasMatchingChild; + }); + } + if (isMatching) { + foundIds.push(node.id); + } + return isMatching; + }; + + filterTree = (nodes: TreeNode[], matchedIds: number[]): TreeNode[] => + nodes.filter((node: TreeNode) => matchedIds.indexOf(node.id) > -1) + .map((node: TreeNode) => ({ + ...node, + children: node.children ? this.filterTree(node.children, matchedIds) : [] + })); + + renderFilterTree = (): JSX.Element => + )=> this.handleFilterChanged(e.target.value)}/> + {this.state.filteredNodes.length + ? this.renderTable(this.state.filteredNodes) + : No entries found + } + ; + + renderTable = (nodes: TreeNode[]): JSX.Element => + ; + + render(): JSX.Element | undefined { + if (!this.props.nodes) {return undefined;} + const rootNodes = this.getRootNodes(); + if (rootNodes && rootNodes.length) { + return + { this.props.showFilter + ? this.renderFilterTree() + : this.renderTable(rootNodes) + } + + ; + } else { + return ; + } + } +} diff --git a/react-components/src/components/utils/filtrer-tree/utils.tsx b/react-components/src/components/utils/filtrer-tree/utils.tsx new file mode 100644 index 00000000..eea5c56e --- /dev/null +++ b/react-components/src/components/utils/filtrer-tree/utils.tsx @@ -0,0 +1,57 @@ +import { Entry } from 'tsp-typescript-client/lib/models/entry'; +import { TreeNode } from './tree-node'; +import ColumnHeader from './column-header'; + +const entryToTreeNode = (entry: Entry, headers: ColumnHeader[]) => { + // TODO Instead of padding the labels, ColumnHeader should use a getter function instead of just assuming strings, this will allow to get the legend for XY charts + const labels = ((entry.labels) && (entry.labels.length > 0)) ? entry.labels : ['']; + // Pad the labels to match the header count + for (let i = labels.length; i <= headers.length - 1; i++) { + labels[i] = ''; + } + return ({ + labels: labels, + isRoot: false, + id: entry.id, + parentId: entry.parentId, + children: [] + } as TreeNode); +}; + +export const listToTree = (list: Entry[], headers: ColumnHeader[]): TreeNode[] => { + const rootNodes: TreeNode[] = []; + const lookup: { [key: string]: TreeNode } = {}; + // Fill-in the lookup table + list.forEach(entry => { + lookup[entry.id] = entryToTreeNode(entry, headers); + }); + // Create the tree in the order it has been received + list.forEach(entry => { + const node = lookup[entry.id]; + if ((entry.parentId !== undefined) && (entry.parentId !== -1)) { + const parent: TreeNode = lookup[entry.parentId]; + if (parent) { + parent.children.push(node); + } else { + // no parent available, treat is as root node + node.isRoot = true; + rootNodes.push(node); + } + } else { + node.isRoot = true; + rootNodes.push(node); + } + }); + return rootNodes; +}; + +export const getAllExpandedNodeIds = (nodes: TreeNode[],collapsedNodes: number[]): number[] => { + const visibleIds: number[] = []; + nodes.forEach((node: TreeNode) => { + visibleIds.push(node.id); + if (node.children.length && !collapsedNodes.includes(node.id)) { + visibleIds.push(...getAllExpandedNodeIds(node.children, collapsedNodes)); + } + }); + return visibleIds; +}; diff --git a/react-components/src/components/utils/output-component-style.ts b/react-components/src/components/utils/output-component-style.ts new file mode 100644 index 00000000..b7867ec0 --- /dev/null +++ b/react-components/src/components/utils/output-component-style.ts @@ -0,0 +1,14 @@ +export interface OutputComponentStyle { + width: number; + chartWidth: number; + // react-grid-layout - The library used for resizing components + // inserts new React components during compilation, and the dimensions + // it returns are strings (pixels). + // Currently, the components are only height-resizable. + height: number | string; + naviBackgroundColor: number; + chartBackgroundColor: number; + cursorColor: number; + lineColor: number; + rowHeight: number; +} diff --git a/react-components/src/components/utils/time-axis-component.tsx b/react-components/src/components/utils/time-axis-component.tsx new file mode 100644 index 00000000..a251c42b --- /dev/null +++ b/react-components/src/components/utils/time-axis-component.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { TimeGraphAxis } from 'timeline-chart/lib/layer/time-graph-axis'; +import { TimeGraphAxisCursors } from 'timeline-chart/lib/layer/time-graph-axis-cursors'; +import { ReactTimeGraphContainer } from './timegraph-container-component'; +import { TimeGraphUnitController } from 'timeline-chart/lib/time-graph-unit-controller'; + +interface TimeAxisProps { + unitController: TimeGraphUnitController; + style: { + chartWidth: number, + naviBackgroundColor: number, + cursorColor: number, + lineColor: number + }; + addWidgetResizeHandler: (handler: () => void) => void; +} + +export class TimeAxisComponent extends React.Component { + render(): JSX.Element { + return ; + } + + protected getAxisLayer(): TimeGraphAxis { + const timeAxisLayer = new TimeGraphAxis('timeGraphAxis', { + color: this.props.style.naviBackgroundColor, + lineColor: this.props.style.lineColor + }); + return timeAxisLayer; + } + + protected getAxisCursors(): TimeGraphAxisCursors { + return new TimeGraphAxisCursors('timeGraphAxisCursors', { color: this.props.style.cursorColor }); + } +} diff --git a/react-components/src/components/utils/time-navigator-component.tsx b/react-components/src/components/utils/time-navigator-component.tsx new file mode 100644 index 00000000..73c49fa3 --- /dev/null +++ b/react-components/src/components/utils/time-navigator-component.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { ReactTimeGraphContainer } from './timegraph-container-component'; +import { TimeGraphUnitController } from 'timeline-chart/lib/time-graph-unit-controller'; +import { TimeGraphNavigator } from 'timeline-chart/lib/layer/time-graph-navigator'; + +interface TimeNavigatorProps { + unitController: TimeGraphUnitController; + style: { + chartWidth: number, + naviBackgroundColor: number, + cursorColor: number, + lineColor: number + }; + addWidgetResizeHandler: (handler: () => void) => void; +} + +export class TimeNavigatorComponent extends React.Component { + render(): JSX.Element { + const navi = new TimeGraphNavigator('timeGraphNavigator'); + return ; + } +} diff --git a/react-components/src/components/utils/timegraph-container-component.tsx b/react-components/src/components/utils/timegraph-container-component.tsx new file mode 100644 index 00000000..7b29d84e --- /dev/null +++ b/react-components/src/components/utils/timegraph-container-component.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { TimeGraphContainer, TimeGraphContainerOptions } from 'timeline-chart/lib/time-graph-container'; +import { TimeGraphUnitController } from 'timeline-chart/lib/time-graph-unit-controller'; +import { TimeGraphLayer } from 'timeline-chart/lib/layer/time-graph-layer'; + +export namespace ReactTimeGraphContainer { + export interface Props { + id: string, + options: TimeGraphContainerOptions, + unitController: TimeGraphUnitController, + layer: TimeGraphLayer[], + onWidgetResize: (handler: () => void) => void + } +} + +export class ReactTimeGraphContainer extends React.Component { + protected ref: HTMLCanvasElement | undefined; + protected container?: TimeGraphContainer; + + componentDidMount(): void { + this.container = new TimeGraphContainer(this.props.options, this.props.unitController, this.ref); + this.props.layer.forEach(l => { + if (this.container) { this.container.addLayer(l); } + }); + + this.props.onWidgetResize(() => { + if (this.container) { this.container.reInitCanvasSize(this.props.options.width, this.props.options.height); } + }); + } + + shouldComponentUpdate(nextProps: ReactTimeGraphContainer.Props): boolean { + return nextProps.options.height !== this.props.options.height + || nextProps.options.width !== this.props.options.width; + } + + componentDidUpdate(prevProps: ReactTimeGraphContainer.Props): void { + if (prevProps.options.height !== this.props.options.height && this.container) { + this.container.reInitCanvasSize(this.props.options.width, this.props.options.height); + } + } + + render(): JSX.Element { + return this.ref = ref || undefined } onWheel={ e => e.preventDefault() } tabIndex={ 1 }>; + } +} diff --git a/react-components/src/components/xy-output-component.tsx b/react-components/src/components/xy-output-component.tsx new file mode 100644 index 00000000..38223b7a --- /dev/null +++ b/react-components/src/components/xy-output-component.tsx @@ -0,0 +1,301 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { AbstractOutputProps, AbstractOutputState } from './abstract-output-component'; +import { AbstractTreeOutputComponent } from './abstract-tree-output-component'; +import * as React from 'react'; +import { Line } from 'react-chartjs-2'; +import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; +import { Entry, EntryModel } from 'tsp-typescript-client/lib/models/entry'; +import { ResponseStatus } from 'tsp-typescript-client/lib/models/response/responses'; +import { XYSeries } from 'tsp-typescript-client/lib/models/xy'; +import Chart = require('chart.js'); +import { EntryTree } from './utils/filtrer-tree/entry-tree'; +import { getAllExpandedNodeIds } from './utils/filtrer-tree/utils'; +import { TreeNode } from './utils/filtrer-tree/tree-node'; +import ColumnHeader from './utils/filtrer-tree/column-header'; + +type XYOuputState = AbstractOutputState & { + selectedSeriesId: number[]; + XYTree: Entry[]; + checkedSeries: number[]; + collapsedNodes: number[]; + orderedNodes: number[]; + // FIXME Type this properly + XYData: any; + columns: ColumnHeader[]; +}; + +export class XYOutputComponent extends AbstractTreeOutputComponent { + private currentColorIndex = 0; + private colorMap: Map = new Map(); + + private lineChartRef: any; + + constructor(props: AbstractOutputProps) { + super(props); + this.state = { + outputStatus: ResponseStatus.RUNNING, + selectedSeriesId: [], + XYTree: [], + checkedSeries: [], + collapsedNodes: [], + orderedNodes: [], + XYData: {}, + columns: [{title: 'Name', sortable: true}] + }; + + this.afterChartDraw = this.afterChartDraw.bind(this); + Chart.pluginService.register({ + afterDraw: (chart, _easing) => { + this.afterChartDraw(chart); + } + }); + this.lineChartRef = React.createRef(); + } + + componentDidMount(): void { + this.waitAnalysisCompletion(); + } + + componentDidUpdate(prevProps: AbstractOutputProps, prevState: XYOuputState): void { + const viewRangeChanged = this.props.viewRange !== prevProps.viewRange; + const checkedSeriesChanged = this.state.checkedSeries !== prevState.checkedSeries; + const collapsedNodesChanged = this.state.collapsedNodes !== prevState.collapsedNodes; + const needToUpdate = viewRangeChanged || checkedSeriesChanged || !this.state.XYData || !this.state.XYTree.length || collapsedNodesChanged; + if (needToUpdate && this.state.outputStatus === ResponseStatus.COMPLETED) { + this.updateTree(); + this.updateXY(); + } + if (prevProps.style.chartWidth !== this.props.style.chartWidth) { + this.updateXY(); + } + if (this.lineChartRef.current) { + this.lineChartRef.current.chartInstance.render(); + } + } + + synchronizeTreeScroll(): void { /* Nothing to do by default */ } + + renderTree(): React.ReactNode | undefined { + this.onToggleCheck = this.onToggleCheck.bind(this); + this.onToggleCollapse = this.onToggleCollapse.bind(this); + this.onOrderChange = this.onOrderChange.bind(this); + return this.state.XYTree.length + ? + : undefined + ; + } + + renderChart(): React.ReactNode { + const lineOptions: Chart.ChartOptions = { + responsive: true, + elements: { + point: { radius: 0 }, + line: { tension: 0 } + }, + maintainAspectRatio: false, + legend: { display: false }, + layout: { + padding: { + left: 0, + right: 0, + top: 15, + bottom: 5 + } + }, + scales: { + xAxes: [{ id: 'time-axis', display: false }], + yAxes: [{ display: false }] + }, + animation: { duration: 0 }, + }; + // width={this.props.style.chartWidth} + return + {this.state.outputStatus === ResponseStatus.COMPLETED ? + : + 'Analysis running...'} + ; + } + + private afterChartDraw(chart: Chart) { + const ctx = chart.ctx; + const xScale = (chart as any).scales['time-axis']; + const ticks: number[] = xScale.ticks; + if (ctx && this.props.selectionRange) { + const valueStart = this.findNearestValue(this.props.selectionRange.getstart(), ticks); + const valueEnd = this.findNearestValue(this.props.selectionRange.getEnd(), ticks); + const pixelStart = xScale.getPixelForValue(this.props.selectionRange.getstart(), valueStart); + const pixelEnd = xScale.getPixelForValue(this.props.selectionRange.getEnd(), valueEnd); + ctx.save(); + + ctx.lineWidth = 1; + ctx.strokeStyle = '#259fd8'; + + ctx.beginPath(); + ctx.moveTo(pixelStart, 0); + ctx.lineTo(pixelStart, chart.chartArea.bottom); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(pixelEnd, 0); + ctx.lineTo(pixelEnd, chart.chartArea.bottom); + ctx.stroke(); + + ctx.globalAlpha = 0.2; + ctx.fillStyle = '#259fd8'; + ctx.fillRect(pixelStart, 0, pixelEnd - pixelStart, chart.chartArea.bottom); + + ctx.restore(); + } + } + + private findNearestValue(value: number, ticks: number[]): number { + let nearestIndex: number | undefined = undefined; + ticks.forEach((tick, index) => { + if (tick >= value) { + if (!nearestIndex) { + nearestIndex = index; + } + } + }); + return nearestIndex ? nearestIndex : 0; + } + + private onToggleCheck(ids: number[]) { + let newList = [...this.state.checkedSeries]; + ids.forEach(id => { + const exist = this.state.checkedSeries.find(seriesId => seriesId === id); + + if (exist !== undefined) { + newList = newList.filter(series => id !== series); + } else { + newList = newList.concat(id); + } + }); + this.setState({checkedSeries: newList}); + } + + private onToggleCollapse(id: number, nodes: TreeNode[]) { + let newList = [...this.state.collapsedNodes]; + + const exist = this.state.collapsedNodes.find(expandId => expandId === id); + + if (exist !== undefined) { + newList = newList.filter(collapsed => id !== collapsed); + } else { + newList = newList.concat(id); + } + const orderedIds = getAllExpandedNodeIds(nodes, newList); + this.setState({collapsedNodes: newList, orderedNodes: orderedIds}); + } + + private onOrderChange(ids: number[]) { + this.setState({orderedNodes: ids}); + } + + // private async waitAnalysisCompletion() { + // const traceUUID = this.props.traceId; + // const tspClient = this.props.tspClient; + // const outPutId = this.props.outputDescriptor.id; + + // // TODO Use the output descriptor to find out if the analysis is completed + // const xyTreeParameters = QueryHelper.selectionTimeQuery( + // QueryHelper.splitRangeIntoEqualParts(this.props.range.getstart(), this.props.range.getEnd(), 1120), []); // , [], { 'cpus': [] } + // let xyTreeResponse = (await tspClient.fetchXYTree(traceUUID, outPutId, xyTreeParameters)).getModel(); + // while (xyTreeResponse.status === ResponseStatus.RUNNING) { + // xyTreeResponse = (await tspClient.fetchXYTree(traceUUID, outPutId, xyTreeParameters)).getModel(); + // } + // this.setState({ + // outputStatus: xyTreeResponse.status + // }); + // } + + private async updateTree() { + // TODO Remove cpus parameters at some point. This is very specific to Trace Compass server + const xyTreeParameters = QueryHelper.selectionTimeQuery( + QueryHelper.splitRangeIntoEqualParts(this.props.range.getstart(), this.props.range.getEnd(), 1120), []); // , [], { 'cpus': [] } + const xyTreeResponse = (await this.props.tspClient.fetchXYTree(this.props.traceId, this.props.outputDescriptor.id, xyTreeParameters)).getModel(); + const treeModel = xyTreeResponse.model; + if (treeModel) { + this.buildTreeNodes(treeModel); + } + } + + private async updateXY() { + let start = 1332170682440133097; + let end = 1332170682540133097; + const viewRange = this.props.viewRange; + if (viewRange) { + start = viewRange.getstart(); + end = viewRange.getEnd(); + } + + const xyDataParameters = QueryHelper.selectionTimeQuery( + QueryHelper.splitRangeIntoEqualParts(Math.trunc(start), Math.trunc(end), this.props.style.chartWidth), this.state.checkedSeries); + + const xyDataResponse = (await this.props.tspClient.fetchXY(this.props.traceId, this.props.outputDescriptor.id, xyDataParameters)).getModel(); + this.buildXYData(xyDataResponse.model.series); + } + + private buildXYData(seriesObj: XYSeries[]) { + const dataSetArray = new Array(); + let xValues: number[] = []; + seriesObj.forEach(series => { + const color = this.getSeriesColor(series.seriesName); + xValues = series.xValues; + dataSetArray.push({ + label: series.seriesName, + fill: false, + borderColor: color, + borderWidth: 2, + data: series.yValues + }); + }); + const lineData = { + labels: xValues, + datasets: dataSetArray + }; + + this.setState({ + XYData: lineData + }); + } + + private buildTreeNodes(entryModel: EntryModel) { + const tree: Entry[] = entryModel.entries; + const headers = entryModel.headers; + const columns: ColumnHeader[] = []; + if (headers && headers.length > 0) { + headers.forEach(header => { + columns.push({title: header.name, sortable: true, tooltip: header.tooltip}); + }); + } else { + columns.push({title: 'Name', sortable: true}); + } + columns.push({title: 'Legend', sortable: false}); + this.setState({ + XYTree: tree, + columns + }); + } + + private getSeriesColor(key: string): string { + const colors = ['rgba(191, 33, 30, 1)', 'rgba(30, 56, 136, 1)', 'rgba(71, 168, 189, 1)', 'rgba(245, 230, 99, 1)', 'rgba(255, 173, 105, 1)', + 'rgba(216, 219, 226, 1)', 'rgba(212, 81, 19, 1)', 'rgba(187, 155, 176 , 1)', 'rgba(6, 214, 160, 1)', 'rgba(239, 71, 111, 1)']; + let colorIndex = this.colorMap.get(key); + if (colorIndex === undefined) { + colorIndex = this.currentColorIndex % colors.length; + this.colorMap.set(key, colorIndex); + this.currentColorIndex++; + } + return colors[colorIndex]; + } +} diff --git a/react-components/src/index.tsx b/react-components/src/index.tsx new file mode 100644 index 00000000..5d3e758d --- /dev/null +++ b/react-components/src/index.tsx @@ -0,0 +1,3 @@ +import { TraceContextComponent } from './components/trace-context-component'; + +export default TraceContextComponent; diff --git a/react-components/src/style/chart-line-solid.svg b/react-components/src/style/chart-line-solid.svg new file mode 100644 index 00000000..011f58ba --- /dev/null +++ b/react-components/src/style/chart-line-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-components/src/style/output-components-style.css b/react-components/src/style/output-components-style.css new file mode 100644 index 00000000..853b64e2 --- /dev/null +++ b/react-components/src/style/output-components-style.css @@ -0,0 +1,96 @@ +/* Output component styling */ +/* Main container*/ +.output-container { + display: flex; + color: var(--theia-ui-font-color0) +} + +.widget-handle { + background-color: var(--theia-layout-color4); + display: grid; + grid-template-rows: 25px 1fr; +} +.title-bar-label { + text-align: center; + writing-mode: vertical-rl; + height: 50%; + min-height: 150px; + transform: rotate(180deg); + user-select: none; + align-self: center; + justify-self: center; +} +.remove-component-button { + background: none; + border: none; + padding: 2px 8px; + align-self: center; + justify-self: center; + color: var(--theia-ui-font-color0) +} + +.main-output-container { + display: flex; +} + +.output-component-tree { + overflow-y: scroll; + white-space: pre-wrap; +} + +.output-component-chart { + align-self: center; + text-align: center; +} + +#timegraph-main { + width: 100%; + display: flex; +} + +#main { + border: 1px solid; + margin: 10px 0; + overflow: hidden; +} + +canvas { + display: block; +} + +.innerContainer { + width: 100%; +} + +.table-tree>tbody>tr:nth-child(1)>th { + background-color: var(--theia-editor-background); + position: sticky; + top: 0; +} + +.table-tree th, .table-tree td { + padding: 3px 5px; + text-align: left; + border-bottom: 1px solid #333; + border-right: 1px solid #333; + white-space: nowrap; + min-width: 50px; +} + +.timegraph-tree tr { + /* TODO: Fix row alignment, this number is arbitrary, it works [on my machine], but it should match line height in timeline-chart */ + line-height: 18px; + position: relative; + white-space: nowrap; + top: 50%; + padding: 0 0; +} + +#input-filter-tree { + background-color: var(--theia-input-background); + border: none; + border-bottom: 1px solid var(--theia-input-foreground); + padding: 3px; + width: 180px; + color: var(--theia-input-placeholder-foreground) +} \ No newline at end of file diff --git a/react-components/src/style/status-bar.css b/react-components/src/style/status-bar.css new file mode 100644 index 00000000..3c20c08d --- /dev/null +++ b/react-components/src/style/status-bar.css @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2020 École Polytechnique de montréal + */ + + +.traceserver-mod-offline #theia-statusBar { + background-color: var(--theia-statusBar-offlineBackground) !important; +} + +.traceserver-mod-offline #theia-statusBar .area .element { + color: var(--theia-statusBar-offlineForeground) !important; +} + +.traceserver-mod-offline #theia-statusBar .area .element.hasCommand:hover { + background-color: var(--theia-statusBarItem-offlineHoverBackground) !important; +} + +.traceserver-mod-offline #theia-statusBar .area .element.hasCommand:active { + background-color: var(--theia-statusBarItem-offlineActiveBackground) !important; +} diff --git a/react-components/src/style/trace-context-style.css b/react-components/src/style/trace-context-style.css new file mode 100644 index 00000000..04551291 --- /dev/null +++ b/react-components/src/style/trace-context-style.css @@ -0,0 +1,22 @@ +.no-output-placeholder { + color: var(--theia-ui-font-color3); + font-size: 24px; + transform: translate(30%, 500%); +} + +.react-grid-item.react-grid-placeholder { + background: var(--theia-accent-color3); +} + +.trace-context-container { + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.outputs-grid-layout { + flex-grow: 1; + overflow-y: scroll; + overflow-x: hidden; +} \ No newline at end of file diff --git a/react-components/src/style/trace-explorer.css b/react-components/src/style/trace-explorer.css new file mode 100644 index 00000000..89182a37 --- /dev/null +++ b/react-components/src/style/trace-explorer.css @@ -0,0 +1,165 @@ +.trace-explorer-tab-icon { + -webkit-mask: url('chart-line-solid.svg'); + mask: url('chart-line-solid.svg'); +} + +/* Main container */ +.trace-explorer-container { + display: grid; + grid-template-columns: 100%; + grid-template-rows: 1fr 1fr; /*1fr*/ + grid-row-gap: 10px; + color: var(--theia-ui-font-color0); +} + +.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: 2; + display: grid; + grid-template-columns: 100%; + grid-template-rows: 25px 1fr; +} + +.trace-explorer-panel-title { + grid-row-start: 1; + background-color: var(--theia-layout-color4); + /* 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 var(--theia-layout-color4); + /* color: white; */ + padding-inline-start: 5px; + 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: hidden; + white-space: nowrap; +} + +.outputs-list-container.theia-mod-selected { + background-color: var(--theia-selection-background); +} + +.trace-element-container { + display: grid; + grid-template-columns: minmax(0, 1fr) 50px; + grid-template-rows: auto; +} + +.trace-element-container.theia-mod-selected { + background-color: var(--theia-selection-background); +} + +.trace-element-info { + overflow-x: hidden; + grid-column-start: 1; +} + +.trace-element-name, .outputs-element-name { + font-weight: bold; +} + +.trace-element-path, .outputs-element-description { + color: var(--theia-ui-font-color2); + /* color: rgb(160, 160, 160); */ + white-space: pre; +} + +.trace-element-options { + grid-column-start: 2; + padding-left: 10px; +} + +.share-context-button { + width: 100%; + height: 100%; + min-width: 10px; + margin-left: 0px; +} + +.sharing-modal { + position: absolute; + top: 400px; + left: 600px; + right: 600px; + bottom: 550px; /*470px;*/ + background-color: rgb(30, 30, 30); +} + +.sharing-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6) +} + +.sharing-container { + margin: 10px; +} + +.sharing-description { + color: white; + padding-bottom: 15px; +} + +.sharing-link-info { + display: grid; + grid-template-columns: minmax(0, 1fr) 50px; +} + +.sharing-link { + grid-column-start: 1; +} + +.sharing-link>textarea { + color: white; + background-color: rgb(50, 50, 50); + border-color: var(--theia-ui-button-color); + min-width: 100%; + min-height: 100%; + resize: none; + box-sizing: border-box; +} + +.sharing-link-copy { + grid-column-start: 2; + padding-left: 10px; +} + +.copy-link-button { + width: 100%; + height: 100%; + min-width: 10px; + margin-left: 0px; +} + +.source-code-tooltip { + text-decoration: underline; +} \ No newline at end of file diff --git a/react-components/src/style/trace-viewer.css b/react-components/src/style/trace-viewer.css new file mode 100644 index 00000000..00f0b806 --- /dev/null +++ b/react-components/src/style/trace-viewer.css @@ -0,0 +1,16 @@ +/* textarea { + background-color: black; + color: var(--theia-ui-font-color0); + font-family: var(--theia-ui-font-family); +} + +div { + color: var(--theia-ui-font-color0); + font-family: var(--theia-ui-font-family); +} */ + +/* Main container */ +.trace-viewer-container { + margin: 0px 5px 0px 5px; + height: 100%; +} \ No newline at end of file diff --git a/react-components/tsconfig.json b/react-components/tsconfig.json new file mode 100644 index 00000000..5bdfc020 --- /dev/null +++ b/react-components/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "strict": true, + "sourceMap": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "module": "commonjs", + "target": "ES2020", + "rootDir": "./src", + "outDir": "./lib", + "jsx": "react", + "declaration": true, + "declarationMap": true, + "resolveJsonModule": true, + "skipLibCheck": true + }, + "include": [ + "src" + ], + "exclude": [ + "**/__tests__", + "**/*.spec.ts", + "**/*.test.ts" + ] +} \ No newline at end of file From 2d96953f96c0fb7ada515046288cf1ab379276f5 Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Thu, 19 Nov 2020 10:11:57 -0500 Subject: [PATCH 002/373] Simplify openExperiment conflict handling Use the same code for the first and subsequent attempts. Signed-off-by: Patrick Tasse --- base/src/experiment-manager.ts | 48 +++++++++++++--------------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/base/src/experiment-manager.ts b/base/src/experiment-manager.ts index 3260d37a..416f4e58 100644 --- a/base/src/experiment-manager.ts +++ b/base/src/experiment-manager.ts @@ -77,36 +77,23 @@ export class ExperimentManager { traceURIs.push(traces[i].UUID); } - const experimentResponse = await this.fTspClient.createExperiment(new Query({ - 'name': name, - 'traces': traceURIs - })); - const opendExperiment = experimentResponse.getModel(); - if (opendExperiment && experimentResponse.isOk()) { - this.addExperiment(opendExperiment); - signalManager().emit(Signals.EXPERIMENT_OPENED, {experiment: opendExperiment}); - return opendExperiment; - } else if (opendExperiment && experimentResponse.getStatusCode() === 409) { - // Repost with a suffix as long as there are conflicts - const handleConflict = async function (tspClient: TspClient, tryNb: number): Promise> { - const suffix = '(' + tryNb + ')'; - return tspClient.createExperiment(new Query({ - 'name': name + suffix, - 'traces': traceURIs - })); - }; - let conflictResolutionResponse = experimentResponse; - let i = 1; - while (conflictResolutionResponse.getStatusCode() === 409) { - conflictResolutionResponse = await handleConflict(this.fTspClient, i); - i++; - } - const experiment = conflictResolutionResponse.getModel(); - if (experiment && conflictResolutionResponse.isOk()) { - this.addExperiment(experiment); - signalManager().emit(Signals.EXPERIMENT_OPENED, {experiment: experiment}); - return experiment; - } + const tryCreate = async function (tspClient: TspClient, retry: number): Promise> { + return tspClient.createExperiment(new Query({ + 'name': retry === 0 ? name : name + '(' + retry + ')', + 'traces': traceURIs + })); + }; + let tryNb = 0; + let experimentResponse: TspClientResponse | undefined; + while (experimentResponse === undefined || experimentResponse.getStatusCode() === 409) { + experimentResponse = await tryCreate(this.fTspClient, tryNb); + tryNb++; + } + if (experimentResponse.isOk()) { + const experiment = experimentResponse.getModel(); + this.addExperiment(experiment); + signalManager().emit(Signals.EXPERIMENT_OPENED, {experiment: experiment}); + return experiment; } // TODO Handle any other experiment open errors return undefined; @@ -156,3 +143,4 @@ export class ExperimentManager { return deletedExperiment; } } + From 9c7e59b2d0c7b9582c1086fb7684b48aa70d6e04 Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Thu, 26 Nov 2020 16:19:15 -0500 Subject: [PATCH 003/373] Fix and simplify openTrace conflict handling Use the same code for the first and subsequent attempts. Set the name from traceURI if traceName is not specified. Fix undefined base name in retry attempts. Signed-off-by: Patrick Tasse --- base/src/trace-manager.ts | 48 +++++++++++++++------------------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/base/src/trace-manager.ts b/base/src/trace-manager.ts index 287232d1..ff9c9d2f 100644 --- a/base/src/trace-manager.ts +++ b/base/src/trace-manager.ts @@ -70,37 +70,25 @@ export class TraceManager { * @returns The opened trace */ async openTrace(traceURI: string, traceName?: string): Promise { - const traceResponse = await this.fTspClient.openTrace(new Query({ - 'name': traceName, - 'uri': traceURI - })); + const name = traceName ? traceName : traceURI.replace(/\/$/, '').replace(/(.*\/)?/, ''); - const openedTrace = traceResponse.getModel(); - if (openedTrace && traceResponse.isOk()) { - this.addTrace(openedTrace); - signalManager().emit(Signals.TRACE_OPENED, {trace: openedTrace}); - return openedTrace; - } else if (openedTrace && traceResponse.getStatusCode() === 409) { - // Repost with a suffix as long as there are conflicts - const handleConflict = async function (tspClient: TspClient, tryNb: number): Promise> { - const suffix = '(' + tryNb + ')'; - return tspClient.openTrace(new Query({ - 'name': name + suffix, - 'uri': traceURI - })); - }; - let conflictResolutionResponse = traceResponse; - let i = 1; - while (conflictResolutionResponse.getStatusCode() === 409) { - conflictResolutionResponse = await handleConflict(this.fTspClient, i); - i++; - } - const trace = conflictResolutionResponse.getModel(); - if (trace && conflictResolutionResponse.isOk()) { - this.addTrace(trace); - signalManager().emit(Signals.TRACE_OPENED, {trace: openedTrace}); - return trace; - } + const tryOpen = async function (tspClient: TspClient, retry: number): Promise> { + return tspClient.openTrace(new Query({ + 'name': retry === 0 ? name : name + '(' + retry + ')', + 'uri': traceURI + })); + }; + let tryNb = 0; + let traceResponse: TspClientResponse | undefined; + while (traceResponse === undefined || traceResponse.getStatusCode() === 409) { + traceResponse = await tryOpen(this.fTspClient, tryNb); + tryNb++; + } + if (traceResponse.isOk()) { + const trace = traceResponse.getModel(); + this.addTrace(trace); + signalManager().emit(Signals.TRACE_OPENED, {trace: trace}); + return trace; } // TODO Handle trace open errors return undefined; From 7ee09201461f100ef4fcd04b1d5241f2add43f4d Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Tue, 1 Dec 2020 13:15:44 -0500 Subject: [PATCH 004/373] Close traces after experiment and handle trace deletion rejection Wait for experiment deletion response to be received before attempting to close its traces. Handle trace deletion response '409 Conflict' and do not remove it from the trace manager if the deletion was rejected by the trace server. Signed-off-by: Patrick Tasse --- base/src/experiment-manager.ts | 18 +++++++++++++++++- base/src/trace-manager.ts | 10 ++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/base/src/experiment-manager.ts b/base/src/experiment-manager.ts index 416f4e58..c94869e0 100644 --- a/base/src/experiment-manager.ts +++ b/base/src/experiment-manager.ts @@ -3,6 +3,7 @@ import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; import { Query } from 'tsp-typescript-client/lib/models/query/query'; import { OutputDescriptor } from 'tsp-typescript-client/lib/models/output-descriptor'; import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; +import { TraceManager } from './trace-manager'; import { TspClientResponse } from 'tsp-typescript-client/lib/protocol/tsp-client-response'; import { signalManager, Signals } from './signal-manager'; @@ -10,11 +11,15 @@ export class ExperimentManager { private fOpenExperiments: Map = new Map(); private fTspClient: TspClient; + private fTraceManager: TraceManager; constructor( - tspClient: TspClient + tspClient: TspClient, + traceManager: TraceManager ) { this.fTspClient = tspClient; + this.fTraceManager = traceManager; + signalManager().on(Signals.EXPERIMENT_CLOSED, ({experiment}) => this.onExperimentClosed(experiment)); } /** @@ -133,6 +138,17 @@ export class ExperimentManager { } } + private onExperimentClosed(experiment: Experiment) { + /* + * TODO: Do not close traces used by another experiment + */ + // Close each trace + const traces = experiment.traces; + for (let i = 0; i < traces.length; i++) { + this.fTraceManager.closeTrace(traces[i].UUID); + } + } + private addExperiment(experiment: Experiment) { this.fOpenExperiments.set(experiment.UUID, experiment); } diff --git a/base/src/trace-manager.ts b/base/src/trace-manager.ts index ff9c9d2f..72a3326b 100644 --- a/base/src/trace-manager.ts +++ b/base/src/trace-manager.ts @@ -120,10 +120,12 @@ export class TraceManager { async closeTrace(traceUUID: string): Promise { const traceToClose = this.fOpenTraces.get(traceUUID); if (traceToClose) { - await this.fTspClient.deleteTrace(traceUUID); - const deletedTrace = this.removeTrace(traceUUID); - if (deletedTrace) { - signalManager().emit(Signals.TRACE_CLOSED, {trace: deletedTrace}); + const deleteResponse = await this.fTspClient.deleteTrace(traceUUID); + if (deleteResponse.getStatusCode() !== 409) { + const deletedTrace = this.removeTrace(traceUUID); + if (deletedTrace) { + signalManager().emit(Signals.TRACE_CLOSED, {trace: deletedTrace}); + } } } } From 764c488b979d27d397e4f2449c93056263bcc16d Mon Sep 17 00:00:00 2001 From: muddana-satish Date: Mon, 21 Dec 2020 00:28:04 -0800 Subject: [PATCH 005/373] Make trace-extension compatible with latest timeline-chart Signed-off-by: muddana-satish --- .../src/components/data-providers/tsp-data-provider.ts | 4 +++- .../src/components/timegraph-output-component.tsx | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/react-components/src/components/data-providers/tsp-data-provider.ts b/react-components/src/components/data-providers/tsp-data-provider.ts index 62daef78..4defa458 100644 --- a/react-components/src/components/data-providers/tsp-data-provider.ts +++ b/react-components/src/components/data-providers/tsp-data-provider.ts @@ -108,7 +108,8 @@ export class TspDataProvider { } else { gapStyle = entry.style; } - const states: TimelineChart.TimeGraphRowElementModel[] = []; + const states: TimelineChart.TimeGraphState[] = []; + const annotations: TimelineChart.TimeGraphAnnotation[] = []; let prevPossibleState = entry.start; let nextPossibleState = entry.end; row.states.forEach((state: TimeGraphState, idx: number) => { @@ -161,6 +162,7 @@ export class TspDataProvider { end: entry.end - chartStart }, states, + annotations, prevPossibleState, nextPossibleState }; diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index 63571df4..43392c05 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/react-components/src/components/timegraph-output-component.tsx @@ -58,7 +58,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent this.fetchTimegraphData(range, resolution), - rowElementStyleProvider: (model: TimelineChart.TimeGraphRowElementModel) => this.getElementStyle(model), + rowElementStyleProvider: (model: TimelineChart.TimeGraphState) => this.getElementStyle(model), rowStyleProvider: (row: TimelineChart.TimeGraphRowModel) => ({ backgroundColor: 0x979797,// 0xaaaaff, backgroundOpacity: row.selected ? 0.1 : 0, @@ -247,7 +247,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent Date: Mon, 7 Dec 2020 10:27:14 -0800 Subject: [PATCH 006/373] Change widget colors based on theia themes fixes #76 Signed-off-by: muddana-satish --- base/src/signal-manager.ts | 8 +++- .../components/abstract-output-component.tsx | 1 + .../abstract-tree-output-component.tsx | 5 +- .../src/components/table-output-component.tsx | 4 +- .../components/trace-context-component.tsx | 47 ++++++++++++++----- 5 files changed, 50 insertions(+), 15 deletions(-) diff --git a/base/src/signal-manager.ts b/base/src/signal-manager.ts index 6be9afe7..8706eb32 100644 --- a/base/src/signal-manager.ts +++ b/base/src/signal-manager.ts @@ -3,6 +3,7 @@ import { EventEmitter } from 'events'; export declare interface SignalManager { fireTooltipSignal(tooltip: { [key: string]: string }): void; + fireThemeChangedSignal(theme: string): void; } @@ -12,7 +13,8 @@ export const Signals = { EXPERIMENT_OPENED: 'experiment opened', EXPERIMENT_CLOSED: 'experiment closed', EXPERIMENT_SELECTED: 'experiment selected', - TOOLTIP_UPDATED: 'tooltip updated' + TOOLTIP_UPDATED: 'tooltip updated', + THEME_CHANGED: 'theme changed' }; export class SignalManager extends EventEmitter implements SignalManager { @@ -21,6 +23,10 @@ export class SignalManager extends EventEmitter implements SignalManager { this.emit(Signals.TOOLTIP_UPDATED, {tooltip}); } + fireThemeChangedSignal(theme: string) { + this.emit(Signals.THEME_CHANGED, theme); + } + } let instance: SignalManager = new SignalManager(); diff --git a/react-components/src/components/abstract-output-component.tsx b/react-components/src/components/abstract-output-component.tsx index 5286a805..9f28901d 100644 --- a/react-components/src/components/abstract-output-component.tsx +++ b/react-components/src/components/abstract-output-component.tsx @@ -21,6 +21,7 @@ export interface AbstractOutputProps { // Workaround for it needs width to be explicitly passed // https://github.com/STRML/react-grid-layout/issues/961 widthWPBugWorkaround: number; + backgroundTheme: string; onOutputRemove: (outputId: string) => void; // TODO Not sure unitController: TimeGraphUnitController; diff --git a/react-components/src/components/abstract-tree-output-component.tsx b/react-components/src/components/abstract-tree-output-component.tsx index bf7309ba..98a7e12b 100644 --- a/react-components/src/components/abstract-tree-output-component.tsx +++ b/react-components/src/components/abstract-tree-output-component.tsx @@ -13,7 +13,10 @@ export abstract class AbstractTreeOutputComponent

{this.renderTree()} -

+
{this.renderChart()}
; diff --git a/react-components/src/components/table-output-component.tsx b/react-components/src/components/table-output-component.tsx index 8f0b1ba3..afbcf7e6 100644 --- a/react-components/src/components/table-output-component.tsx +++ b/react-components/src/components/table-output-component.tsx @@ -51,7 +51,9 @@ export class TableOutputComponent extends AbstractOutputComponent + return
void) => void; + backgroundTheme: string; } interface TraceContextState { @@ -41,6 +43,7 @@ interface TraceContextState { experiment: Experiment traceIndexing: boolean; style: OutputComponentStyle; + backgroundTheme: string; } export class TraceContextComponent extends React.Component { @@ -84,11 +87,12 @@ export class TraceContextComponent extends React.Component { this.handleViewRangeChange(viewRangeParam); }); this.traceContextContainer = React.createRef(); this.initialize(); + signalManager().on(Signals.THEME_CHANGED, (theme: string) => this.updateBackgroundTheme(theme)); + } + + public updateBackgroundTheme(theme: string) { + this.setState({ + style: { + width: this.DEFAULT_COMPONENT_WIDTH, + chartWidth: this.DEFAULT_CHART_WIDTH, + height: this.DEFAULT_COMPONENT_HEIGHT, + rowHeight: this.DEFAULT_COMPONENT_ROWHEIGHT, + naviBackgroundColor: theme === 'light' ? 0xf4f7fb : 0x3f3f3f, + chartBackgroundColor: theme === 'light' ? 0xf4f7fb : 0x3f3f3f, + cursorColor: 0x259fd8, + lineColor: theme === 'light' ? 0x757575 : 0xbbbbbb + }, + backgroundTheme: theme + }); } private async initialize() { @@ -150,6 +171,7 @@ export class TraceContextComponent extends React.Component this.updateBackgroundTheme(theme)); this.props.messageManager.removeStatusMessage(this.INDEXING_STATUS_BAR_KEY); this.props.messageManager.removeStatusMessage(this.TIME_SELECTION_STATUS_BAR_KEY); } @@ -173,14 +195,14 @@ export class TraceContextComponent extends React.Component ({ - currentTimeSelection: new TimeRange(range.start, range.end, prevState.timeOffset) - })); + currentTimeSelection: new TimeRange(range.start, range.end, prevState.timeOffset) + })); } private handleViewRangeChange(viewRange: TimelineChart.TimeGraphRange) { this.setState(prevState => ({ - currentViewRange: new TimeRange(viewRange.start, viewRange.end, prevState.timeOffset) - })); + currentViewRange: new TimeRange(viewRange.start, viewRange.end, prevState.timeOffset) + })); } render(): JSX.Element { @@ -197,8 +219,8 @@ export class TraceContextComponent extends React.Component
{ - // Syntax to use ReactGridLayout with Custom Components, while passing resized dimensions to children: - // https://github.com/STRML/react-grid-layout/issues/299#issuecomment-524959229 + // Syntax to use ReactGridLayout with Custom Components, while passing resized dimensions to children: + // https://github.com/STRML/react-grid-layout/issues/299#issuecomment-524959229 } ; + addWidgetResizeHandler={this.addWidgetResizeHandler} />; case 'TREE_TIME_XY': return ; case 'TABLE': From ccefcdb23a2bb0ed3f098235c1204908f844748a Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Mon, 21 Dec 2020 11:09:52 -0500 Subject: [PATCH 007/373] Check for unsuccessful TspClientResponse Always check if the response is successful and if its model is set. Signed-off-by: Patrick Tasse --- base/src/experiment-manager.ts | 11 ++-- base/src/trace-manager.ts | 9 ++-- .../abstract-tree-output-component.tsx | 16 +++--- .../data-providers/style-provider.ts | 14 +++-- .../data-providers/tsp-data-provider.ts | 13 +++-- .../src/components/table-output-component.tsx | 33 +++++++----- .../components/timegraph-output-component.tsx | 52 ++++++++++++------- .../components/trace-context-component.tsx | 36 +++++++------ .../src/components/xy-output-component.tsx | 18 ++++--- 9 files changed, 122 insertions(+), 80 deletions(-) diff --git a/base/src/experiment-manager.ts b/base/src/experiment-manager.ts index c94869e0..3642d0ca 100644 --- a/base/src/experiment-manager.ts +++ b/base/src/experiment-manager.ts @@ -29,9 +29,10 @@ export class ExperimentManager { async getOpenedExperiments(): Promise { const openedExperiments: Array = []; // Look on the server for opened experiments - const experimentResponse = await this.fTspClient.fetchExperiments(); - if (experimentResponse.isOk()) { - openedExperiments.push(...experimentResponse.getModel()); + const experimentsResponse = await this.fTspClient.fetchExperiments(); + const experiments = experimentsResponse.getModel(); + if (experimentsResponse.isOk() && experiments) { + openedExperiments.push(...experiments); } return openedExperiments; } @@ -94,8 +95,8 @@ export class ExperimentManager { experimentResponse = await tryCreate(this.fTspClient, tryNb); tryNb++; } - if (experimentResponse.isOk()) { - const experiment = experimentResponse.getModel(); + const experiment = experimentResponse.getModel(); + if (experimentResponse.isOk() && experiment) { this.addExperiment(experiment); signalManager().emit(Signals.EXPERIMENT_OPENED, {experiment: experiment}); return experiment; diff --git a/base/src/trace-manager.ts b/base/src/trace-manager.ts index 72a3326b..42b72fa2 100644 --- a/base/src/trace-manager.ts +++ b/base/src/trace-manager.ts @@ -25,8 +25,9 @@ export class TraceManager { const openedTraces: Array = []; // Look on the server for opened trace const tracesResponse = await this.fTspClient.fetchTraces(); - if (tracesResponse.isOk()) { - openedTraces.push(...tracesResponse.getModel()); + const traces = tracesResponse.getModel(); + if (tracesResponse.isOk() && traces) { + openedTraces.push(...traces); } return openedTraces; } @@ -84,8 +85,8 @@ export class TraceManager { traceResponse = await tryOpen(this.fTspClient, tryNb); tryNb++; } - if (traceResponse.isOk()) { - const trace = traceResponse.getModel(); + const trace = traceResponse.getModel(); + if (traceResponse.isOk() && trace) { this.addTrace(trace); signalManager().emit(Signals.TRACE_OPENED, {trace: trace}); return trace; diff --git a/react-components/src/components/abstract-tree-output-component.tsx b/react-components/src/components/abstract-tree-output-component.tsx index 98a7e12b..a5f6b540 100644 --- a/react-components/src/components/abstract-tree-output-component.tsx +++ b/react-components/src/components/abstract-tree-output-component.tsx @@ -38,13 +38,17 @@ export abstract class AbstractTreeOutputComponent

{ if (!this.styles || forceUpdate) { - const styleResponse = await this.tspClient.fetchStyles(this.traceId, this.outputId, QueryHelper.query()); - const styleModel = styleResponse.getModel().model; - const styles = styleModel.styles; - this.styles = styles; - return styles; + const tspClientResponse = await this.tspClient.fetchStyles(this.traceId, this.outputId, QueryHelper.query()); + const styleResponse = tspClientResponse.getModel(); + if (tspClientResponse.isOk() && styleResponse) { + const styleModel = styleResponse.model; + const styles = styleModel.styles; + this.styles = styles; + return styles; + } + this.styles = {}; } return this.styles; } diff --git a/react-components/src/components/data-providers/tsp-data-provider.ts b/react-components/src/components/data-providers/tsp-data-provider.ts index 4defa458..e5343384 100644 --- a/react-components/src/components/data-providers/tsp-data-provider.ts +++ b/react-components/src/components/data-providers/tsp-data-provider.ts @@ -45,11 +45,14 @@ export class TspDataProvider { const end = viewRange.end + this.timeGraphEntries[0].start; statesParameters = QueryHelper.selectionTimeQuery(QueryHelper.splitRangeIntoEqualParts(Math.trunc(start), Math.trunc(end), resolution), ids); } - const stateResponse = (await this.client.fetchTimeGraphStates(this.traceUUID, - this.outputId, statesParameters)).getModel(); - - this.timeGraphRows = stateResponse.model.rows; - this.timeGraphRowsOrdering(ids); + const tspClientResponse = await this.client.fetchTimeGraphStates(this.traceUUID, this.outputId, statesParameters); + const stateResponse = tspClientResponse.getModel(); + if (tspClientResponse.isOk() && stateResponse) { + this.timeGraphRows = stateResponse.model.rows; + this.timeGraphRowsOrdering(ids); + } else { + this.timeGraphRows = []; + } // the start time which is normalized to logical 0 in timeline chart. const chartStart = this.timeGraphEntries[0].start; diff --git a/react-components/src/components/table-output-component.tsx b/react-components/src/components/table-output-component.tsx index afbcf7e6..da2c5672 100644 --- a/react-components/src/components/table-output-component.tsx +++ b/react-components/src/components/table-output-component.tsx @@ -74,10 +74,14 @@ export class TableOutputComponent extends AbstractOutputComponent(); + if (!tspClientResponse.isOk() || !lineResponse) { + return linesArray; + } const model = lineResponse.model; const lines = model.lines; - const linesArray = new Array(); lines.forEach(line => { const obj: any = {}; const cells = line.cells; @@ -127,8 +131,8 @@ export class TableOutputComponent extends AbstractOutputComponent = []; const columnsArray = new Array(); @@ -142,16 +146,19 @@ export class TableOutputComponent extends AbstractOutputComponent { - const id = this.showIndexColumn ? ++columnHeader.id : columnHeader.id; - colIds.push(id); - columnsArray.push({ - headerName: columnHeader.name, - field: columnHeader.id.toString(), - width: this.props.columnWidth - + if (tspClientResponse.isOk() && columnsResponse) { + const columnEntries = columnsResponse.model; + columnEntries.forEach(columnHeader => { + const id = this.showIndexColumn ? ++columnHeader.id : columnHeader.id; + colIds.push(id); + columnsArray.push({ + headerName: columnHeader.name, + field: columnHeader.id.toString(), + width: this.props.columnWidth + + }); }); - }); + } if (!this.showIndexColumn) { columnsArray[0].cellRenderer = 'loadingRenderer'; diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index 43392c05..e5608549 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/react-components/src/components/timegraph-output-component.tsx @@ -101,27 +101,39 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent { if (this.state.outputStatus !== ResponseStatus.COMPLETED || !this.state.timegraphTree.length) { const treeParameters = QueryHelper.timeQuery([0, 1]); - const treeResponse = (await this.props.tspClient.fetchTimeGraphTree(this.props.traceId, - this.props.outputDescriptor.id, treeParameters)).getModel(); - const nbEntries = treeResponse.model.entries.length; - this.totalHeight = nbEntries * this.props.style.rowHeight; - this.rowController.totalHeight = this.totalHeight; - const columns: ColumnHeader[] = []; - if (treeResponse.model.headers && treeResponse.model.headers.length > 0) { - treeResponse.model.headers.forEach(header => { - columns.push({title: header.name, sortable: true, tooltip: header.tooltip}); - }); - } else { - columns.push({title: 'Name', sortable: true}); - } + const tspClientResponse = await this.props.tspClient.fetchTimeGraphTree(this.props.traceId, + this.props.outputDescriptor.id, treeParameters); + const treeResponse = tspClientResponse.getModel(); // TODO Style should not be retreive in the "initialization" part or at least async - const styleResponse = (await this.props.tspClient.fetchStyles(this.props.traceId, this.props.outputDescriptor.id, QueryHelper.query())).getModel(); - this.setState({ - // outputStatus: ResponseStatus.COMPLETED, - timegraphTree: treeResponse.model.entries, - styleModel: styleResponse.model, - columns - }); + if (tspClientResponse.isOk() && treeResponse) { + const nbEntries = treeResponse.model.entries.length; + this.totalHeight = nbEntries * this.props.style.rowHeight; + this.rowController.totalHeight = this.totalHeight; + const columns: ColumnHeader[] = []; + if (treeResponse.model.headers && treeResponse.model.headers.length > 0) { + treeResponse.model.headers.forEach(header => { + columns.push({title: header.name, sortable: true, tooltip: header.tooltip}); + }); + } else { + columns.push({title: 'Name', sortable: true}); + } + const tspClientResponse2 = await this.props.tspClient.fetchStyles(this.props.traceId, this.props.outputDescriptor.id, QueryHelper.query()); + const styleResponse = tspClientResponse2.getModel(); + if (tspClientResponse2.isOk() && styleResponse) { + this.setState({ + // outputStatus: ResponseStatus.COMPLETED, + timegraphTree: treeResponse.model.entries, + styleModel: styleResponse.model, + columns + }); + } else { + this.setState({ + // outputStatus: ResponseStatus.COMPLETED, + timegraphTree: treeResponse.model.entries, + columns + }); + } + } this.chartLayer.updateChart(); } diff --git a/react-components/src/components/trace-context-component.tsx b/react-components/src/components/trace-context-component.tsx index 526e51e0..76a47d48 100644 --- a/react-components/src/components/trace-context-component.tsx +++ b/react-components/src/components/trace-context-component.tsx @@ -137,24 +137,28 @@ export class TraceContextComponent extends React.Component Date: Wed, 25 Nov 2020 14:43:02 -0500 Subject: [PATCH 008/373] Support fetching annotations and populating them in TimelineChart model Signed-off-by: Patrick Tasse --- .../data-providers/tsp-data-provider.ts | 51 ++++++++++++++--- .../components/timegraph-output-component.tsx | 57 ++++++++++++++----- 2 files changed, 87 insertions(+), 21 deletions(-) diff --git a/react-components/src/components/data-providers/tsp-data-provider.ts b/react-components/src/components/data-providers/tsp-data-provider.ts index e5343384..5561fb5d 100644 --- a/react-components/src/components/data-providers/tsp-data-provider.ts +++ b/react-components/src/components/data-providers/tsp-data-provider.ts @@ -3,6 +3,7 @@ import { TimeGraphEntry, TimeGraphRow, TimeGraphState } from 'tsp-typescript-cli import { TimelineChart } from 'timeline-chart/lib/time-graph-model'; import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; import { OutputElementStyle } from 'tsp-typescript-client/lib/models/styles'; +import { Annotation, Type } from 'tsp-typescript-client/lib/models/annotation'; export class TspDataProvider { @@ -32,20 +33,20 @@ export class TspDataProvider { return { id: 'model', totalLength: this.totalRange, - arrows: [], rows: [], + arrows: [], data: {} }; } this.totalRange = this.timeGraphEntries[0].end - this.timeGraphEntries[0].start; // 1332170682540133097 - starttime - let statesParameters = QueryHelper.selectionTimeQuery(QueryHelper.splitRangeIntoEqualParts(1332170682440133097, 1332170682540133097, 1120), ids); + let fetchParameters = QueryHelper.selectionTimeQuery(QueryHelper.splitRangeIntoEqualParts(1332170682440133097, 1332170682540133097, 1120), ids); if (viewRange && resolution) { const start = viewRange.start + this.timeGraphEntries[0].start; const end = viewRange.end + this.timeGraphEntries[0].start; - statesParameters = QueryHelper.selectionTimeQuery(QueryHelper.splitRangeIntoEqualParts(Math.trunc(start), Math.trunc(end), resolution), ids); + fetchParameters = QueryHelper.selectionTimeQuery(QueryHelper.splitRangeIntoEqualParts(Math.trunc(start), Math.trunc(end), resolution), ids); } - const tspClientResponse = await this.client.fetchTimeGraphStates(this.traceUUID, this.outputId, statesParameters); + const tspClientResponse = await this.client.fetchTimeGraphStates(this.traceUUID, this.outputId, fetchParameters); const stateResponse = tspClientResponse.getModel(); if (tspClientResponse.isOk() && stateResponse) { this.timeGraphRows = stateResponse.model.rows; @@ -65,11 +66,34 @@ export class TspDataProvider { } }); + const annotations: Map = new Map(); + const tspClientResponse2 = await this.client.fetchAnnotations(this.traceUUID, this.outputId, fetchParameters); + const annotationsResponse = tspClientResponse2.getModel(); + if (tspClientResponse2.isOk() && annotationsResponse) { + Object.values(annotationsResponse.model.annotations).forEach(categoryArray => { + categoryArray.forEach(annotation => { + if (annotation.type === Type.CHART) { + let entryArray = annotations.get(annotation.entryId); + if (entryArray === undefined) { + entryArray = []; + annotations.set(annotation.entryId, entryArray); + } + entryArray.push(this.getAnnotation(annotation, entryArray.length, chartStart)); + } + }); + }); + } + for (const [entryId, entryArray] of annotations.entries()) { + const row = rows.find(tgEntry => tgEntry.id === entryId); + if (row) { + row.annotations = entryArray; + } + } return { id: 'model', totalLength: this.totalRange, - arrows: [], rows, + arrows: [], data: { originalStart: chartStart } @@ -112,7 +136,6 @@ export class TspDataProvider { gapStyle = entry.style; } const states: TimelineChart.TimeGraphState[] = []; - const annotations: TimelineChart.TimeGraphAnnotation[] = []; let prevPossibleState = entry.start; let nextPossibleState = entry.end; row.states.forEach((state: TimeGraphState, idx: number) => { @@ -165,9 +188,23 @@ export class TspDataProvider { end: entry.end - chartStart }, states, - annotations, + annotations: [], prevPossibleState, nextPossibleState }; } + + private getAnnotation(annotation: Annotation, idx: number, chartStart: number) { + return { + id: annotation.entryId + '-' + idx, + range: { + start: annotation.time - chartStart, + end: annotation.time + annotation.duration - chartStart + }, + label: annotation.label, + data: { + style: annotation.style + }, + }; + } } diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index e5608549..5cd2169a 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/react-components/src/components/timegraph-output-component.tsx @@ -58,7 +58,8 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent this.fetchTimegraphData(range, resolution), - rowElementStyleProvider: (model: TimelineChart.TimeGraphState) => this.getElementStyle(model), + rowElementStyleProvider: (state: TimelineChart.TimeGraphState) => this.getStateStyle(state), + rowAnnotationStyleProvider: (annotation: TimelineChart.TimeGraphAnnotation) => this.getAnnotationStyle(annotation), rowStyleProvider: (row: TimelineChart.TimeGraphRowModel) => ({ backgroundColor: 0x979797,// 0xaaaaff, backgroundOpacity: row.selected ? 0.1 : 0, @@ -259,10 +260,10 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent Date: Thu, 7 Jan 2021 11:46:10 -0600 Subject: [PATCH 009/373] Fixed placeholder text positioning and updated placeholder text Signed-off-by: danieltomasku --- .../src/components/trace-context-component.tsx | 4 +++- react-components/src/style/trace-context-style.css | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/react-components/src/components/trace-context-component.tsx b/react-components/src/components/trace-context-component.tsx index 76a47d48..de6d5b02 100644 --- a/react-components/src/components/trace-context-component.tsx +++ b/react-components/src/components/trace-context-component.tsx @@ -265,7 +265,9 @@ export class TraceContextComponent extends React.Component - {'Add outputs by clicking on an analysis in the trace explorer view'} + {'Trace loaded successfully.'} +
+ {'To see available analyses, open the Trace Explorer view.'}

; } diff --git a/react-components/src/style/trace-context-style.css b/react-components/src/style/trace-context-style.css index 04551291..1dc6ed3a 100644 --- a/react-components/src/style/trace-context-style.css +++ b/react-components/src/style/trace-context-style.css @@ -1,7 +1,12 @@ .no-output-placeholder { color: var(--theia-ui-font-color3); font-size: 24px; - transform: translate(30%, 500%); + text-align: center; + position: relative; + transform: translate(0, -50%); + top: 50%; + width: 90%; + align-self: center; } .react-grid-item.react-grid-placeholder { From c408309fc51a8481b3fbdb8a8b980ea1e4ea16f1 Mon Sep 17 00:00:00 2001 From: Bernd Hufmann Date: Mon, 11 Jan 2021 09:24:52 -0500 Subject: [PATCH 010/373] Support inband OutputElementStyle for annotations To provide annotation markers the output styles needed to defined in the list of style provided through the fetch style method which are referenced by the parent style key in the annotation itself. This patch makes it possible to supply the OutputElementStyle inside the annotation without using the parent style key. Signed-off-by: Bernd Hufmann --- .../components/timegraph-output-component.tsx | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index 5cd2169a..6edb61dc 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/react-components/src/components/timegraph-output-component.tsx @@ -377,23 +377,24 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent Date: Tue, 15 Dec 2020 00:02:54 -0800 Subject: [PATCH 011/373] Sync event timestamp with timegraph Signed-off-by: muddana-satish --- base/src/signal-manager.ts | 14 +++++++---- .../src/components/table-output-component.tsx | 17 +++++++++++++- .../components/timegraph-output-component.tsx | 23 +++++++++++++++---- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/base/src/signal-manager.ts b/base/src/signal-manager.ts index 8706eb32..7e5f7255 100644 --- a/base/src/signal-manager.ts +++ b/base/src/signal-manager.ts @@ -4,29 +4,35 @@ export declare interface SignalManager { fireTooltipSignal(tooltip: { [key: string]: string }): void; fireThemeChangedSignal(theme: string): void; + fireSelectionChangedSignal(payload: { [key: string]: string }): void; } export const Signals = { - TRACE_OPENED : 'trace opened', - TRACE_CLOSED : 'trace closed', + TRACE_OPENED: 'trace opened', + TRACE_CLOSED: 'trace closed', EXPERIMENT_OPENED: 'experiment opened', EXPERIMENT_CLOSED: 'experiment closed', EXPERIMENT_SELECTED: 'experiment selected', TOOLTIP_UPDATED: 'tooltip updated', - THEME_CHANGED: 'theme changed' + THEME_CHANGED: 'theme changed', + SELECTION_CHANGED: 'selection changed' }; export class SignalManager extends EventEmitter implements SignalManager { fireTooltipSignal(tooltip: { [key: string]: string; }): void { - this.emit(Signals.TOOLTIP_UPDATED, {tooltip}); + this.emit(Signals.TOOLTIP_UPDATED, { tooltip }); } fireThemeChangedSignal(theme: string) { this.emit(Signals.THEME_CHANGED, theme); } + fireSelectionChangedSignal(payload: { [key: string]: string; }): void { + this.emit(Signals.SELECTION_CHANGED, { payload }); + } + } let instance: SignalManager = new SignalManager(); diff --git a/react-components/src/components/table-output-component.tsx b/react-components/src/components/table-output-component.tsx index da2c5672..e5bececa 100644 --- a/react-components/src/components/table-output-component.tsx +++ b/react-components/src/components/table-output-component.tsx @@ -2,9 +2,10 @@ import { AbstractOutputComponent, AbstractOutputProps, AbstractOutputState } from './abstract-output-component'; import * as React from 'react'; import { AgGridReact } from 'ag-grid-react'; -import { ColDef, IDatasource, GridReadyEvent } from 'ag-grid-community'; +import { ColDef, IDatasource, GridReadyEvent, CellClickedEvent } from 'ag-grid-community'; import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; import { cloneDeep } from 'lodash'; +import { signalManager } from '@trace-viewer/base/lib/signal-manager'; type TableOuputState = AbstractOutputState & { tableColumns: ColDef[]; @@ -64,11 +65,25 @@ export class TableOutputComponent extends AbstractOutputComponent ; } + private onEventClick(event: CellClickedEvent) { + const columns = event.columnApi.getAllColumns(); + const timestampHeader = columns.find(column => column.getColDef().headerName === 'Timestamp ns'); + if (timestampHeader) { + const timestamp = timestampHeader.getColDef().field; + const payload = { + 'timestamp': (timestamp ? event.data[timestamp] : '') + }; + signalManager().fireSelectionChangedSignal(payload); + } + } + private async fetchTableLines(fetchIndex: number, linesToFetch: number) { const traceUUID = this.props.traceId; const tspClient = this.props.tspClient; diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index 6edb61dc..a2a3452b 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/react-components/src/components/timegraph-output-component.tsx @@ -10,7 +10,7 @@ import { TimeGraphRowController } from 'timeline-chart/lib/time-graph-row-contro import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; import { ResponseStatus } from 'tsp-typescript-client/lib/models/response/responses'; import { TimeGraphEntry } from 'tsp-typescript-client/lib/models/timegraph'; -import { signalManager } from '@trace-viewer/base/lib/signal-manager'; +import { signalManager, Signals } from '@trace-viewer/base/lib/signal-manager'; import { AbstractOutputProps, AbstractOutputState } from './abstract-output-component'; import { AbstractTreeOutputComponent } from './abstract-tree-output-component'; import { StyleProvider } from './data-providers/style-provider'; @@ -37,6 +37,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent; private tspDataProvider: TspDataProvider; @@ -69,7 +70,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent { if (this.treeRef.current) { this.treeRef.current.scrollTop = this.rowController.verticalOffset; @@ -87,6 +88,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent this.onSelectionChanged(payload)); } synchronizeTreeScroll(): void { @@ -154,6 +156,19 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent
; From 90b314d25513073b9180ac85bc128b95959d16c0 Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Wed, 13 Jan 2021 14:47:05 -0500 Subject: [PATCH 012/373] Support synchronization of event table from time graph selection Signed-off-by: Patrick Tasse --- .../components/abstract-output-component.tsx | 1 + .../src/components/table-output-component.tsx | 95 ++++++++++++++++--- .../components/trace-context-component.tsx | 1 + 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/react-components/src/components/abstract-output-component.tsx b/react-components/src/components/abstract-output-component.tsx index 9f28901d..656486ad 100644 --- a/react-components/src/components/abstract-output-component.tsx +++ b/react-components/src/components/abstract-output-component.tsx @@ -12,6 +12,7 @@ export interface AbstractOutputProps { tspClient: TspClient; traceId: string; range: TimeRange; + nbEvents: number; viewRange: TimeRange; selectionRange: TimeRange | undefined; resolution?: number; diff --git a/react-components/src/components/table-output-component.tsx b/react-components/src/components/table-output-component.tsx index e5bececa..de458398 100644 --- a/react-components/src/components/table-output-component.tsx +++ b/react-components/src/components/table-output-component.tsx @@ -2,10 +2,11 @@ import { AbstractOutputComponent, AbstractOutputProps, AbstractOutputState } from './abstract-output-component'; import * as React from 'react'; import { AgGridReact } from 'ag-grid-react'; -import { ColDef, IDatasource, GridReadyEvent, CellClickedEvent } from 'ag-grid-community'; +import { ColDef, IDatasource, GridReadyEvent, CellClickedEvent, GridApi } from 'ag-grid-community'; import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; import { cloneDeep } from 'lodash'; import { signalManager } from '@trace-viewer/base/lib/signal-manager'; +import { TimelineChart } from 'timeline-chart/lib/time-graph-model'; type TableOuputState = AbstractOutputState & { tableColumns: ColDef[]; @@ -27,6 +28,9 @@ export class TableOutputComponent extends AbstractOutputComponent(); private showIndexColumn = false; private components: any; + private gridApi: GridApi | undefined = undefined; + private pendingIndex: number | undefined = undefined; + private lastIndex = { timestamp: Number.MIN_VALUE, index: 0 }; static defaultProps: Partial = { cacheBlockSize: 200, @@ -49,6 +53,10 @@ export class TableOutputComponent extends AbstractOutputComponent { this.handleTimeSelectionChange(range); }); + this.onEventClick = this.onEventClick.bind(this); + this.onModelUpdated = this.onModelUpdated.bind(this); } renderMainArea(): React.ReactNode { @@ -67,29 +75,43 @@ export class TableOutputComponent extends AbstractOutputComponent ; } + componentDidMount() { + this.props.unitController.onSelectionRangeChange(range => { this.handleTimeSelectionChange(range); }); + } + + componentWillUnmount() { + // TODO: replace with removing the handler from unit controller + // See timeline-chart issue #98 + // In the meantime, replace the handler with a noop on unmount + this.handleTimeSelectionChange = () => Promise.resolve(); + } + private onEventClick(event: CellClickedEvent) { const columns = event.columnApi.getAllColumns(); const timestampHeader = columns.find(column => column.getColDef().headerName === 'Timestamp ns'); if (timestampHeader) { - const timestamp = timestampHeader.getColDef().field; - const payload = { - 'timestamp': (timestamp ? event.data[timestamp] : '') - }; - signalManager().fireSelectionChangedSignal(payload); + const timestampCol = timestampHeader.getColDef().field; + if (timestampCol) { + const timestamp = event.data[timestampCol]; + const payload = { 'timestamp': timestamp }; + this.lastIndex = { timestamp: Math.trunc(Number(timestamp)), index: event.rowIndex }; + signalManager().fireSelectionChangedSignal(payload); + } } } private async fetchTableLines(fetchIndex: number, linesToFetch: number) { const traceUUID = this.props.traceId; const tspClient = this.props.tspClient; - const outPutId = this.props.outputDescriptor.id; + const outputId = this.props.outputDescriptor.id; - const tspClientResponse = await tspClient.fetchTableLines(traceUUID, outPutId, QueryHelper.tableQuery(this.columnIds, fetchIndex, linesToFetch)); + const tspClientResponse = await tspClient.fetchTableLines(traceUUID, outputId, QueryHelper.tableQuery(this.columnIds, fetchIndex, linesToFetch)); const lineResponse = tspClientResponse.getModel(); const linesArray = new Array(); if (!tspClientResponse.isOk() || !lineResponse) { @@ -117,6 +139,7 @@ export class TableOutputComponent extends AbstractOutputComponent { + this.gridApi = event.api; const dataSource: IDatasource = { getRows: async params => { if (this.fetchColumns) { @@ -130,11 +153,7 @@ export class TableOutputComponent extends AbstractOutputComponent = []; const columnsArray = new Array(); @@ -186,4 +205,50 @@ export class TableOutputComponent extends AbstractOutputComponent { + if (this.pendingIndex && this.gridApi) { + if (this.gridApi.getSelectedNodes().length === 0) { + this.gridApi.getDisplayedRowAtIndex(this.pendingIndex).setSelected(true); + } + this.pendingIndex = undefined; + } + }; } diff --git a/react-components/src/components/trace-context-component.tsx b/react-components/src/components/trace-context-component.tsx index de6d5b02..8dbe3577 100644 --- a/react-components/src/components/trace-context-component.tsx +++ b/react-components/src/components/trace-context-component.tsx @@ -236,6 +236,7 @@ export class TraceContextComponent extends React.Component Date: Tue, 19 Jan 2021 11:24:58 -0500 Subject: [PATCH 013/373] Pack event table columns on first model update Signed-off-by: Patrick Tasse --- .../src/components/table-output-component.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/react-components/src/components/table-output-component.tsx b/react-components/src/components/table-output-component.tsx index de458398..7bf84290 100644 --- a/react-components/src/components/table-output-component.tsx +++ b/react-components/src/components/table-output-component.tsx @@ -2,7 +2,7 @@ import { AbstractOutputComponent, AbstractOutputProps, AbstractOutputState } from './abstract-output-component'; import * as React from 'react'; import { AgGridReact } from 'ag-grid-react'; -import { ColDef, IDatasource, GridReadyEvent, CellClickedEvent, GridApi } from 'ag-grid-community'; +import { ColDef, IDatasource, GridReadyEvent, CellClickedEvent, GridApi, ColumnApi } from 'ag-grid-community'; import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; import { cloneDeep } from 'lodash'; import { signalManager } from '@trace-viewer/base/lib/signal-manager'; @@ -29,8 +29,10 @@ export class TableOutputComponent extends AbstractOutputComponent = { cacheBlockSize: 200, @@ -54,7 +56,6 @@ export class TableOutputComponent extends AbstractOutputComponent { this.handleTimeSelectionChange(range); }); this.onEventClick = this.onEventClick.bind(this); this.onModelUpdated = this.onModelUpdated.bind(this); } @@ -140,6 +141,7 @@ export class TableOutputComponent extends AbstractOutputComponent { this.gridApi = event.api; + this.columnApi = event.columnApi; const dataSource: IDatasource = { getRows: async params => { if (this.fetchColumns) { @@ -244,11 +246,15 @@ export class TableOutputComponent extends AbstractOutputComponent { - if (this.pendingIndex && this.gridApi) { + if (this.pendingIndex && this.gridApi) { if (this.gridApi.getSelectedNodes().length === 0) { this.gridApi.getDisplayedRowAtIndex(this.pendingIndex).setSelected(true); } this.pendingIndex = undefined; - } + } + if (this.columnArray.length > 0 && !this.columnsPacked && this.columnApi) { + this.columnApi.autoSizeAllColumns(); + this.columnsPacked = true; + } }; } From 9f8c77f7e9a3bf4e8823223eb12318613cd7818f Mon Sep 17 00:00:00 2001 From: kenneth-marut-work Date: Tue, 12 Jan 2021 20:50:52 +0000 Subject: [PATCH 014/373] Panelize widget Signed-off-by: Kenneth Marut --- base/src/signal-manager.ts | 4 +- .../components/trace-context-component.tsx | 2 +- react-components/src/style/trace-explorer.css | 67 ++++++++++--------- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/base/src/signal-manager.ts b/base/src/signal-manager.ts index 7e5f7255..fe378cc0 100644 --- a/base/src/signal-manager.ts +++ b/base/src/signal-manager.ts @@ -25,7 +25,7 @@ export class SignalManager extends EventEmitter implements SignalManager { this.emit(Signals.TOOLTIP_UPDATED, { tooltip }); } - fireThemeChangedSignal(theme: string) { + fireThemeChangedSignal(theme: string): void { this.emit(Signals.THEME_CHANGED, theme); } @@ -37,7 +37,7 @@ export class SignalManager extends EventEmitter implements SignalManager { let instance: SignalManager = new SignalManager(); -export const setSignalManagerInstance = (sm: SignalManager) => { +export const setSignalManagerInstance = (sm: SignalManager): void => { instance = sm; }; diff --git a/react-components/src/components/trace-context-component.tsx b/react-components/src/components/trace-context-component.tsx index 8dbe3577..ff53f85c 100644 --- a/react-components/src/components/trace-context-component.tsx +++ b/react-components/src/components/trace-context-component.tsx @@ -113,7 +113,7 @@ export class TraceContextComponent extends React.Component this.updateBackgroundTheme(theme)); } - public updateBackgroundTheme(theme: string) { + public updateBackgroundTheme(theme: string): void { this.setState({ style: { width: this.DEFAULT_COMPONENT_WIDTH, diff --git a/react-components/src/style/trace-explorer.css b/react-components/src/style/trace-explorer.css index 89182a37..f0142285 100644 --- a/react-components/src/style/trace-explorer.css +++ b/react-components/src/style/trace-explorer.css @@ -1,36 +1,21 @@ -.trace-explorer-tab-icon { - -webkit-mask: url('chart-line-solid.svg'); - mask: url('chart-line-solid.svg'); +:root { + --trace-extension-list-line-height: 16px; } -/* Main container */ -.trace-explorer-container { - display: grid; - grid-template-columns: 100%; - grid-template-rows: 1fr 1fr; /*1fr*/ - grid-row-gap: 10px; - color: var(--theia-ui-font-color0); +#trace-explorer-analysis-widget, +#trace-explorer-opened-traces-widget { + height: 100%; } -.trace-explorer-opened { - grid-row-start: 1; - display: grid; - grid-template-columns: 100%; - grid-template-rows: 25px 1fr; +.trace-explorer-tab-icon { + -webkit-mask: url('chart-line-solid.svg'); + mask: url('chart-line-solid.svg'); } -/* .trace-explorer-files { - grid-row-start: 2; - display: grid; - grid-template-columns: 100%; - grid-template-rows: 25px 1fr; -} */ - -.trace-explorer-analysis { - grid-row-start: 2; - display: grid; - grid-template-columns: 100%; - grid-template-rows: 25px 1fr; +.trace-explorer-opened, +.trace-explorer-analysis, +.trace-explorer-tooltip { + margin: 0 2px; } .trace-explorer-panel-title { @@ -46,7 +31,6 @@ grid-row-start: 2; border: 1px solid var(--theia-layout-color4); /* color: white; */ - padding-inline-start: 5px; white-space: nowrap; } @@ -61,18 +45,27 @@ white-space: nowrap; } +.trace-list-container:hover, +.outputs-list-container:hover { + background-color: var(--theia-list-hoverBackground); + cursor: pointer; +} + +.trace-list-container.theia-mod-selected, .outputs-list-container.theia-mod-selected { background-color: var(--theia-selection-background); } -.trace-element-container { +/* Share options have been commented out, grid is disabled to optimize horizontal space */ +/* .trace-element-container { display: grid; grid-template-columns: minmax(0, 1fr) 50px; grid-template-rows: auto; -} +} */ -.trace-element-container.theia-mod-selected { - background-color: var(--theia-selection-background); +/* Remove focus outline on grid when selected */ +.ReactVirtualized__Grid { + outline: none; } .trace-element-info { @@ -82,12 +75,20 @@ .trace-element-name, .outputs-element-name { font-weight: bold; + margin: unset; + height: var(--trace-extension-list-line-height); } -.trace-element-path, .outputs-element-description { +.trace-element-path-container, .outputs-element-description { + margin: unset; color: var(--theia-ui-font-color2); /* color: rgb(160, 160, 160); */ white-space: pre; + font-size: .95em; +} + +.child-element { + height: var(--trace-extension-list-line-height); } .trace-element-options { From 0c3f52bc9b7791cedad3d3880da3edbebce82ac4 Mon Sep 17 00:00:00 2001 From: Ankush Tyagi Date: Mon, 18 Jan 2021 16:35:32 +0100 Subject: [PATCH 015/373] Keep the color of status bar blue even if the server is offline Signed-off-by: Ankush Tyagi --- .../components/trace-context-component.tsx | 1 - react-components/src/style/status-bar.css | 22 ------------------- 2 files changed, 23 deletions(-) delete mode 100644 react-components/src/style/status-bar.css diff --git a/react-components/src/components/trace-context-component.tsx b/react-components/src/components/trace-context-component.tsx index ff53f85c..184ee53b 100644 --- a/react-components/src/components/trace-context-component.tsx +++ b/react-components/src/components/trace-context-component.tsx @@ -3,7 +3,6 @@ import '../style/trace-viewer.css'; import '../style/trace-context-style.css'; import '../style/output-components-style.css'; import '../style/trace-explorer.css'; -import '../style/status-bar.css'; import { Layout, Responsive, WidthProvider } from 'react-grid-layout'; import { TimelineChart } from 'timeline-chart/lib/time-graph-model'; import { TimeGraphUnitController } from 'timeline-chart/lib/time-graph-unit-controller'; diff --git a/react-components/src/style/status-bar.css b/react-components/src/style/status-bar.css deleted file mode 100644 index 3c20c08d..00000000 --- a/react-components/src/style/status-bar.css +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * - * Copyright (C) 2020 École Polytechnique de montréal - */ - - -.traceserver-mod-offline #theia-statusBar { - background-color: var(--theia-statusBar-offlineBackground) !important; -} - -.traceserver-mod-offline #theia-statusBar .area .element { - color: var(--theia-statusBar-offlineForeground) !important; -} - -.traceserver-mod-offline #theia-statusBar .area .element.hasCommand:hover { - background-color: var(--theia-statusBarItem-offlineHoverBackground) !important; -} - -.traceserver-mod-offline #theia-statusBar .area .element.hasCommand:active { - background-color: var(--theia-statusBarItem-offlineActiveBackground) !important; -} From b0a5afd7c2ab724e8cb9156dff5dd9ed8266d6ad Mon Sep 17 00:00:00 2001 From: Abdellah Rahmani Date: Tue, 12 Jan 2021 11:27:57 -0500 Subject: [PATCH 016/373] transformed cursor into cross-arrow when moving is possible fixes #163 Signed-off-by: Abdellah Rahmani --- react-components/src/style/output-components-style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/react-components/src/style/output-components-style.css b/react-components/src/style/output-components-style.css index 853b64e2..b6b89111 100644 --- a/react-components/src/style/output-components-style.css +++ b/react-components/src/style/output-components-style.css @@ -19,6 +19,7 @@ user-select: none; align-self: center; justify-self: center; + cursor: move; } .remove-component-button { background: none; From ae64cdf99de01391fdeb6ccc40ac1887014e44f4 Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Mon, 25 Jan 2021 17:12:12 -0500 Subject: [PATCH 017/373] Update total height on toggle of node collapse/expand state Fixes https://github.com/theia-ide/timeline-chart/issues/53 when total height is reduced below chart height by collapsing nodes. Signed-off-by: Patrick Tasse --- .../components/timegraph-output-component.tsx | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index a2a3452b..91499117 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/react-components/src/components/timegraph-output-component.tsx @@ -109,9 +109,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent 0) { treeResponse.model.headers.forEach(header => { @@ -120,21 +118,11 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent this.isVisible(entry)); + this.totalHeight = visibleEntries.length * this.props.style.rowHeight; + this.rowController.totalHeight = this.totalHeight; + } + + private isVisible(entry: TimeGraphEntry): boolean { + let parentId = entry.parentId; + while (parentId !== undefined && parentId !== -1) { + if (this.state.collapsedNodes.includes(parentId)) { + return false; + } + const parent = this.state.timegraphTree.find(e => e.id === parentId); + parentId = parent ? parent.parentId : undefined; + } + return true; } private onSelectionChanged(payload: { [key: string]: number }) { From cabd07684e3c0ee6d1fe36ec8bf1745321398b85 Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Thu, 28 Jan 2021 16:07:36 -0500 Subject: [PATCH 018/373] Fix exponential recursion when fetching timegraph tree Reverts the exponential recursion back to a linear recursion. The linear recursion should be addressed in a later patch. Partially fixes https://github.com/theia-ide/theia-trace-extension/issues/254 Signed-off-by: Patrick Tasse --- .../src/components/timegraph-output-component.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index 91499117..1076d163 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/react-components/src/components/timegraph-output-component.tsx @@ -109,7 +109,6 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent 0) { treeResponse.model.headers.forEach(header => { @@ -118,12 +117,18 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent Date: Fri, 18 Dec 2020 19:49:37 -0500 Subject: [PATCH 019/373] reorganize the project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use TypeScript references to make TypeScript compilation faster: - Added a script to generate TypeScript references from yarn's workspaces under `scripts/`. - Run `yarn tsbuild` to only build changed TypeScript files. - Rewrite yarn scripts accordingly: - The `prepare` scripts now doesn't build TypeScript packages. - `yarn clean` has to be called manually if you still need it. - Move packages around: - Example applications are now under `examples/`. - Theia extensions are under `theia-extensions/`. - Removed the `assets` script from `@trace-viewer/react-components`: - Placed the styles under `style` and added it to `files` in the `package.json` file. You can now do `yarn tsbuild` from the root to only compile what changed and dependent files. You can also use `yarn tswatch` to watch all TypeScript files and rebuild when needed. It only works for TypeScript files, you still need to rerun `theia build` to regenerate and build the example applications. The advantage of `tswatch` is that it will catch workspace-wide errors as soon as you introduce a problem into files. Signed-off-by: Paul Maréchal --- base/package.json | 7 +++--- base/tsconfig.json | 7 ++++-- react-components/package.json | 11 +++++----- .../components/trace-context-component.tsx | 9 ++++---- .../{src => }/style/chart-line-solid.svg | 0 .../style/output-components-style.css | 0 react-components/style/status-bar.css | 22 +++++++++++++++++++ .../{src => }/style/trace-context-style.css | 0 .../{src => }/style/trace-explorer.css | 0 .../{src => }/style/trace-viewer.css | 0 react-components/tsconfig.json | 12 +++++++--- 11 files changed, 49 insertions(+), 19 deletions(-) rename react-components/{src => }/style/chart-line-solid.svg (100%) rename react-components/{src => }/style/output-components-style.css (100%) create mode 100644 react-components/style/status-bar.css rename react-components/{src => }/style/trace-context-style.css (100%) rename react-components/{src => }/style/trace-explorer.css (100%) rename react-components/{src => }/style/trace-viewer.css (100%) diff --git a/base/package.json b/base/package.json index c1c60661..9cb13d0b 100644 --- a/base/package.json +++ b/base/package.json @@ -25,11 +25,10 @@ "typescript": "latest" }, "scripts": { - "build": "tsc", - "clean": "rimraf lib", + "build": "tsc -b", + "clean": "rimraf lib *.tsbuildinfo", "lint": "eslint .", - "prepare": "yarn clean && yarn build", "test": "echo 'test'", "watch": "tsc -w" } -} \ No newline at end of file +} diff --git a/base/tsconfig.json b/base/tsconfig.json index 5a096972..668988d4 100644 --- a/base/tsconfig.json +++ b/base/tsconfig.json @@ -1,16 +1,19 @@ { "compilerOptions": { + "composite": true, "strict": true, "sourceMap": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "module": "commonjs", "target": "ES2020", + "rootDir": "src", "outDir": "lib", "declaration": true, "skipLibCheck": true }, "include": [ "src" - ] -} \ No newline at end of file + ], + "references": [] +} diff --git a/react-components/package.json b/react-components/package.json index 37ced015..ea465f0b 100644 --- a/react-components/package.json +++ b/react-components/package.json @@ -9,7 +9,8 @@ }, "files": [ "lib", - "src" + "src", + "style" ], "dependencies": { "@fortawesome/fontawesome-svg-core": "^1.2.17", @@ -55,12 +56,10 @@ "typescript": "latest" }, "scripts": { - "assets": "mkdir -p ./lib/style && cp -r ./src/style/* ./lib/style", - "build": "tsc && yarn run assets", - "clean": "rimraf lib", + "build": "tsc -b", + "clean": "rimraf lib *.tsbuildinfo", "lint": "eslint .", - "prepare": "yarn clean && yarn build", "test": "jest --config jest.config.json", - "watch": "tsc -w" + "watch": "tsc -b -w" } } diff --git a/react-components/src/components/trace-context-component.tsx b/react-components/src/components/trace-context-component.tsx index 184ee53b..8d1fae5a 100644 --- a/react-components/src/components/trace-context-component.tsx +++ b/react-components/src/components/trace-context-component.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import '../style/trace-viewer.css'; -import '../style/trace-context-style.css'; -import '../style/output-components-style.css'; -import '../style/trace-explorer.css'; +import '../../style/trace-viewer.css'; +import '../../style/trace-context-style.css'; +import '../../style/output-components-style.css'; +import '../../style/trace-explorer.css'; +import '../../style/status-bar.css'; import { Layout, Responsive, WidthProvider } from 'react-grid-layout'; import { TimelineChart } from 'timeline-chart/lib/time-graph-model'; import { TimeGraphUnitController } from 'timeline-chart/lib/time-graph-unit-controller'; diff --git a/react-components/src/style/chart-line-solid.svg b/react-components/style/chart-line-solid.svg similarity index 100% rename from react-components/src/style/chart-line-solid.svg rename to react-components/style/chart-line-solid.svg diff --git a/react-components/src/style/output-components-style.css b/react-components/style/output-components-style.css similarity index 100% rename from react-components/src/style/output-components-style.css rename to react-components/style/output-components-style.css diff --git a/react-components/style/status-bar.css b/react-components/style/status-bar.css new file mode 100644 index 00000000..3c20c08d --- /dev/null +++ b/react-components/style/status-bar.css @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2020 École Polytechnique de montréal + */ + + +.traceserver-mod-offline #theia-statusBar { + background-color: var(--theia-statusBar-offlineBackground) !important; +} + +.traceserver-mod-offline #theia-statusBar .area .element { + color: var(--theia-statusBar-offlineForeground) !important; +} + +.traceserver-mod-offline #theia-statusBar .area .element.hasCommand:hover { + background-color: var(--theia-statusBarItem-offlineHoverBackground) !important; +} + +.traceserver-mod-offline #theia-statusBar .area .element.hasCommand:active { + background-color: var(--theia-statusBarItem-offlineActiveBackground) !important; +} diff --git a/react-components/src/style/trace-context-style.css b/react-components/style/trace-context-style.css similarity index 100% rename from react-components/src/style/trace-context-style.css rename to react-components/style/trace-context-style.css diff --git a/react-components/src/style/trace-explorer.css b/react-components/style/trace-explorer.css similarity index 100% rename from react-components/src/style/trace-explorer.css rename to react-components/style/trace-explorer.css diff --git a/react-components/src/style/trace-viewer.css b/react-components/style/trace-viewer.css similarity index 100% rename from react-components/src/style/trace-viewer.css rename to react-components/style/trace-viewer.css diff --git a/react-components/tsconfig.json b/react-components/tsconfig.json index 5bdfc020..41b6bcc0 100644 --- a/react-components/tsconfig.json +++ b/react-components/tsconfig.json @@ -1,13 +1,14 @@ { "compilerOptions": { + "composite": true, "strict": true, "sourceMap": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "module": "commonjs", "target": "ES2020", - "rootDir": "./src", - "outDir": "./lib", + "rootDir": "src", + "outDir": "lib", "jsx": "react", "declaration": true, "declarationMap": true, @@ -21,5 +22,10 @@ "**/__tests__", "**/*.spec.ts", "**/*.test.ts" + ], + "references": [ + { + "path": "../base" + } ] -} \ No newline at end of file +} From 35738e287cd73407bfac3d8c9ab272ecc7bf3531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Mar=C3=A9chal?= Date: Mon, 25 Jan 2021 16:41:42 -0500 Subject: [PATCH 020/373] ts: target es5 to avoid runtime issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Webpack has trouble bundling ES2020 scripts, and things like `a?.b` will make it fail. It is recommended to run ES5 code when dealing with Theia applications anyway. Signed-off-by: Paul Maréchal --- base/tsconfig.json | 3 ++- react-components/tsconfig.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/base/tsconfig.json b/base/tsconfig.json index 668988d4..af635928 100644 --- a/base/tsconfig.json +++ b/base/tsconfig.json @@ -6,7 +6,8 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "module": "commonjs", - "target": "ES2020", + "target": "ES5", + "downlevelIteration": true, "rootDir": "src", "outDir": "lib", "declaration": true, diff --git a/react-components/tsconfig.json b/react-components/tsconfig.json index 41b6bcc0..f64d6595 100644 --- a/react-components/tsconfig.json +++ b/react-components/tsconfig.json @@ -6,7 +6,8 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "module": "commonjs", - "target": "ES2020", + "target": "ES5", + "downlevelIteration": true, "rootDir": "src", "outDir": "lib", "jsx": "react", From 54869b1f15c24e323889b3ab2d09b6f2269ecacd Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Fri, 5 Feb 2021 11:14:40 -0500 Subject: [PATCH 021/373] Fetch the style model only once Fetch the style model from the style provider when the component is mounted, not every time the component is updated if the outputStatus is not COMPLETED. Signed-off-by: Patrick Tasse --- .../data-providers/style-provider.ts | 20 ++++++++----------- .../components/timegraph-output-component.tsx | 12 +++++------ 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/react-components/src/components/data-providers/style-provider.ts b/react-components/src/components/data-providers/style-provider.ts index 662c0cfc..089f64c8 100644 --- a/react-components/src/components/data-providers/style-provider.ts +++ b/react-components/src/components/data-providers/style-provider.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; -import { OutputElementStyle } from 'tsp-typescript-client/lib/models/styles'; +import { OutputStyleModel } from 'tsp-typescript-client/lib/models/styles'; export class StyleProvider { private tspClient: TspClient; @@ -10,7 +10,7 @@ export class StyleProvider { private tmpStyleObject: { [key: string]: { [key: string]: { [key: string]: any } } }; - private styles: { [key: string]: OutputElementStyle } | undefined; + private styleModel: OutputStyleModel | undefined; constructor(outputId: string, traceId: string, tspClient: TspClient) { this.outputId = outputId; @@ -80,22 +80,18 @@ export class StyleProvider { } /** - * Get the style for a specific output - * @param forceUpdate Force the update of the current cached styles from the server + * Get the style model for a specific output + * @param forceUpdate Force the update of the current cached style model from the server */ - public async getStyles(forceUpdate?: boolean): Promise<{ [key: string]: OutputElementStyle }> { - if (!this.styles || forceUpdate) { + public async getStyleModel(forceUpdate?: boolean): Promise { + if (!this.styleModel || forceUpdate) { const tspClientResponse = await this.tspClient.fetchStyles(this.traceId, this.outputId, QueryHelper.query()); const styleResponse = tspClientResponse.getModel(); if (tspClientResponse.isOk() && styleResponse) { - const styleModel = styleResponse.model; - const styles = styleModel.styles; - this.styles = styles; - return styles; + this.styleModel = styleResponse.model; } - this.styles = {}; } - return this.styles; + return this.styleModel; } public getStylesTmp(_forceUpdate?: boolean): { [key: string]: { [key: string]: any } } { diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index 1076d163..e32fe2fe 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/react-components/src/components/timegraph-output-component.tsx @@ -41,6 +41,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent; private tspDataProvider: TspDataProvider; + private styleProvider: StyleProvider; private styleMap = new Map(); private selectedElement: TimeGraphRowElement | undefined; @@ -55,6 +56,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent { + this.setState({ + styleModel: await this.styleProvider.getStyleModel() + }); this.waitAnalysisCompletion(); } @@ -117,17 +122,10 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent Date: Mon, 8 Feb 2021 14:45:51 -0500 Subject: [PATCH 022/373] Fetch the tree only while running analysis and use throttling The tree should not be re-fetched when the component is updated, as this creates an endless loop of fetching the tree while the analysis is running. Use a single fetch tree to both update the model and monitor the analysis state. Introduce a delay between fetch tree iterations. Update the chart while the analysis is running and once more on analysis completion. Always check that the tree model is defined before trying to access it. Signed-off-by: Patrick Tasse --- .../abstract-tree-output-component.tsx | 25 ++--- .../components/timegraph-output-component.tsx | 31 +++--- .../src/components/xy-output-component.tsx | 103 +++++++----------- 3 files changed, 63 insertions(+), 96 deletions(-) diff --git a/react-components/src/components/abstract-tree-output-component.tsx b/react-components/src/components/abstract-tree-output-component.tsx index a5f6b540..b80caee2 100644 --- a/react-components/src/components/abstract-tree-output-component.tsx +++ b/react-components/src/components/abstract-tree-output-component.tsx @@ -1,6 +1,5 @@ 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'; export abstract class AbstractTreeOutputComponent

extends AbstractOutputComponent { @@ -30,24 +29,14 @@ export abstract class AbstractTreeOutputComponent

; + protected async waitAnalysisCompletion(): Promise { - const traceUUID = this.props.traceId; - const tspClient = this.props.tspClient; - const outPutId = this.props.outputDescriptor.id; - - // TODO Use the output descriptor to find out if the analysis is completed - const xyTreeParameters = QueryHelper.selectionTimeQuery( - QueryHelper.splitRangeIntoEqualParts(this.props.range.getstart(), this.props.range.getEnd(), 1120), []); - let tspClientResponse = await tspClient.fetchXYTree(traceUUID, outPutId, xyTreeParameters); - let xyTreeResponse = tspClientResponse.getModel(); - while (tspClientResponse.isOk() && xyTreeResponse && xyTreeResponse.status === ResponseStatus.RUNNING) { - tspClientResponse = await tspClient.fetchXYTree(traceUUID, outPutId, xyTreeParameters); - xyTreeResponse = tspClientResponse.getModel(); - } - if (tspClientResponse.isOk() && xyTreeResponse) { - this.setState({ - outputStatus: xyTreeResponse.status - }); + let outputStatus = this.state.outputStatus; + const timeout = 500; + while (this.state && outputStatus === ResponseStatus.RUNNING) { + outputStatus = await this.fetchTree(); + await new Promise(resolve => setTimeout(resolve, timeout)); } } diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index e32fe2fe..f379e2aa 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/react-components/src/components/timegraph-output-component.tsx @@ -106,17 +106,16 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent { - if (this.state.outputStatus !== ResponseStatus.COMPLETED || !this.state.timegraphTree.length) { - const treeParameters = QueryHelper.timeQuery([0, 1]); - const tspClientResponse = await this.props.tspClient.fetchTimeGraphTree(this.props.traceId, - this.props.outputDescriptor.id, treeParameters); - const treeResponse = tspClientResponse.getModel(); - // TODO Style should not be retreive in the "initialization" part or at least async - if (tspClientResponse.isOk() && treeResponse) { - const columns: ColumnHeader[] = []; - if (treeResponse.model.headers && treeResponse.model.headers.length > 0) { - treeResponse.model.headers.forEach(header => { + async fetchTree(): Promise { + const parameters = QueryHelper.timeQuery([this.props.range.getstart(), this.props.range.getEnd()]); + const tspClientResponse = await this.props.tspClient.fetchTimeGraphTree(this.props.traceId, this.props.outputDescriptor.id, parameters); + const treeResponse = tspClientResponse.getModel(); + if (tspClientResponse.isOk() && treeResponse) { + if (treeResponse.model) { + const headers = treeResponse.model.headers; + const columns = []; + if (headers && headers.length > 0) { + headers.forEach(header => { columns.push({title: header.name, sortable: true, tooltip: header.tooltip}); }); } else { @@ -125,13 +124,17 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent { + if (prevState.outputStatus === ResponseStatus.RUNNING || + this.state.collapsedNodes !== prevState.collapsedNodes) { this.chartLayer.updateChart(); } } diff --git a/react-components/src/components/xy-output-component.tsx b/react-components/src/components/xy-output-component.tsx index 6066c985..50b43332 100644 --- a/react-components/src/components/xy-output-component.tsx +++ b/react-components/src/components/xy-output-component.tsx @@ -4,7 +4,7 @@ import { AbstractTreeOutputComponent } from './abstract-tree-output-component'; import * as React from 'react'; import { Line } from 'react-chartjs-2'; import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; -import { Entry, EntryModel } from 'tsp-typescript-client/lib/models/entry'; +import { Entry } from 'tsp-typescript-client/lib/models/entry'; import { ResponseStatus } from 'tsp-typescript-client/lib/models/response/responses'; import { XYSeries } from 'tsp-typescript-client/lib/models/xy'; import Chart = require('chart.js'); @@ -15,12 +15,12 @@ import ColumnHeader from './utils/filtrer-tree/column-header'; type XYOuputState = AbstractOutputState & { selectedSeriesId: number[]; - XYTree: Entry[]; + xyTree: Entry[]; checkedSeries: number[]; collapsedNodes: number[]; orderedNodes: number[]; // FIXME Type this properly - XYData: any; + xyData: any; columns: ColumnHeader[]; }; @@ -35,11 +35,11 @@ export class XYOutputComponent extends AbstractTreeOutputComponent { + const parameters = QueryHelper.timeQuery([this.props.range.getstart(), this.props.range.getEnd()]); + const tspClientResponse = await this.props.tspClient.fetchXYTree(this.props.traceId, this.props.outputDescriptor.id, parameters); + const treeResponse = tspClientResponse.getModel(); + if (tspClientResponse.isOk() && treeResponse) { + if (treeResponse.model) { + const headers = treeResponse.model.headers; + const columns = []; + if (headers && headers.length > 0) { + headers.forEach(header => { + columns.push({title: header.name, sortable: true, tooltip: header.tooltip}); + }); + } else { + columns.push({title: 'Name', sortable: true}); + } + columns.push({title: 'Legend', sortable: false}); + this.setState({ + outputStatus: treeResponse.status, + xyTree: treeResponse.model.entries, + columns + }); + } + return treeResponse.status; + } + return ResponseStatus.FAILED; + } + componentDidUpdate(prevProps: AbstractOutputProps, prevState: XYOuputState): void { const viewRangeChanged = this.props.viewRange !== prevProps.viewRange; const checkedSeriesChanged = this.state.checkedSeries !== prevState.checkedSeries; const collapsedNodesChanged = this.state.collapsedNodes !== prevState.collapsedNodes; - const needToUpdate = viewRangeChanged || checkedSeriesChanged || !this.state.XYData || !this.state.XYTree.length || collapsedNodesChanged; - if (needToUpdate && this.state.outputStatus === ResponseStatus.COMPLETED) { - this.updateTree(); - this.updateXY(); - } - if (prevProps.style.chartWidth !== this.props.style.chartWidth) { + const chartWidthChanged = this.props.style.chartWidth !== prevProps.style.chartWidth; + const needToUpdate = viewRangeChanged || checkedSeriesChanged || collapsedNodesChanged || chartWidthChanged; + if (needToUpdate || prevState.outputStatus === ResponseStatus.RUNNING) { this.updateXY(); } if (this.lineChartRef.current) { @@ -79,9 +103,9 @@ export class XYOutputComponent extends AbstractTreeOutputComponent {this.state.outputStatus === ResponseStatus.COMPLETED ? - : + : 'Analysis running...'} ; } @@ -201,37 +225,6 @@ export class XYOutputComponent extends AbstractTreeOutputComponent(traceUUID, outPutId, xyTreeParameters)).getModel(); - // while (xyTreeResponse.status === ResponseStatus.RUNNING) { - // xyTreeResponse = (await tspClient.fetchXYTree(traceUUID, outPutId, xyTreeParameters)).getModel(); - // } - // this.setState({ - // outputStatus: xyTreeResponse.status - // }); - // } - - private async updateTree() { - // TODO Remove cpus parameters at some point. This is very specific to Trace Compass server - const xyTreeParameters = QueryHelper.selectionTimeQuery( - QueryHelper.splitRangeIntoEqualParts(this.props.range.getstart(), this.props.range.getEnd(), 1120), []); // , [], { 'cpus': [] } - const tspClientResponse = await this.props.tspClient.fetchXYTree(this.props.traceId, this.props.outputDescriptor.id, xyTreeParameters); - const xyTreeResponse = tspClientResponse.getModel(); - if (tspClientResponse.isOk() && xyTreeResponse) { - const treeModel = xyTreeResponse.model; - if (treeModel) { - this.buildTreeNodes(treeModel); - } - } - } - private async updateXY() { let start = 1332170682440133097; let end = 1332170682540133097; @@ -271,25 +264,7 @@ export class XYOutputComponent extends AbstractTreeOutputComponent) { - const tree: Entry[] = entryModel.entries; - const headers = entryModel.headers; - const columns: ColumnHeader[] = []; - if (headers && headers.length > 0) { - headers.forEach(header => { - columns.push({title: header.name, sortable: true, tooltip: header.tooltip}); - }); - } else { - columns.push({title: 'Name', sortable: true}); - } - columns.push({title: 'Legend', sortable: false}); - this.setState({ - XYTree: tree, - columns + xyData: lineData }); } From eebf86f9a2c85fad94738eae715ddd6361ccaa2c Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Wed, 10 Feb 2021 14:40:59 -0500 Subject: [PATCH 023/373] Update Event Table row count during trace indexing Signed-off-by: Patrick Tasse --- .../src/components/table-output-component.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/react-components/src/components/table-output-component.tsx b/react-components/src/components/table-output-component.tsx index 7bf84290..2f55a7f4 100644 --- a/react-components/src/components/table-output-component.tsx +++ b/react-components/src/components/table-output-component.tsx @@ -82,17 +82,23 @@ export class TableOutputComponent extends AbstractOutputComponent; } - componentDidMount() { + componentDidMount(): void { this.props.unitController.onSelectionRangeChange(range => { this.handleTimeSelectionChange(range); }); } - componentWillUnmount() { + componentWillUnmount(): void { // TODO: replace with removing the handler from unit controller // See timeline-chart issue #98 // In the meantime, replace the handler with a noop on unmount this.handleTimeSelectionChange = () => Promise.resolve(); } + async componentDidUpdate(prevProps: TableOutputProps, prevState: TableOuputState): Promise { + if (this.props.nbEvents !== prevProps.nbEvents) { + this.gridApi?.setInfiniteRowCount(this.props.nbEvents); + } + } + private onEventClick(event: CellClickedEvent) { const columns = event.columnApi.getAllColumns(); const timestampHeader = columns.find(column => column.getColDef().headerName === 'Timestamp ns'); From e648d79f4f93d96a2c76a7feb33067f81dcbbd4a Mon Sep 17 00:00:00 2001 From: Arnaud Fiorini Date: Fri, 12 Feb 2021 20:21:38 -0500 Subject: [PATCH 024/373] XY Chart: Support time and time range selection When clicking on the XY chart, the user can select a time. When clicking and dragging, the user can select a time range. fixes #121 Signed-off-by: Arnaud Fiorini --- .../src/components/xy-output-component.tsx | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/react-components/src/components/xy-output-component.tsx b/react-components/src/components/xy-output-component.tsx index 50b43332..acf3cad1 100644 --- a/react-components/src/components/xy-output-component.tsx +++ b/react-components/src/components/xy-output-component.tsx @@ -29,6 +29,25 @@ export class XYOutputComponent extends AbstractTreeOutputComponent = new Map(); private lineChartRef: any; + private mouseIsDown = false; + private posPixelSelect = 0; + + private updateSelection = (event: MouseEvent) => { + if (this.mouseIsDown && this.props.unitController.selectionRange) { + const xStartPos = this.props.unitController.selectionRange.start; + const scale = this.props.viewRange.getEnd() - this.props.viewRange.getstart(); + this.props.unitController.selectionRange = { + start: xStartPos, + end: xStartPos + ((event.screenX - this.posPixelSelect) / this.lineChartRef.current.chartInstance.width) * scale + }; + } + }; + + private endSelection = () => { + this.mouseIsDown = false; + document.removeEventListener('mousemove', this.updateSelection); + document.removeEventListener('mouseup', this.endSelection); + }; constructor(props: AbstractOutputProps) { super(props); @@ -140,11 +159,19 @@ export class XYOutputComponent extends AbstractTreeOutputComponent {this.state.outputStatus === ResponseStatus.COMPLETED ? - : +

this.beginSelection(event)}> + + +
: 'Analysis running...'} ; } @@ -225,6 +252,21 @@ export class XYOutputComponent extends AbstractTreeOutputComponent) { + this.mouseIsDown = true; + this.posPixelSelect = event.nativeEvent.screenX; + const offset = this.props.viewRange.getOffset() ?? 0; + const scale = this.props.viewRange.getEnd() - this.props.viewRange.getstart(); + const xPos = this.props.viewRange.getstart() - offset + + (event.nativeEvent.offsetX / this.lineChartRef.current.chartInstance.width) * scale; + this.props.unitController.selectionRange = { + start: xPos, + end: xPos + }; + document.addEventListener('mousemove', this.updateSelection); + document.addEventListener('mouseup', this.endSelection); + } + private async updateXY() { let start = 1332170682440133097; let end = 1332170682540133097; From 33ee55d4bb0216c34907c6b3e93e7869153b6c7e Mon Sep 17 00:00:00 2001 From: Arnaud Fiorini Date: Mon, 15 Feb 2021 08:10:00 -0500 Subject: [PATCH 025/373] XY Chart: Correct selecting display out of range When selecting time range out of range with times bigger than the trace range, the blue overlay was not correctly displayed, this change fixes it. Signed-off-by: Arnaud Fiorini --- .../src/components/xy-output-component.tsx | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/react-components/src/components/xy-output-component.tsx b/react-components/src/components/xy-output-component.tsx index acf3cad1..4d1f07a3 100644 --- a/react-components/src/components/xy-output-component.tsx +++ b/react-components/src/components/xy-output-component.tsx @@ -181,28 +181,41 @@ export class XYOutputComponent extends AbstractTreeOutputComponent this.props.viewRange.getEnd() || max < this.props.viewRange.getstart()) { + return; + } + const minValue = this.findNearestValue(min, ticks); + const minPixel = xScale.getPixelForValue(min, minValue); + const maxValue = this.findNearestValue(max, ticks); + let maxPixel = xScale.getPixelForValue(max, maxValue); + // In the case the selection is going out of bounds, the pixelValue needs to be in the displayed range. + if (maxPixel === 0) { + maxPixel = chart.chartArea.right; + } ctx.save(); ctx.lineWidth = 1; ctx.strokeStyle = '#259fd8'; - - ctx.beginPath(); - ctx.moveTo(pixelStart, 0); - ctx.lineTo(pixelStart, chart.chartArea.bottom); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(pixelEnd, 0); - ctx.lineTo(pixelEnd, chart.chartArea.bottom); - ctx.stroke(); - + // Selection borders + if (min > this.props.viewRange.getstart()) { + ctx.beginPath(); + ctx.moveTo(minPixel, 0); + ctx.lineTo(minPixel, chart.chartArea.bottom); + ctx.stroke(); + } + if (max < this.props.viewRange.getEnd()) { + ctx.beginPath(); + ctx.moveTo(maxPixel, 0); + ctx.lineTo(maxPixel, chart.chartArea.bottom); + ctx.stroke(); + } + // Selection fill ctx.globalAlpha = 0.2; ctx.fillStyle = '#259fd8'; - ctx.fillRect(pixelStart, 0, pixelEnd - pixelStart, chart.chartArea.bottom); + ctx.fillRect(minPixel, 0, maxPixel - minPixel, chart.chartArea.bottom); ctx.restore(); } From 4250bbcc36f7efdf011c622560ac70adaeab53ef Mon Sep 17 00:00:00 2001 From: Bernd Hufmann Date: Mon, 22 Feb 2021 11:18:34 -0500 Subject: [PATCH 026/373] Use plugin per chart instance instead of global for time selection Fixes #269 Signed-off-by: Bernd Hufmann --- .../src/components/xy-output-component.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/react-components/src/components/xy-output-component.tsx b/react-components/src/components/xy-output-component.tsx index 4d1f07a3..a463ae65 100644 --- a/react-components/src/components/xy-output-component.tsx +++ b/react-components/src/components/xy-output-component.tsx @@ -31,7 +31,9 @@ export class XYOutputComponent extends AbstractTreeOutputComponent { this.afterChartDraw(chartInstance); } + }; private updateSelection = (event: MouseEvent) => { if (this.mouseIsDown && this.props.unitController.selectionRange) { const xStartPos = this.props.unitController.selectionRange.start; @@ -63,11 +65,6 @@ export class XYOutputComponent extends AbstractTreeOutputComponent { - this.afterChartDraw(chart); - } - }); this.lineChartRef = React.createRef(); } @@ -169,7 +166,8 @@ export class XYOutputComponent extends AbstractTreeOutputComponent + ref={this.lineChartRef} + plugins={[this.plugin]}> : 'Analysis running...'} From 3ad26518131a10ffb42045cb691b2c53001a75af Mon Sep 17 00:00:00 2001 From: muddana-satish Date: Thu, 28 Jan 2021 16:02:53 +0530 Subject: [PATCH 027/373] Decouple delete experiments from trace-viewer On Click of 'x' in trace-viewer tab doesn't delete the experiment from trace explorer. Added a context menu to open, close and delete traces. fixes #149 Signed-off-by: muddana-satish --- base/src/experiment-manager.ts | 12 +++++++----- base/src/signal-manager.ts | 8 +++++++- base/src/trace-manager.ts | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/base/src/experiment-manager.ts b/base/src/experiment-manager.ts index 3642d0ca..057b51be 100644 --- a/base/src/experiment-manager.ts +++ b/base/src/experiment-manager.ts @@ -19,7 +19,7 @@ export class ExperimentManager { ) { this.fTspClient = tspClient; this.fTraceManager = traceManager; - signalManager().on(Signals.EXPERIMENT_CLOSED, ({experiment}) => this.onExperimentClosed(experiment)); + signalManager().on(Signals.EXPERIMENT_CLOSED, ({ experiment }) => this.onExperimentClosed(experiment)); } /** @@ -98,7 +98,7 @@ export class ExperimentManager { const experiment = experimentResponse.getModel(); if (experimentResponse.isOk() && experiment) { this.addExperiment(experiment); - signalManager().emit(Signals.EXPERIMENT_OPENED, {experiment: experiment}); + signalManager().emit(Signals.EXPERIMENT_OPENED, { experiment: experiment }); return experiment; } // TODO Handle any other experiment open errors @@ -134,7 +134,7 @@ export class ExperimentManager { await this.fTspClient.deleteExperiment(experimentUUID); const deletedExperiment = this.removeExperiment(experimentUUID); if (deletedExperiment) { - signalManager().emit(Signals.EXPERIMENT_CLOSED, {experiment: deletedExperiment}); + signalManager().emit(Signals.EXPERIMENT_CLOSED, { experiment: deletedExperiment }); } } } @@ -150,8 +150,11 @@ export class ExperimentManager { } } - private addExperiment(experiment: Experiment) { + public addExperiment(experiment: Experiment): void { this.fOpenExperiments.set(experiment.UUID, experiment); + experiment.traces.forEach(trace => { + this.fTraceManager.addTrace(trace); + }); } private removeExperiment(experimentUUID: string): Experiment | undefined { @@ -160,4 +163,3 @@ export class ExperimentManager { return deletedExperiment; } } - diff --git a/base/src/signal-manager.ts b/base/src/signal-manager.ts index fe378cc0..c9144271 100644 --- a/base/src/signal-manager.ts +++ b/base/src/signal-manager.ts @@ -5,6 +5,7 @@ export declare interface SignalManager { fireTooltipSignal(tooltip: { [key: string]: string }): void; fireThemeChangedSignal(theme: string): void; fireSelectionChangedSignal(payload: { [key: string]: string }): void; + fireCloseTraceViewerTabSignal(traceUUID: string): void; } @@ -16,7 +17,8 @@ export const Signals = { EXPERIMENT_SELECTED: 'experiment selected', TOOLTIP_UPDATED: 'tooltip updated', THEME_CHANGED: 'theme changed', - SELECTION_CHANGED: 'selection changed' + SELECTION_CHANGED: 'selection changed', + TRACEVIEWER_CLOSED: 'tab closed' }; export class SignalManager extends EventEmitter implements SignalManager { @@ -33,6 +35,10 @@ export class SignalManager extends EventEmitter implements SignalManager { this.emit(Signals.SELECTION_CHANGED, { payload }); } + fireCloseTraceViewerTabSignal(traceUUID: string): void { + this.emit(Signals.TRACEVIEWER_CLOSED, traceUUID); + } + } let instance: SignalManager = new SignalManager(); diff --git a/base/src/trace-manager.ts b/base/src/trace-manager.ts index 42b72fa2..7f6ac9a9 100644 --- a/base/src/trace-manager.ts +++ b/base/src/trace-manager.ts @@ -131,7 +131,7 @@ export class TraceManager { } } - private addTrace(trace: Trace) { + public addTrace(trace: Trace) { this.fOpenTraces.set(trace.UUID, trace); } From c104cd6b7d7ada6c66ba11e0b5e72f47704092f8 Mon Sep 17 00:00:00 2001 From: Matthew Khouzam Date: Fri, 26 Feb 2021 16:45:17 -0500 Subject: [PATCH 028/373] Propagate vertical alignment style. Signed-off-by: Matthew Khouzam --- .../src/components/timegraph-output-component.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index f379e2aa..cc8bf012 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/react-components/src/components/timegraph-output-component.tsx @@ -415,10 +415,15 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent Date: Mon, 1 Mar 2021 18:44:23 -0800 Subject: [PATCH 029/373] Enable text selection in events table Signed-off-by: muddana-satish --- react-components/src/components/table-output-component.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/react-components/src/components/table-output-component.tsx b/react-components/src/components/table-output-component.tsx index 2f55a7f4..2259194e 100644 --- a/react-components/src/components/table-output-component.tsx +++ b/react-components/src/components/table-output-component.tsx @@ -77,6 +77,7 @@ export class TableOutputComponent extends AbstractOutputComponent ; From 32f23758424a48fa49499c33ba3c81bdb60328bf Mon Sep 17 00:00:00 2001 From: Ankush Tyagi Date: Fri, 12 Mar 2021 07:21:02 +0100 Subject: [PATCH 030/373] Fixed event_table component's font-family/font-size fixes#216 -The ag-grid has default color for selected row which isn't consistent with theia's font family and font size. This change will override those default values. Signed-off-by: Ankush Tyagi --- react-components/style/output-components-style.css | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/react-components/style/output-components-style.css b/react-components/style/output-components-style.css index b6b89111..1d186907 100644 --- a/react-components/style/output-components-style.css +++ b/react-components/style/output-components-style.css @@ -94,4 +94,18 @@ canvas { padding: 3px; width: 180px; color: var(--theia-input-placeholder-foreground) +} + +.ag-theme-balham{ + font-family: var(--theia-ui-font-family) !important; + font-size: var(--theia-content-font-size) !important; +} + +.ag-theme-balham-dark{ + font-family: var(--theia-ui-font-family) !important; + font-size: var(--theia-content-font-size) !important; +} + +.ag-theme-balham-dark .ag-row-selected { + background-color: var(--theia-selection-background) !important; } \ No newline at end of file From 40d7462eae62a64ee3adae099ce06b6e48126e01 Mon Sep 17 00:00:00 2001 From: muddana-satish Date: Tue, 16 Mar 2021 15:58:33 -0500 Subject: [PATCH 031/373] Fix console warnings Signed-off-by: muddana-satish --- react-components/src/components/table-output-component.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/react-components/src/components/table-output-component.tsx b/react-components/src/components/table-output-component.tsx index 2259194e..627cf7c5 100644 --- a/react-components/src/components/table-output-component.tsx +++ b/react-components/src/components/table-output-component.tsx @@ -73,7 +73,6 @@ export class TableOutputComponent extends AbstractOutputComponent Date: Thu, 18 Mar 2021 14:44:07 -0400 Subject: [PATCH 032/373] Use trace range for time graph view boundary validation before a fetch fixes #304 Signed-off-by: Bernd Hufmann --- .../components/data-providers/tsp-data-provider.ts | 12 +++++++----- .../src/components/timegraph-output-component.tsx | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/react-components/src/components/data-providers/tsp-data-provider.ts b/react-components/src/components/data-providers/tsp-data-provider.ts index 5561fb5d..f07ed693 100644 --- a/react-components/src/components/data-providers/tsp-data-provider.ts +++ b/react-components/src/components/data-providers/tsp-data-provider.ts @@ -4,6 +4,7 @@ import { TimelineChart } from 'timeline-chart/lib/time-graph-model'; import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; import { OutputElementStyle } from 'tsp-typescript-client/lib/models/styles'; import { Annotation, Type } from 'tsp-typescript-client/lib/models/annotation'; +import { TimeRange } from '@trace-viewer/base/lib/utils/time-range'; export class TspDataProvider { @@ -27,7 +28,8 @@ export class TspDataProvider { this.totalRange = 0; } - async getData(ids: number[], entries: TimeGraphEntry[], viewRange?: TimelineChart.TimeGraphRange, resolution?: number): Promise { + async getData(ids: number[], entries: TimeGraphEntry[], totalTimeRange: TimeRange, + viewRange?: TimelineChart.TimeGraphRange, resolution?: number): Promise { this.timeGraphEntries = [...entries]; if (!this.timeGraphEntries.length) { return { @@ -39,11 +41,11 @@ export class TspDataProvider { }; } - this.totalRange = this.timeGraphEntries[0].end - this.timeGraphEntries[0].start; // 1332170682540133097 - starttime + this.totalRange = totalTimeRange.getEnd() - totalTimeRange.getstart(); let fetchParameters = QueryHelper.selectionTimeQuery(QueryHelper.splitRangeIntoEqualParts(1332170682440133097, 1332170682540133097, 1120), ids); if (viewRange && resolution) { - const start = viewRange.start + this.timeGraphEntries[0].start; - const end = viewRange.end + this.timeGraphEntries[0].start; + const start = totalTimeRange.getstart() + viewRange.start; + const end = totalTimeRange.getstart() + viewRange.end; fetchParameters = QueryHelper.selectionTimeQuery(QueryHelper.splitRangeIntoEqualParts(Math.trunc(start), Math.trunc(end), resolution), ids); } const tspClientResponse = await this.client.fetchTimeGraphStates(this.traceUUID, this.outputId, fetchParameters); @@ -56,7 +58,7 @@ export class TspDataProvider { } // the start time which is normalized to logical 0 in timeline chart. - const chartStart = this.timeGraphEntries[0].start; + const chartStart = totalTimeRange.getstart(); const rows: TimelineChart.TimeGraphRowModel[] = []; this.timeGraphRows.forEach((row: TimeGraphRow) => { const rowId: number = row.entryId; diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index cc8bf012..4ec6eea7 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/react-components/src/components/timegraph-output-component.tsx @@ -277,7 +277,8 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent Date: Tue, 2 Mar 2021 09:01:39 -0500 Subject: [PATCH 033/373] Add tooltip support for timegraph states Signed-off-by: Patrick Tasse --- react-components/package.json | 1 + .../components/abstract-output-component.tsx | 7 +- .../components/timegraph-output-component.tsx | 33 +++++ .../src/components/tooltip-component.tsx | 119 ++++++++++++++++++ .../components/trace-context-component.tsx | 12 +- react-components/tsconfig.json | 1 + 6 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 react-components/src/components/tooltip-component.tsx diff --git a/react-components/package.json b/react-components/package.json index ea465f0b..6456607a 100644 --- a/react-components/package.json +++ b/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/react-components/src/components/abstract-output-component.tsx b/react-components/src/components/abstract-output-component.tsx index 656486ad..425c2a07 100644 --- a/react-components/src/components/abstract-output-component.tsx +++ b/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/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index 4ec6eea7..03d3e1d7 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/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/react-components/src/components/tooltip-component.tsx b/react-components/src/components/tooltip-component.tsx new file mode 100644 index 00000000..65633c1a --- /dev/null +++ b/react-components/src/components/tooltip-component.tsx @@ -0,0 +1,119 @@ +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} + 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()} + /> +
; + } + + 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/react-components/src/components/trace-context-component.tsx b/react-components/src/components/trace-context-component.tsx index 8d1fae5a..d4452670 100644 --- a/react-components/src/components/trace-context-component.tsx +++ b/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 Date: Fri, 19 Mar 2021 01:04:30 -0500 Subject: [PATCH 034/373] Set Focus to the trace viewer tab on click of analysis Signed-off-by: muddana-satish --- base/src/signal-manager.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/base/src/signal-manager.ts b/base/src/signal-manager.ts index c9144271..a9001ada 100644 --- a/base/src/signal-manager.ts +++ b/base/src/signal-manager.ts @@ -1,4 +1,5 @@ import { EventEmitter } from 'events'; +import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; export declare interface SignalManager { @@ -6,6 +7,7 @@ export declare interface SignalManager { fireThemeChangedSignal(theme: string): void; fireSelectionChangedSignal(payload: { [key: string]: string }): void; fireCloseTraceViewerTabSignal(traceUUID: string): void; + fireExperimentSelectedSignal(experiment: Experiment | undefined): void; } @@ -39,6 +41,10 @@ export class SignalManager extends EventEmitter implements SignalManager { this.emit(Signals.TRACEVIEWER_CLOSED, traceUUID); } + fireExperimentSelectedSignal(experiment: Experiment | undefined): void { + this.emit(Signals.EXPERIMENT_SELECTED, experiment); + } + } let instance: SignalManager = new SignalManager(); From eb799a0ed9102464b4d154008a0783291c4e3511 Mon Sep 17 00:00:00 2001 From: Bernd Hufmann Date: Mon, 22 Mar 2021 14:23:17 -0400 Subject: [PATCH 035/373] Fix incorrect resizing of XY Charts Fixes #309 Signed-off-by: Bernd Hufmann --- react-components/src/components/xy-output-component.tsx | 2 +- react-components/style/output-components-style.css | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/react-components/src/components/xy-output-component.tsx b/react-components/src/components/xy-output-component.tsx index a463ae65..77d413fc 100644 --- a/react-components/src/components/xy-output-component.tsx +++ b/react-components/src/components/xy-output-component.tsx @@ -161,7 +161,7 @@ export class XYOutputComponent extends AbstractTreeOutputComponent {this.state.outputStatus === ResponseStatus.COMPLETED ? -
this.beginSelection(event)}> +
this.beginSelection(event)} style={{ height: this.props.style.height }} > Date: Tue, 9 Mar 2021 21:17:18 -0500 Subject: [PATCH 036/373] Rename Analyses to Views in trace explorer Fixes #292 Analyses are an abstract concept of computation, and more importantly, the dataproviders are shown, they have a one for one relation to views, but a many to one relation to analyses, so views is more accurate too. Signed-off-by: Matthew Khouzam --- react-components/src/components/trace-context-component.tsx | 2 +- react-components/style/trace-explorer.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/react-components/src/components/trace-context-component.tsx b/react-components/src/components/trace-context-component.tsx index d4452670..b0d1ab7f 100644 --- a/react-components/src/components/trace-context-component.tsx +++ b/react-components/src/components/trace-context-component.tsx @@ -278,7 +278,7 @@ export class TraceContextComponent extends React.Component {'Trace loaded successfully.'}
- {'To see available analyses, open the Trace Explorer view.'} + {'To see available views, open the Trace Explorer view.'}
; } diff --git a/react-components/style/trace-explorer.css b/react-components/style/trace-explorer.css index f0142285..516eb10c 100644 --- a/react-components/style/trace-explorer.css +++ b/react-components/style/trace-explorer.css @@ -2,7 +2,7 @@ --trace-extension-list-line-height: 16px; } -#trace-explorer-analysis-widget, +#trace-explorer-views-widget, #trace-explorer-opened-traces-widget { height: 100%; } @@ -13,7 +13,7 @@ } .trace-explorer-opened, -.trace-explorer-analysis, +.trace-explorer-views, .trace-explorer-tooltip { margin: 0 2px; } From 9262f016553e6d6818c24499ddc413f3ae041962 Mon Sep 17 00:00:00 2001 From: Matthew Khouzam Date: Mon, 22 Mar 2021 16:19:19 -0400 Subject: [PATCH 037/373] Change "no view" warning to direct users to trace viewer tab The name of the trace explorer has been change to viewer, update the warning message to reflect this. Signed-off-by: Matthew Khouzam --- react-components/src/components/trace-context-component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react-components/src/components/trace-context-component.tsx b/react-components/src/components/trace-context-component.tsx index b0d1ab7f..6a187c9c 100644 --- a/react-components/src/components/trace-context-component.tsx +++ b/react-components/src/components/trace-context-component.tsx @@ -278,7 +278,7 @@ export class TraceContextComponent extends React.Component {'Trace loaded successfully.'}
- {'To see available views, open the Trace Explorer view.'} + {'To see available views, open the Trace Viewer.'}
; } From d3ee37c7bc3e6bda703e4a5da35d6867f02eaccf Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Wed, 24 Mar 2021 17:14:34 -0400 Subject: [PATCH 038/373] Change trace-explorer-tooltip-widget title Change the title from 'Time Graph Tooltip' to 'Item Properties'. Use a default message when there is no tooltip information available. Allow TOOLTIP_UPDATED signal to carry undefined tooltip to clear. Signed-off-by: Patrick Tasse --- base/src/signal-manager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/src/signal-manager.ts b/base/src/signal-manager.ts index a9001ada..7bb8eded 100644 --- a/base/src/signal-manager.ts +++ b/base/src/signal-manager.ts @@ -3,7 +3,7 @@ import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; export declare interface SignalManager { - fireTooltipSignal(tooltip: { [key: string]: string }): void; + fireTooltipSignal(tooltip?: { [key: string]: string }): void; fireThemeChangedSignal(theme: string): void; fireSelectionChangedSignal(payload: { [key: string]: string }): void; fireCloseTraceViewerTabSignal(traceUUID: string): void; @@ -25,7 +25,7 @@ export const Signals = { export class SignalManager extends EventEmitter implements SignalManager { - fireTooltipSignal(tooltip: { [key: string]: string; }): void { + fireTooltipSignal(tooltip?: { [key: string]: string; }): void { this.emit(Signals.TOOLTIP_UPDATED, { tooltip }); } From 319fa0c5c3947173e66778b873da223dfde26a2e Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Wed, 24 Mar 2021 17:18:20 -0400 Subject: [PATCH 039/373] Consolidate time graph tooltip information Use the same method to fill tooltip data on hovering and selection. Use the middle time of the time graph state to fetch tooltip data. Signed-off-by: Patrick Tasse --- .../components/timegraph-output-component.tsx | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index 03d3e1d7..2c31b072 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/react-components/src/components/timegraph-output-component.tsx @@ -225,7 +225,8 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent Date: Mon, 8 Mar 2021 09:36:16 -0500 Subject: [PATCH 040/373] Use signals to decouple trace explorer widgets and signals clean-up - Remove direct dependencies between class - Streamline signal sending and receiving - Move signal manager in own package - Make sure removeListener actually removes listener This decoupling will make it easier to create standalone react components for trace-explorer-opened-traces-widget.tsx and trace-explorer-analysis-widget.tsx that can be re-used outside the Theia framework. Signed-off-by: Bernd Hufmann --- base/src/experiment-manager.ts | 14 ++- ...ilable-analyses-changed-signal-payload.tsx | 20 +++++ .../signals/output-added-signal-payload.tsx | 20 +++++ base/src/signals/signal-manager.ts | 87 +++++++++++++++++++ base/src/trace-manager.ts | 6 +- base/tsconfig.json | 3 +- .../src/components/table-output-component.tsx | 2 +- .../components/timegraph-output-component.tsx | 13 ++- .../components/trace-context-component.tsx | 10 ++- 9 files changed, 155 insertions(+), 20 deletions(-) create mode 100644 base/src/signals/available-analyses-changed-signal-payload.tsx create mode 100644 base/src/signals/output-added-signal-payload.tsx create mode 100644 base/src/signals/signal-manager.ts diff --git a/base/src/experiment-manager.ts b/base/src/experiment-manager.ts index 057b51be..d33ad88c 100644 --- a/base/src/experiment-manager.ts +++ b/base/src/experiment-manager.ts @@ -5,7 +5,7 @@ import { OutputDescriptor } from 'tsp-typescript-client/lib/models/output-descri import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; import { TraceManager } from './trace-manager'; import { TspClientResponse } from 'tsp-typescript-client/lib/protocol/tsp-client-response'; -import { signalManager, Signals } from './signal-manager'; +import { signalManager, Signals } from './signals/signal-manager'; export class ExperimentManager { @@ -19,7 +19,7 @@ export class ExperimentManager { ) { this.fTspClient = tspClient; this.fTraceManager = traceManager; - signalManager().on(Signals.EXPERIMENT_CLOSED, ({ experiment }) => this.onExperimentClosed(experiment)); + signalManager().on(Signals.EXPERIMENT_CLOSED, (experiment: Experiment) => this.onExperimentClosed(experiment)); } /** @@ -60,10 +60,8 @@ export class ExperimentManager { * @param experimentUUID experiment UUID */ async getAvailableOutputs(experimentUUID: string): Promise { - // Check if the experiment is opened - const experiment = this.fOpenExperiments.get(experimentUUID); - if (experiment) { - const outputsResponse = await this.fTspClient.experimentOutputs(experiment.UUID); + const outputsResponse = await this.fTspClient.experimentOutputs(experimentUUID); + if (outputsResponse && outputsResponse.getStatusCode() === 200) { return outputsResponse.getModel(); } return undefined; @@ -98,7 +96,7 @@ export class ExperimentManager { const experiment = experimentResponse.getModel(); if (experimentResponse.isOk() && experiment) { this.addExperiment(experiment); - signalManager().emit(Signals.EXPERIMENT_OPENED, { experiment: experiment }); + signalManager().fireExperimentOpenedSignal(experiment); return experiment; } // TODO Handle any other experiment open errors @@ -134,7 +132,7 @@ export class ExperimentManager { await this.fTspClient.deleteExperiment(experimentUUID); const deletedExperiment = this.removeExperiment(experimentUUID); if (deletedExperiment) { - signalManager().emit(Signals.EXPERIMENT_CLOSED, { experiment: deletedExperiment }); + signalManager().fireExperimentClosedSignal(deletedExperiment); } } } diff --git a/base/src/signals/available-analyses-changed-signal-payload.tsx b/base/src/signals/available-analyses-changed-signal-payload.tsx new file mode 100644 index 00000000..d5083a09 --- /dev/null +++ b/base/src/signals/available-analyses-changed-signal-payload.tsx @@ -0,0 +1,20 @@ +import { OutputDescriptor } from 'tsp-typescript-client/lib/models/output-descriptor'; +import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; + +export class AvailableAnalysesChangedSignalPayload { + private _availableOutputDescriptors: OutputDescriptor[]; + private _experiment: Experiment; + + constructor(availableOutputDescriptors: OutputDescriptor[], experiment: Experiment) { + this._availableOutputDescriptors = availableOutputDescriptors; + this._experiment = experiment; + } + + public getAvailableOutputDescriptors(): OutputDescriptor[] { + return this._availableOutputDescriptors; + } + + public getExperiment(): Experiment { + return this._experiment; + } +} diff --git a/base/src/signals/output-added-signal-payload.tsx b/base/src/signals/output-added-signal-payload.tsx new file mode 100644 index 00000000..86a8d24b --- /dev/null +++ b/base/src/signals/output-added-signal-payload.tsx @@ -0,0 +1,20 @@ +import { OutputDescriptor } from 'tsp-typescript-client/lib/models/output-descriptor'; +import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; + +export class OutputAddedSignalPayload { + private outputDescriptor: OutputDescriptor; + private experiment: Experiment; + + constructor(outputDescriptor: OutputDescriptor, trace: Experiment) { + this.outputDescriptor = outputDescriptor; + this.experiment = trace; + } + + public getOutputDescriptor(): OutputDescriptor { + return this.outputDescriptor; + } + + public getExperiment(): Experiment { + return this.experiment; + } +} diff --git a/base/src/signals/signal-manager.ts b/base/src/signals/signal-manager.ts new file mode 100644 index 00000000..e276869b --- /dev/null +++ b/base/src/signals/signal-manager.ts @@ -0,0 +1,87 @@ +import { EventEmitter } from 'events'; +import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; +import { Trace } from 'tsp-typescript-client/lib/models/trace'; +import { AvailableAnalysesChangedSignalPayload } from './available-analyses-changed-signal-payload'; +import { OutputAddedSignalPayload } from './output-added-signal-payload'; + +export declare interface SignalManager { + fireTraceOpenedSignal(trace: Trace): void; + fireTraceClosedSignal(trace: Trace): void; + fireExperimentExperimentSignal(experiment: Experiment): void; + fireExperimentClosedSignal(experiment: Experiment): void; + fireExperimentSelectedSignal(experiment: Experiment): void; + fireOpenedTracesChangedSignal(): void; + fireAvailableOutputsChangedSignal(payload: AvailableAnalysesChangedSignalPayload): void; + fireOutputAddedSignal(payload: OutputAddedSignalPayload): void; + fireTooltipSignal(tooltip?: { [key: string]: string }): void; + fireThemeChangedSignal(theme: string): void; + fireSelectionChangedSignal(payload: { [key: string]: string }): void; + fireCloseTraceViewerTabSignal(traceUUID: string): void; + fireTraceViewerTabActivatedSignal(experiment: Experiment): void; +} + +export const Signals = { + TRACE_OPENED: 'trace opened', + TRACE_CLOSED: 'trace closed', + EXPERIMENT_OPENED: 'experiment opened', + EXPERIMENT_CLOSED: 'experiment closed', + EXPERIMENT_SELECTED: 'experiment selected', + OPENED_TRACES_UPDATED: 'opened traces updated', + AVAILABLE_OUTPUTS_CHANGED: 'available outputs changed', + OUTPUT_ADDED: 'output added', + TOOLTIP_UPDATED: 'tooltip updated', + THEME_CHANGED: 'theme changed', + SELECTION_CHANGED: 'selection changed', + CLOSE_TRACEVIEWERTAB: 'tab closed', + TRACEVIEWERTAB_ACTIVATED: 'widget activated' +}; + +export class SignalManager extends EventEmitter implements SignalManager { + fireTraceOpenedSignal(trace: Trace): void { + this.emit(Signals.TRACE_OPENED, trace); + } + fireTraceClosedSignal(trace: Trace): void { + this.emit(Signals.TRACE_CLOSED, { trace }); + } + fireExperimentOpenedSignal(experiment: Experiment): void { + this.emit(Signals.EXPERIMENT_OPENED, experiment); + } + fireExperimentClosedSignal(experiment: Experiment): void { + this.emit(Signals.EXPERIMENT_CLOSED, experiment); + } + fireExperimentSelectedSignal(experiment: Experiment): void { + this.emit(Signals.EXPERIMENT_SELECTED, experiment); + } + fireOpenedTracesChangedSignal(): void { + this.emit(Signals.OPENED_TRACES_UPDATED); + } + fireAvailableOutputsChangedSignal(payload: AvailableAnalysesChangedSignalPayload): void { + this.emit(Signals.AVAILABLE_OUTPUTS_CHANGED, payload); + } + fireOutputAddedSignal(payload: OutputAddedSignalPayload): void { + this.emit(Signals.OUTPUT_ADDED, payload); + } + fireTooltipSignal(tooltip?: { [key: string]: string; }): void { + this.emit(Signals.TOOLTIP_UPDATED, tooltip ); + } + fireThemeChangedSignal(theme: string): void { + this.emit(Signals.THEME_CHANGED, theme); + } + fireSelectionChangedSignal(payload: { [key: string]: string; }): void { + this.emit(Signals.SELECTION_CHANGED, payload); + } + fireCloseTraceViewerTabSignal(traceUUID: string): void { + this.emit(Signals.CLOSE_TRACEVIEWERTAB, traceUUID); + } + fireTraceViewerTabActivatedSignal(experiment: Experiment): void { + this.emit(Signals.TRACEVIEWERTAB_ACTIVATED, experiment); + } +} + +let instance: SignalManager = new SignalManager(); + +export const setSignalManagerInstance = (sm: SignalManager): void => { + instance = sm; +}; + +export const signalManager = (): SignalManager => instance; diff --git a/base/src/trace-manager.ts b/base/src/trace-manager.ts index 7f6ac9a9..4a225638 100644 --- a/base/src/trace-manager.ts +++ b/base/src/trace-manager.ts @@ -4,7 +4,7 @@ import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; import { Query } from 'tsp-typescript-client/lib/models/query/query'; import { OutputDescriptor } from 'tsp-typescript-client/lib/models/output-descriptor'; import { TspClientResponse } from 'tsp-typescript-client/lib/protocol/tsp-client-response'; -import { signalManager, Signals } from './signal-manager'; +import { signalManager, Signals } from './signals/signal-manager'; export class TraceManager { @@ -88,7 +88,7 @@ export class TraceManager { const trace = traceResponse.getModel(); if (traceResponse.isOk() && trace) { this.addTrace(trace); - signalManager().emit(Signals.TRACE_OPENED, {trace: trace}); + signalManager().fireTraceOpenedSignal(trace); return trace; } // TODO Handle trace open errors @@ -125,7 +125,7 @@ export class TraceManager { if (deleteResponse.getStatusCode() !== 409) { const deletedTrace = this.removeTrace(traceUUID); if (deletedTrace) { - signalManager().emit(Signals.TRACE_CLOSED, {trace: deletedTrace}); + signalManager().fireTraceClosedSignal(deletedTrace); } } } diff --git a/base/tsconfig.json b/base/tsconfig.json index af635928..40233870 100644 --- a/base/tsconfig.json +++ b/base/tsconfig.json @@ -11,7 +11,8 @@ "rootDir": "src", "outDir": "lib", "declaration": true, - "skipLibCheck": true + "skipLibCheck": true, + "jsx": "react" }, "include": [ "src" diff --git a/react-components/src/components/table-output-component.tsx b/react-components/src/components/table-output-component.tsx index 627cf7c5..7fb2d70a 100644 --- a/react-components/src/components/table-output-component.tsx +++ b/react-components/src/components/table-output-component.tsx @@ -5,7 +5,7 @@ import { AgGridReact } from 'ag-grid-react'; import { ColDef, IDatasource, GridReadyEvent, CellClickedEvent, GridApi, ColumnApi } from 'ag-grid-community'; import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; import { cloneDeep } from 'lodash'; -import { signalManager } from '@trace-viewer/base/lib/signal-manager'; +import { signalManager } from '@trace-viewer/base/lib/signals/signal-manager'; import { TimelineChart } from 'timeline-chart/lib/time-graph-model'; type TableOuputState = AbstractOutputState & { diff --git a/react-components/src/components/timegraph-output-component.tsx b/react-components/src/components/timegraph-output-component.tsx index 2c31b072..18cd8775 100644 --- a/react-components/src/components/timegraph-output-component.tsx +++ b/react-components/src/components/timegraph-output-component.tsx @@ -10,7 +10,7 @@ import { TimeGraphRowController } from 'timeline-chart/lib/time-graph-row-contro import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; import { ResponseStatus } from 'tsp-typescript-client/lib/models/response/responses'; import { TimeGraphEntry } from 'tsp-typescript-client/lib/models/timegraph'; -import { signalManager, Signals } from '@trace-viewer/base/lib/signal-manager'; +import { signalManager, Signals } from '@trace-viewer/base/lib/signals/signal-manager'; import { AbstractOutputProps, AbstractOutputState } from './abstract-output-component'; import { AbstractTreeOutputComponent } from './abstract-tree-output-component'; import { StyleProvider } from './data-providers/style-provider'; @@ -48,6 +48,8 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent this.doHandleSelectionChangedSigna(payload); + constructor(props: TimegraphOutputProps) { super(props); this.state = { @@ -100,7 +102,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent this.onSelectionChanged(payload)); + signalManager().on(Signals.SELECTION_CHANGED, this.onSelectionChanged); } synchronizeTreeScroll(): void { @@ -116,6 +118,11 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent { const parameters = QueryHelper.timeQuery([this.props.range.getstart(), this.props.range.getEnd()]); const tspClientResponse = await this.props.tspClient.fetchTimeGraphTree(this.props.traceId, this.props.outputDescriptor.id, parameters); @@ -178,7 +185,7 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent; private traceContextContainer: React.RefObject; + private onBackgroundThemeUpdated = (theme: string): void => this.updateBackgroundTheme(theme); + protected widgetResizeHandlers: (() => void)[] = []; protected readonly addWidgetResizeHandler = (h: () => void): void => { this.widgetResizeHandlers.push(h); @@ -113,10 +115,10 @@ export class TraceContextComponent extends React.Component this.updateBackgroundTheme(theme)); + signalManager().on(Signals.THEME_CHANGED, this.onBackgroundThemeUpdated); } - public updateBackgroundTheme(theme: string): void { + private updateBackgroundTheme(theme: string): void { this.setState({ style: { width: this.DEFAULT_COMPONENT_WIDTH, @@ -178,7 +180,7 @@ export class TraceContextComponent extends React.Component this.updateBackgroundTheme(theme)); + signalManager().off(Signals.THEME_CHANGED, this.onBackgroundThemeUpdated); this.props.messageManager.removeStatusMessage(this.INDEXING_STATUS_BAR_KEY); this.props.messageManager.removeStatusMessage(this.TIME_SELECTION_STATUS_BAR_KEY); } From 462e8df3d738ca4d40af9e80106a657967abf7a2 Mon Sep 17 00:00:00 2001 From: Bernd Hufmann Date: Mon, 8 Mar 2021 11:31:43 -0500 Subject: [PATCH 041/373] Make trace explorer's opened traces widget a pure React component This new react component ReactOpenTracesWidget will be part of the packages/react-components. It can be used in other web applications independently from Theia. Signed-off-by: Bernd Hufmann --- .../opened-traces-updated-signal-payload.tsx | 11 + base/src/signals/signal-manager.ts | 7 +- base/src/tsp-client-provider.ts | 15 + .../trace-explorer-opened-traces-widget.tsx | 326 ++++++++++++++++++ 4 files changed, 356 insertions(+), 3 deletions(-) create mode 100644 base/src/signals/opened-traces-updated-signal-payload.tsx create mode 100644 base/src/tsp-client-provider.ts create mode 100644 react-components/src/trace-explorer/trace-explorer-opened-traces-widget.tsx diff --git a/base/src/signals/opened-traces-updated-signal-payload.tsx b/base/src/signals/opened-traces-updated-signal-payload.tsx new file mode 100644 index 00000000..a21a461c --- /dev/null +++ b/base/src/signals/opened-traces-updated-signal-payload.tsx @@ -0,0 +1,11 @@ +export class OpenedTracesUpdatedSignalPayload { + private _numberOfOpenedTraces: number; + + constructor(numberOfOpenedTraces: number) { + this._numberOfOpenedTraces = numberOfOpenedTraces; + } + + public getNumberOfOpenedTraces(): number { + return this._numberOfOpenedTraces; + } +} diff --git a/base/src/signals/signal-manager.ts b/base/src/signals/signal-manager.ts index e276869b..41935c47 100644 --- a/base/src/signals/signal-manager.ts +++ b/base/src/signals/signal-manager.ts @@ -2,6 +2,7 @@ import { EventEmitter } from 'events'; import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; import { Trace } from 'tsp-typescript-client/lib/models/trace'; import { AvailableAnalysesChangedSignalPayload } from './available-analyses-changed-signal-payload'; +import { OpenedTracesUpdatedSignalPayload } from './opened-traces-updated-signal-payload'; import { OutputAddedSignalPayload } from './output-added-signal-payload'; export declare interface SignalManager { @@ -10,7 +11,7 @@ export declare interface SignalManager { fireExperimentExperimentSignal(experiment: Experiment): void; fireExperimentClosedSignal(experiment: Experiment): void; fireExperimentSelectedSignal(experiment: Experiment): void; - fireOpenedTracesChangedSignal(): void; + fireOpenedTracesChangedSignal(payload: OpenedTracesUpdatedSignalPayload): void; fireAvailableOutputsChangedSignal(payload: AvailableAnalysesChangedSignalPayload): void; fireOutputAddedSignal(payload: OutputAddedSignalPayload): void; fireTooltipSignal(tooltip?: { [key: string]: string }): void; @@ -52,8 +53,8 @@ export class SignalManager extends EventEmitter implements SignalManager { fireExperimentSelectedSignal(experiment: Experiment): void { this.emit(Signals.EXPERIMENT_SELECTED, experiment); } - fireOpenedTracesChangedSignal(): void { - this.emit(Signals.OPENED_TRACES_UPDATED); + fireOpenedTracesChangedSignal(payload: OpenedTracesUpdatedSignalPayload): void { + this.emit(Signals.OPENED_TRACES_UPDATED, payload); } fireAvailableOutputsChangedSignal(payload: AvailableAnalysesChangedSignalPayload): void { this.emit(Signals.AVAILABLE_OUTPUTS_CHANGED, payload); diff --git a/base/src/tsp-client-provider.ts b/base/src/tsp-client-provider.ts new file mode 100644 index 00000000..f22f8b39 --- /dev/null +++ b/base/src/tsp-client-provider.ts @@ -0,0 +1,15 @@ +import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; +import { ExperimentManager } from './experiment-manager'; +import { TraceManager } from './trace-manager'; + +export interface ITspClientProvider { + getTspClient(): TspClient; + getTraceManager(): TraceManager; + getExperimentManager(): ExperimentManager; + /** + * Add a listener for trace server url changes + * @param listener The listener function to be called when the url is + * changed + */ + addTspClientChangeListener(listener: (tspClient: TspClient) => void): void; +} diff --git a/react-components/src/trace-explorer/trace-explorer-opened-traces-widget.tsx b/react-components/src/trace-explorer/trace-explorer-opened-traces-widget.tsx new file mode 100644 index 00000000..d10c66c1 --- /dev/null +++ b/react-components/src/trace-explorer/trace-explorer-opened-traces-widget.tsx @@ -0,0 +1,326 @@ +import * as React from 'react'; +import { List, ListRowProps, Index, AutoSizer } from 'react-virtualized'; +import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; +import { ExperimentManager } from '@trace-viewer/base/lib/experiment-manager'; +import { OutputDescriptor } from 'tsp-typescript-client/lib/models/output-descriptor'; +import { signalManager, Signals } from '@trace-viewer/base/lib/signals/signal-manager'; +import ReactModal from 'react-modal'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCopy } from '@fortawesome/free-solid-svg-icons'; +import { AvailableAnalysesChangedSignalPayload } from '@trace-viewer/base/lib/signals/available-analyses-changed-signal-payload'; +import { OpenedTracesUpdatedSignalPayload } from '@trace-viewer/base/lib/signals/opened-traces-updated-signal-payload'; +import { ITspClientProvider } from '@trace-viewer/base/lib/tsp-client-provider'; + +export interface ReactOpenTracesWidgetProps { + id: string, + title: string, + tspClientProvider: ITspClientProvider, + contextMenuRenderer?: (event: React.MouseEvent, experiment: Experiment) => void, + onDoubleClick?: (event: React.MouseEvent, experiment: Experiment) => void +} + +export interface ReactOpenTracesWidgetState { + openedExperiments: Array +} + +export class ReactOpenTracesWidget extends React.Component { + static LIST_MARGIN = 2; + static LINE_HEIGHT = 16; + + protected forceUpdateKey = false; + + sharingLink = ''; + showShareDialog = false; + lastSelectedOutputIndex = -1; + + private _selectedExperimentIndex = 0; + private _availableOutputDescriptors: Map = new Map(); + private _selectedExperiment: Experiment | undefined; + private _experimentManager: ExperimentManager; + + private _onExperimentOpened = (openedExperiment: Experiment): Promise => this.doHandleExperimentOpenedSignal(openedExperiment); + private _onExperimentClosed = (closedExperiment: Experiment): Promise => this.doHandleExperimentClosedSignal(closedExperiment); + private _onOpenedTracesWidgetActivated = (experiment: Experiment): void => this.doHandleTracesWidgetActivatedSignal(experiment); + + constructor(props: ReactOpenTracesWidgetProps) { + super(props); + signalManager().on(Signals.EXPERIMENT_OPENED, this._onExperimentOpened); + signalManager().on(Signals.EXPERIMENT_CLOSED, this._onExperimentClosed); + signalManager().on(Signals.TRACEVIEWERTAB_ACTIVATED, this._onOpenedTracesWidgetActivated); + + this._experimentManager = this.props.tspClientProvider.getExperimentManager(); + this.props.tspClientProvider.addTspClientChangeListener(() => { + this._experimentManager = this.props.tspClientProvider.getExperimentManager(); + }); + this.state = { openedExperiments: [] }; + } + + componentDidMount(): void { + this.initialize(); + } + + componentWillUnmount(): void { + signalManager().off(Signals.EXPERIMENT_OPENED, this._onExperimentOpened); + signalManager().off(Signals.EXPERIMENT_CLOSED, this._onExperimentClosed); + signalManager().off(Signals.TRACEVIEWERTAB_ACTIVATED, this._onOpenedTracesWidgetActivated); + } + + async initialize(): Promise { + await this.updateOpenedExperiments(); + await this.updateAvailableAnalysis(undefined); + } + + public async doHandleExperimentOpenedSignal(openedExperiment: Experiment): Promise { + await this.updateOpenedExperiments(); + await this.updateAvailableAnalysis(openedExperiment); + } + + public async doHandleExperimentClosedSignal(_closedExperiment: Experiment): Promise { + await this.updateOpenedExperiments(); + await this.updateAvailableAnalysis(undefined); + } + + protected doHandleTracesWidgetActivatedSignal(experiment: Experiment): void { + this._selectedExperiment = experiment; + const selectedIndex = this.state.openedExperiments.findIndex(openedExperiment => openedExperiment.UUID === experiment.UUID); + this.selectExperiment(selectedIndex); + } + + protected doHandleContextMenuEvent(event: React.MouseEvent, traceUUID: string): void { + this.doHandleOnExperimentSelected(event); + const experiment = this.getExperiment(traceUUID); + if (experiment !== undefined && this.props.contextMenuRenderer) { + this.props.contextMenuRenderer(event, experiment); + } + event.preventDefault(); + event.stopPropagation(); + } + + protected dohandleDoubleClickEvent(event: React.MouseEvent, traceUUID: string): void { + this.doHandleOnExperimentSelected(event); + const experiment = this.getExperiment(traceUUID); + if (experiment !== undefined && this.props.onDoubleClick) { + this.props.onDoubleClick(event, experiment); + } + event.preventDefault(); + event.stopPropagation(); + } + + public getExperiment(traceUUID: string): Experiment | undefined { + return this.state.openedExperiments.find(experiment => experiment.UUID === traceUUID); + } + + render(): React.ReactNode { + const totalHeight = this.getTotalHeight(); + this.forceUpdateKey = !this.forceUpdateKey; + const key = Number(this.forceUpdateKey); + return ( + <> + + {this.renderSharingModal()} + +
+
+ + {({ width }) => + } + +
+
+ + ); + } + + protected renderExperimentRow = (props: ListRowProps): React.ReactNode => this.doRenderExperimentRow(props); + + /* + TODO: Implement better visualization of experiment, e.g. a tree + with experiment name as root and traces (name and path) as children + */ + protected doRenderExperimentRow(props: ListRowProps): React.ReactNode { + const traceName = this.state.openedExperiments.length && props.index < this.state.openedExperiments.length + ? this.state.openedExperiments[props.index].name : ''; + const traceUUID = this.state.openedExperiments.length && props.index < this.state.openedExperiments.length + ? this.state.openedExperiments[props.index].UUID : ''; + let traceContainerClassName = 'trace-list-container'; + if (props.index === this._selectedExperimentIndex) { + traceContainerClassName = traceContainerClassName + ' theia-mod-selected'; + } + return
{ this.handleContextMenuEvent(event, traceUUID); }} + onDoubleClick={event => { this.handleDoubleClickEvent(event, traceUUID); }} + data-id={`${props.index}`}> +
+
+

{traceName}

+ {this.renderTracesForExperiment(props.index)} +
+ {/*
+ +
*/} +
+
; + } + + protected renderTracesForExperiment(index: number): React.ReactNode { + const tracePaths = this.state.openedExperiments[index].traces; + return ( +
+ {tracePaths.map(trace => ( +
+ {` > ${trace.name}`} +
+ ))} +
+ ); + } + + protected getRowHeight = (index: Index | number): number => this.doGetRowHeight(index); + + protected doGetRowHeight(index: Index | number): number { + const resolvedIndex = typeof index === 'object' ? index.index : index; + const experiment = this.state.openedExperiments[resolvedIndex]; + let totalHeight = 0; + if (experiment.name) { + totalHeight += ReactOpenTracesWidget.LINE_HEIGHT; + } + for (let i = 0; i < experiment.traces.length; i++) { + totalHeight += ReactOpenTracesWidget.LINE_HEIGHT; + } + return totalHeight; + } + + protected getTotalHeight(): number { + let totalHeight = 0; + for (let i = 0; i < this.state.openedExperiments.length; i++) { + totalHeight += this.getRowHeight(i); + } + return totalHeight; + } + + protected renderSharingModal(): React.ReactNode { + if (this.sharingLink.length) { + return
+
+ {'Copy URL to share your trace context'} +
+
+
+