diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..3ef14f3c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,26 @@ +--- +name: Bug Report (except security vulnerabilities) +about: Create a report to help us improve +--- + + + + + + +### Bug Description: + + +### Steps to Reproduce: + +1. +2. +3. + + + + +### Additional Information + +- Operating System: +- VSCode Version: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..9e9b06c8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,9 @@ +--- +name: Feature Request +about: Propose an idea for the project +--- + + + + +### Feature Description: diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..909c07e2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,9 @@ +--- +name: Question +about: Ask a question related to the content of this repository +--- + + + + +### Your question: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..843e7aad --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ + + +### What it does + + + +### How to test + + + +### Follow-ups + + + +### Review checklist + +- [ ] As an author, I have thoroughly tested my changes and carefully followed the instructions in this template diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 00000000..ccaa57a8 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,36 @@ +name: CI/CD + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build-test: + name: Build & run unit Tests (${{ matrix.os }}, node-${{ matrix.node-version }}) + timeout-minutes: 60 + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + node-version: [22] + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Install npm dependencies and build "traceviewer-base" + working-directory: base + run: yarn --frozen-lockfile + - name: Install npm dependencies and build "traceviewer-react-components" + working-directory: react-components + run: yarn --frozen-lockfile + - name: Run unit tests for "traceviewer-react-components" + working-directory: react-components + # admitedly a hack - these dependencies would be defined one level up, + # which does not exist here + run: | + yarn add react@18.3.1 react-dom@18.3.1 @types/react@18.3.8 lodash.debounce@4.0.8 @types/lodash.debounce@4.0.3 + yarn test diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..faa735b3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,93 @@ +# Community Code of Conduct + +**Version 2.0 +January 1, 2023** + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as community members, contributors, Committers[^1], and Project Leads (collectively "Contributors") pledge to make participation in our projects and our community a harassment-free and inclusive experience for everyone. + +This Community Code of Conduct ("Code") outlines our behavior expectations as members of our community in all Eclipse Foundation activities, both offline and online. It is not intended to govern scenarios or behaviors outside of the scope of Eclipse Foundation activities. Nor is it intended to replace or supersede the protections offered to all our community members under the law. Please follow both the spirit and letter of this Code and encourage other Contributors to follow these principles into our work. Failure to read or acknowledge this Code does not excuse a Contributor from compliance with the Code. + +## Our Standards + +Examples of behavior that contribute to creating a positive and professional environment include: + +- Using welcoming and inclusive language; +- Actively encouraging all voices; +- Helping others bring their perspectives and listening actively. If you find yourself dominating a discussion, it is especially important to encourage other voices to join in; +- Being respectful of differing viewpoints and experiences; +- Gracefully accepting constructive criticism; +- Focusing on what is best for the community; +- Showing empathy towards other community members; +- Being direct but professional; and +- Leading by example by holding yourself and others accountable + +Examples of unacceptable behavior by Contributors include: + +- The use of sexualized language or imagery; +- Unwelcome sexual attention or advances; +- Trolling, insulting/derogatory comments, and personal or political attacks; +- Public or private harassment, repeated harassment; +- Publishing others' private information, such as a physical or electronic address, without explicit permission; +- Violent threats or language directed against another person; +- Sexist, racist, or otherwise discriminatory jokes and language; +- Posting sexually explicit or violent material; +- Sharing private content, such as emails sent privately or non-publicly, or unlogged forums such as IRC channel history; +- Personal insults, especially those using racist or sexist terms; +- Excessive or unnecessary profanity; +- Advocating for, or encouraging, any of the above behavior; and +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +With the support of the Eclipse Foundation employees, consultants, officers, and directors (collectively, the "Staff"), Committers, and Project Leads, the Eclipse Foundation Conduct Committee (the "Conduct Committee") is responsible for clarifying the standards of acceptable behavior. The Conduct Committee takes appropriate and fair corrective action in response to any instances of unacceptable behavior. + +## Scope + +This Code applies within all Project, Working Group, and Interest Group spaces and communication channels of the Eclipse Foundation (collectively, "Eclipse spaces"), within any Eclipse-organized event or meeting, and in public spaces when an individual is representing an Eclipse Foundation Project, Working Group, Interest Group, or their communities. Examples of representing a Project or community include posting via an official social media account, personal accounts, or acting as an appointed representative at an online or offline event. Representation of Projects, Working Groups, and Interest Groups may be further defined and clarified by Committers, Project Leads, or the Staff. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the Conduct Committee via conduct@eclipse-foundation.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Without the explicit consent of the reporter, the Conduct Committee is obligated to maintain confidentiality with regard to the reporter of an incident. The Conduct Committee is further obligated to ensure that the respondent is provided with sufficient information about the complaint to reply. If such details cannot be provided while maintaining confidentiality, the Conduct Committee will take the respondent‘s inability to provide a defense into account in its deliberations and decisions. Further details of enforcement guidelines may be posted separately. + +Staff, Committers and Project Leads have the right to report, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code, or to block temporarily or permanently any Contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. Any such actions will be reported to the Conduct Committee for transparency and record keeping. + +Any Staff (including officers and directors of the Eclipse Foundation), Committers, Project Leads, or Conduct Committee members who are the subject of a complaint to the Conduct Committee will be recused from the process of resolving any such complaint. + +## Responsibility + +The responsibility for administering this Code rests with the Conduct Committee, with oversight by the Executive Director and the Board of Directors. For additional information on the Conduct Committee and its process, please write to . + +## Investigation of Potential Code Violations + +All conflict is not bad as a healthy debate may sometimes be necessary to push us to do our best. It is, however, unacceptable to be disrespectful or offensive, or violate this Code. If you see someone engaging in objectionable behavior violating this Code, we encourage you to address the behavior directly with those involved. If for some reason, you are unable to resolve the matter or feel uncomfortable doing so, or if the behavior is threatening or harassing, please report it following the procedure laid out below. + +Reports should be directed to . It is the Conduct Committee’s role to receive and address reported violations of this Code and to ensure a fair and speedy resolution. + +The Eclipse Foundation takes all reports of potential Code violations seriously and is committed to confidentiality and a full investigation of all allegations. The identity of the reporter will be omitted from the details of the report supplied to the accused. Contributors who are being investigated for a potential Code violation will have an opportunity to be heard prior to any final determination. Those found to have violated the Code can seek reconsideration of the violation and disciplinary action decisions. Every effort will be made to have all matters disposed of within 60 days of the receipt of the complaint. + +## Actions +Contributors who do not follow this Code in good faith may face temporary or permanent repercussions as determined by the Conduct Committee. + +This Code does not address all conduct. It works in conjunction with our [Communication Channel Guidelines](https://www.eclipse.org/org/documents/communication-channel-guidelines/), [Social Media Guidelines](https://www.eclipse.org/org/documents/social_media_guidelines.php), [Bylaws](https://www.eclipse.org/org/documents/eclipse-foundation-be-bylaws-en.pdf), and [Internal Rules](https://www.eclipse.org/org/documents/ef-be-internal-rules.pdf) which set out additional protections for, and obligations of, all contributors. The Foundation has additional policies that provide further guidance on other matters. + +It’s impossible to spell out every possible scenario that might be deemed a violation of this Code. Instead, we rely on one another’s good judgment to uphold a high standard of integrity within all Eclipse Spaces. Sometimes, identifying the right thing to do isn’t an easy call. In such a scenario, raise the issue as early as possible. + +## No Retaliation + +The Eclipse community relies upon and values the help of Contributors who identify potential problems that may need to be addressed within an Eclipse Space. Any retaliation against a Contributor who raises an issue honestly is a violation of this Code. That a Contributor has raised a concern honestly or participated in an investigation, cannot be the basis for any adverse action, including threats, harassment, or discrimination. If you work with someone who has raised a concern or provided information in an investigation, you should continue to treat the person with courtesy and respect. If you believe someone has retaliated against you, report the matter as described by this Code. Honest reporting does not mean that you have to be right when you raise a concern; you just have to believe that the information you are providing is accurate. + +False reporting, especially when intended to retaliate or exclude, is itself a violation of this Code and will not be accepted or tolerated. + +Everyone is encouraged to ask questions about this Code. Your feedback is welcome, and you will get a response within three business days. Write to . + +## Amendments + +The Eclipse Foundation Board of Directors may amend this Code from time to time and may vary the procedures it sets out where appropriate in a particular case. + +### Attribution + +This Code was inspired by the [Contributor Covenant](https://www.contributor-covenant.org/), version 1.4, available [here](https://www.contributor-covenant.org/version/1/4/code-of-conduct/). + +[^1]: Capitalized terms used herein without definition shall have the meanings assigned to them in the Bylaws. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..e815c115 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,8 @@ +# Contributing to traceviewer libraries + +Thanks for your interest! If you are currently looking at a repository named traceviewer-libs, this is probably not the best place to contribute to the contained libraries. The libraries in this repository were split (using git subtree split ...) from their original "home", the `eclipse-cdt-cloud/theia-trace-extension` repository. + +The goal was then to add this repo as a subtree in the product(s) that depend on the traceviewer libraries, so they can be developed, modified and tweaked locally, in-place. One such product/repo is `eclipse-cdt-cloud/vscode-trace-extension`, where you can find them under root folder `local-libs/traceviewer-libs`. If your ultimate goal is to contribute to the `Trace viewer for VSCode extension`, that's where you should make your contribution. + +Periodically, the project's maintainers will push local contributions to the shared subtree repository, making them available to other products that have adopted it. + diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..4e47a6b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 Ericsson and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 518cf840..6b6ca054 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,115 @@ -# traceviewer-libs -Trace viewer libraries: traceviewer-base and traceviewer-react-components +# Trace Viewer Libraries (git subtree) + +This repository contains the trace viewer libraries and minimal infrastructure to maintain them in source code form: + +- `traceviewer-base` and +- `traceviewer-react-components` + +The libraries are meant to provide generally useful building blocks for making a Trace Viewer application based on the Trace Server Protocol (TSP). They are used in at least two components: + +- The VSCode Trace Viewer extension ([repo](https://github.com/eclipse-cdt-cloud/vscode-trace-extension)) +- The Theia Trace Viewer extension ([repo](https://github.com/eclipse-cdt-cloud/theia-trace-extension)) + +It's possible to update the libraries using this repo here, but it might be preferable to update the local version, that's a copy of their source code, imported as a git subtree. In other words, this repo here is best used as for collaboration, where to share improvements to the libraries done in a local subtree, or to pull improvements done by someone else. + +## Why split the trace viewer libraries in their own repo? + +For historical reasons, the libraries were initially co-located with the Theia Trace Viewer and consumed in source form (built along with it) during development, making co-development easy. They were published to npm as needed, along with the extension. In the VSCode Trace Viewer extension, the libraries were consumed from npm at build time and bundled with the published extension (to the Microsoft Marketplace and Open VSX registry). This made it a lot more challenging to co-develop the VSCode Trace Viewer and the `traceviewer` libraries. + +We considered several ways to improve upon this situation, with the following goals: + +- Trace Viewer app/extension feature development testing and upstreaming: make it as easy as possible for all consumer of the libraries. Specially when the libraries need to be updated along with the consuming component. +- Collaboration on the libraries: make it as simple as possible, so that consumers can share improvements and new features they added +- Preserve git authorship contribution history even if the libraries are moved to a new repository + +The solution we picked is to use a `git subtree`. This involved splitting-out the trace viewer libraries from their original development location with `git subtree split`, without using the `--squash` option, so history is preserved. This repo here is the result of that operation, plus some minimal added infrastructure. + +In a separate step, this repo can be used in `vscode-trace-extension`, to "import" a local version of the libraries, as a git subtree. + +## How it was done + +The `traceviewer-*` libraries were split-out from `theia-trace-extension` repo like so: + +```bash +cd theia-trace-extension +git checkout master +# update local branch to latest master +git pull origin master +# clean all local files +git clean -ffdx + + +# split content of the "packages" folder (where the traceviewer +# libraries are). Put the resulting content on a branch called +# "traceviewer-libs-branch" +git subtree split --prefix=packages --branch traceviewer-libs-branch + +``` + +Note: by itself, the above does not remove the original libraries from the main branch of the `theia-trace-extension` repository. + +## Use the subtree in another repo + +### How to use the `traceviewer-libs` subtree in another repository + +This is an example on how to add the subtree to a repo, replacing that repo consuming the +libraries from npm. + +```bash +cd vscode-trace-extension +# add this repo here as subtree "remote", if not already done +git remote add traceviewer-libs git@github.com:eclipse-cdt-cloud/traceviewer-libs.git +# add traceviewer libearies as subtree under folder "local-libs/traceviewer-libs" +git subtree add --prefix=local-libs/traceviewer-libs traceviewer-libs master --squash + +``` + +In the root package.json, add the libraries in the "workspaces" array: +"workspaces": [ + [...] + "local-libs/traceviewer-libs/*" + [...] +} + +More changes/tweaks might be necessary. For example, tweaks to linter / formatter configurations, such that all repos sharing the libraries' code are in agreement how it should look-like. + + +### Push local changes made to the subtree towards the standalone subtree repo + +```bash +cd vscode-trace-extension +# add this repo here as subtree "remote", if not already done +git remote add traceviewer-libs git@github.com:eclipse-cdt-cloud/traceviewer-libs.git +# make sure you are on the master branch - i.e. no pushing un-reviewed changes! +# assumption: remote for official repo is named "origin" +git checkout master && git pull origin master +# push the latest local library changes to the git subtree upstream: +git subtree push -p +# more concrete example: +git subtree push -p local-libs/traceviewer-libs traceviewer-libs update-from-vscode-trace-extension + +# Then create a PR from the freshly pushed branch "update-from-vscode-trace-extension". +# If tweaks are necessary, e.g. to remove some changes that are only relevant to +# vscode-trace-extension, pull the review branch from your local fork of the standalone +# git subtree repo, make changes, and push an updated PR branch. + +``` + +### Pulling latest changes from the subtree repo into the local subtree + +```bash +# make sure your local master is up-to-date: +git checkout master && git pull origin master +git branch update-subtree && git checkout update-subtree +# add this repo here as subtree "remote", if not already done +git remote add traceviewer-libs git@github.com:eclipse-cdt-cloud/traceviewer-libs.git +# then pull the latest library upstream changes: +git subtree pull --prefix= master --squash +# more concrete example: +git subtree pull --prefix=local-libs/traceviewer-libs traceviewer-libs master --squash + +# push update branch and create a PR from it, have it reviewed and merged ASAP: +git push origin update-subtree + +``` + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..4320252e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,8 @@ +# Vulnerability Reporting Policy + +If you think or suspect that you have discovered a new security vulnerability in this project, please __do not__ disclose it on GitHub, e.g. in an issue, a PR, or a discussion. Any such disclosure will be removed/deleted on sight, to promote orderly disclosure, as per the [Eclipse Foundation Vulnerability Reporting Policy][policy]. + +Instead, please report any potential vulnerability to the Eclipse Foundation [Security Team][security]. Make sure to provide a concise description of the issue, a CWE, and other supporting information. + +[policy]: https://www.eclipse.org/security/policy +[security]: https://www.eclipse.org/security 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/LICENSE b/base/LICENSE new file mode 100644 index 00000000..74a365fe --- /dev/null +++ b/base/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019, 2021 Ericsson and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/base/README.md b/base/README.md new file mode 100644 index 00000000..bfb7c8b6 --- /dev/null +++ b/base/README.md @@ -0,0 +1,9 @@ +# Description + +The Trace Viewer base package contains trace management utilities for managing traces using Trace Server applications that implement the Trace Server Protocol (TSP). While being initially used within the Theia Trace Viewer extension, its code base is independent to any Theia APIs and hence can be integrated in other web applications. + +## Additional Information + +- [Theia Trace Viewer Extension git repository](https://github.com/eclipse-cdt-cloud/theia-trace-extension) +- [Trace Server Protocol git repository](https://github.com/eclipse-cdt-cloud/trace-server-protocol) +- [Reference Trace Server - Download (Eclipse Trace Compass)](https://download.eclipse.org/tracecompass.incubator/trace-server/rcp/) diff --git a/base/package.json b/base/package.json new file mode 100644 index 00000000..8d77d812 --- /dev/null +++ b/base/package.json @@ -0,0 +1,41 @@ +{ + "name": "traceviewer-base", + "version": "0.9.1", + "description": "Trace Viewer base package, contains trace management utilities", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-cdt-cloud/theia-trace-extension" + }, + "bugs": { + "url": "https://github.com/eclipse-cdt-cloud/theia-trace-extension/issues" + }, + "homepage": "https://github.com/eclipse-cdt-cloud/theia-trace-extension", + "files": [ + "lib", + "src" + ], + "dependencies": { + "tsp-typescript-client": "^0.7.0" + }, + "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": "^5.0.0", + "typescript": "4.9.5" + }, + "scripts": { + "build": "tsc -b", + "clean": "rimraf lib *.tsbuildinfo", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test": "echo 'test'", + "watch": "tsc -w", + "format:write": "prettier --write ./src", + "format:check": "prettier --check ./src" + } +} diff --git a/base/src/experiment-manager.ts b/base/src/experiment-manager.ts new file mode 100644 index 00000000..fcae417e --- /dev/null +++ b/base/src/experiment-manager.ts @@ -0,0 +1,160 @@ +import { Trace } from 'tsp-typescript-client/lib/models/trace'; +import { ITspClient } 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 } from './signals/signal-manager'; + +export class ExperimentManager { + private fOpenExperiments: Map = new Map(); + private fTspClient: ITspClient; + private fTraceManager: TraceManager; + + constructor(tspClient: ITspClient, traceManager: TraceManager) { + this.fTspClient = tspClient; + this.fTraceManager = traceManager; + signalManager().on('EXPERIMENT_DELETED', (experiment: Experiment) => this.onExperimentDeleted(experiment)); + } + + /** + * Get an array of opened experiments + * @returns Array of experiment + */ + async getOpenedExperiments(): Promise { + const openedExperiments: Array = []; + // Look on the server for opened experiments + const experimentsResponse = await this.fTspClient.fetchExperiments(); + const experiments = experimentsResponse.getModel(); + if (experimentsResponse.isOk() && experiments) { + openedExperiments.push(...experiments); + } + 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 { + const outputsResponse = await this.fTspClient.experimentOutputs(experimentUUID); + if (outputsResponse && outputsResponse.isOk()) { + 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 tryCreate = async function ( + tspClient: ITspClient, + 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++; + } + const experiment = experimentResponse.getModel(); + if (experimentResponse.isOk() && experiment) { + this.addExperiment(experiment); + signalManager().emit('EXPERIMENT_OPENED', experiment); + return experiment; + } + // TODO Handle any other experiment open errors + return undefined; + } + + /** + * Update the experiment with the latest info from the server. + * @param experimentUUID experiment UUID + * @returns The updated experiment or undefined if the experiment failed to update + */ + async updateExperiment(experimentUUID: string): Promise { + const experimentResponse = await this.fTspClient.fetchExperiment(experimentUUID); + const experiment = experimentResponse.getModel(); + if (experiment && experimentResponse.isOk()) { + this.fOpenExperiments.set(experimentUUID, experiment); + return experiment; + } + return undefined; + } + + /** + * Delete the given experiment from the server + * @param experimentUUID experiment UUID + */ + async deleteExperiment(experimentUUID: string): Promise { + const experimentToDelete = this.fOpenExperiments.get(experimentUUID); + if (experimentToDelete) { + await this.fTspClient.deleteExperiment(experimentUUID); + const deletedExperiment = this.removeExperiment(experimentUUID); + if (deletedExperiment) { + signalManager().emit('EXPERIMENT_DELETED', deletedExperiment); + } + } + } + + private onExperimentDeleted(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.deleteTrace(traces[i].UUID); + } + } + + 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 { + const deletedExperiment = this.fOpenExperiments.get(experimentUUID); + this.fOpenExperiments.delete(experimentUUID); + return deletedExperiment; + } +} diff --git a/base/src/lazy-tsp-client.ts b/base/src/lazy-tsp-client.ts new file mode 100644 index 00000000..3430a281 --- /dev/null +++ b/base/src/lazy-tsp-client.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { ITspClient } from 'tsp-typescript-client'; +import { HttpTspClient } from 'tsp-typescript-client/lib/protocol/http-tsp-client'; + +/** + * Hack! + * The `LazyTspClient` replaces _every_ method with an asynchronous one. + * Only keep methods, discard properties. + */ +export type LazyTspClient = { + [K in keyof ITspClient]: ITspClient[K] extends (...args: infer A) => infer R | Promise + ? (...args: A) => Promise + : never; // Discard property. +}; + +export type LazyTspClientFactory = typeof LazyTspClientFactory; +export function LazyTspClientFactory(provider: () => Promise): ITspClient { + // All methods from the `HttpTspClient` are asynchronous. The `LazyTspClient` + // will just delay each call to its methods by first awaiting for the + // asynchronous `baseUrl` resolution to then get a valid `HttpTspClient`. + + // Save the current HttpTspClient and the URL used for it. + let tspClient: HttpTspClient; + let lastUrl: string; + // eslint-disable-next-line no-null/no-null + return new Proxy(Object.create(null), { + get(target, property, _receiver) { + let method = target[property]; + if (!method) { + target[property] = method = async (...args: any[]) => { + tspClient = await provider().then(baseUrl => { + // If the url has not been updated keep the same client. + if (lastUrl === baseUrl) { + return tspClient; + } + // If the url has changed save it and create a new client. + lastUrl = baseUrl; + return new HttpTspClient(baseUrl); + }); + return (tspClient as any)[property](...args); + }; + } + return method; + } + }) as LazyTspClient as ITspClient; +} diff --git a/base/src/message-manager.ts b/base/src/message-manager.ts new file mode 100644 index 00000000..6398c566 --- /dev/null +++ b/base/src/message-manager.ts @@ -0,0 +1,36 @@ +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/signals/available-views-changed-signal-payload.tsx b/base/src/signals/available-views-changed-signal-payload.tsx new file mode 100644 index 00000000..ef3eea8b --- /dev/null +++ b/base/src/signals/available-views-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 AvailableViewsChangedSignalPayload { + 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/context-menu-contributed-signal-payload.tsx b/base/src/signals/context-menu-contributed-signal-payload.tsx new file mode 100644 index 00000000..e94bf8e2 --- /dev/null +++ b/base/src/signals/context-menu-contributed-signal-payload.tsx @@ -0,0 +1,41 @@ +/*************************************************************************************** + * Copyright (c) 2024 BlackBerry Limited and contributors. + * + * Licensed under the MIT license. See LICENSE file in the project root for details. + ***************************************************************************************/ +export interface MenuItem { + id: string; + label: string; + // Parent Menu that this item belongs to - undefined indicates root menu item + parentMenuId?: string; +} + +export interface SubMenu { + id: string; + label: string; + items: MenuItem[]; + submenu: SubMenu | undefined; +} + +export interface ContextMenuItems { + submenus: SubMenu[]; + items: MenuItem[]; +} + +export class ContextMenuContributedSignalPayload { + private outputDescriptorId: string; + private menuItems: ContextMenuItems; + + constructor(descriptorId: string, menuItems: ContextMenuItems) { + this.outputDescriptorId = descriptorId; + this.menuItems = menuItems; + } + + public getOutputDescriptorId(): string { + return this.outputDescriptorId; + } + + public getMenuItems(): ContextMenuItems { + return this.menuItems; + } +} diff --git a/base/src/signals/context-menu-item-clicked-signal-payload.tsx b/base/src/signals/context-menu-item-clicked-signal-payload.tsx new file mode 100644 index 00000000..6a680b6d --- /dev/null +++ b/base/src/signals/context-menu-item-clicked-signal-payload.tsx @@ -0,0 +1,35 @@ +/*************************************************************************************** + * Copyright (c) 2024 BlackBerry Limited and contributors. + * + * Licensed under the MIT license. See LICENSE file in the project root for details. + ***************************************************************************************/ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export class ContextMenuItemClickedSignalPayload { + private outputDescriptorId: string; + private itemId: string; + private parentMenuId: string | undefined; + private props: { [key: string]: any }; + + constructor(descriptorId: string, itemId: string, props: { [key: string]: any }, parentMenuId?: string) { + this.outputDescriptorId = descriptorId; + this.itemId = itemId; + this.props = props; + this.parentMenuId = parentMenuId; + } + + public getOutputDescriptorId(): string { + return this.outputDescriptorId; + } + + public getItemId(): string { + return this.itemId; + } + + public getProps(): { [key: string]: any } { + return this.props; + } + + public getParentMenuId(): string | undefined { + return this.parentMenuId; + } +} diff --git a/base/src/signals/item-properties-signal-payload.tsx b/base/src/signals/item-properties-signal-payload.tsx new file mode 100644 index 00000000..1ca07f4e --- /dev/null +++ b/base/src/signals/item-properties-signal-payload.tsx @@ -0,0 +1,29 @@ +/*************************************************************************************** + * Copyright (c) 2024 BlackBerry Limited and contributors. + * + * Licensed under the MIT license. See LICENSE file in the project root for details. + ***************************************************************************************/ + +export class ItemPropertiesSignalPayload { + private outputDescriptorId: string | undefined; + private experimentUUID: string | undefined; + private properties: { [key: string]: string }; + + constructor(props: { [key: string]: string }, expUUID?: string, descriptorId?: string) { + this.properties = props; + this.experimentUUID = expUUID; + this.outputDescriptorId = descriptorId; + } + + public getOutputDescriptorId(): string | undefined { + return this.outputDescriptorId; + } + + public getExperimentUUID(): string | undefined { + return this.experimentUUID; + } + + public getProperties(): { [key: string]: string } { + return this.properties; + } +} 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/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/row-selections-changed-signal-payload.tsx b/base/src/signals/row-selections-changed-signal-payload.tsx new file mode 100644 index 00000000..d15d437a --- /dev/null +++ b/base/src/signals/row-selections-changed-signal-payload.tsx @@ -0,0 +1,33 @@ +/*************************************************************************************** + * Copyright (c) 2024 BlackBerry Limited and contributors. + * + * Licensed under the MIT license. See LICENSE file in the project root for details. + ***************************************************************************************/ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export class RowSelectionsChangedSignalPayload { + private traceId: string; + private outputDescriptorId: string; + private rows: { id: number; parentId?: number; metadata?: { [key: string]: any } }[]; + + constructor( + traceId: string, + descriptorId: string, + rows: { id: number; parentId?: number; metadata?: { [key: string]: any } }[] + ) { + this.outputDescriptorId = descriptorId; + this.traceId = traceId; + this.rows = rows; + } + + public getOutputDescriptorId(): string { + return this.outputDescriptorId; + } + + public getTraceId(): string { + return this.traceId; + } + + public getRows(): { id: number; parentId?: number; metadata?: { [key: string]: any } }[] { + return this.rows; + } +} diff --git a/base/src/signals/signal-manager.ts b/base/src/signals/signal-manager.ts new file mode 100644 index 00000000..ba17157e --- /dev/null +++ b/base/src/signals/signal-manager.ts @@ -0,0 +1,144 @@ +import { EventEmitter } from 'events'; +import { OutputDescriptor } from 'tsp-typescript-client'; +import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; +import { Trace } from 'tsp-typescript-client/lib/models/trace'; +import { OpenedTracesUpdatedSignalPayload } from './opened-traces-updated-signal-payload'; +import { OutputAddedSignalPayload } from './output-added-signal-payload'; +import { TimeRangeUpdatePayload } from './time-range-data-signal-payloads'; +import { ContextMenuContributedSignalPayload } from './context-menu-contributed-signal-payload'; +import { ContextMenuItemClickedSignalPayload } from './context-menu-item-clicked-signal-payload'; +import { RowSelectionsChangedSignalPayload } from './row-selections-changed-signal-payload'; +import { ItemPropertiesSignalPayload } from './item-properties-signal-payload'; + +export interface Signals { + TRACE_OPENED: [trace: Trace]; + TRACE_DELETED: [payload: { trace: Trace }]; + EXPERIMENT_OPENED: [experiment: Experiment]; + EXPERIMENT_CLOSED: [experiment: Experiment]; + EXPERIMENT_DELETED: [experiment: Experiment]; + EXPERIMENT_SELECTED: [experiment: Experiment | undefined]; + EXPERIMENT_UPDATED: [experiment: Experiment]; + OPENED_TRACES_UPDATED: [payload: OpenedTracesUpdatedSignalPayload]; + AVAILABLE_OUTPUTS_CHANGED: void; + OUTPUT_ADDED: [payload: OutputAddedSignalPayload]; + ITEM_PROPERTIES_UPDATED: [payload: ItemPropertiesSignalPayload]; + THEME_CHANGED: [theme: string]; + SELECTION_CHANGED: [payload: { [key: string]: string }]; + ROW_SELECTIONS_CHANGED: [payload: RowSelectionsChangedSignalPayload]; + CLOSE_TRACEVIEWERTAB: [traceUUID: string]; + TRACEVIEWERTAB_ACTIVATED: [experiment: Experiment]; + UPDATE_ZOOM: [hasZoomedIn: boolean]; + RESET_ZOOM: void; + MARKER_CATEGORIES_FETCHED: void; + MARKERSETS_FETCHED: void; + MARKER_CATEGORY_CLOSED: [traceViewerId: string, markerCategory: string]; + TRACE_SERVER_STARTED: void; + UNDO: void; + REDO: void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + PIN_VIEW: [output: OutputDescriptor, extra?: any]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + UNPIN_VIEW: [output: OutputDescriptor, extra?: any]; + OPEN_OVERVIEW_OUTPUT: [traceId: string]; + OVERVIEW_OUTPUT_SELECTED: [traceId: string, outputDescriptor: OutputDescriptor]; + SAVE_AS_CSV: [traceId: string, data: string]; + VIEW_RANGE_UPDATED: [payload: TimeRangeUpdatePayload]; + SELECTION_RANGE_UPDATED: [payload: TimeRangeUpdatePayload]; + REQUEST_SELECTION_RANGE_CHANGE: [payload: TimeRangeUpdatePayload]; + OUTPUT_DATA_CHANGED: [descriptors: OutputDescriptor[]]; + CONTRIBUTE_CONTEXT_MENU: [payload: ContextMenuContributedSignalPayload]; + CONTEXT_MENU_ITEM_CLICKED: [payload: ContextMenuItemClickedSignalPayload]; +} + +export type SignalType = keyof Signals; +export type SignalArgs = T extends void ? [] : T; + +export class SignalManager extends EventEmitter { + /** + * Registers an event handler for a specific signal type. + * Provides type-safe event registration with correct payload types for each signal. + * + * @template K - The signal type (key of Signals interface) + * @param event - The event name to listen for + * @param listener - The callback function to execute when the event occurs + * Type of arguments is automatically inferred from Signals interface + * @returns The signal manager instance for chaining + * + * @example + * // Single argument event + * signalManager().on('THEME_CHANGED', (theme: string) => { + * console.log(`Theme changed to: ${theme}`); + * }); + * + * // Tuple argument event + * signalManager().on('PIN_VIEW', (output: OutputDescriptor, extra?: any) => { + * console.log(`Pinning view for output: ${output.id}`); + * }); + */ + on( + event: K, + listener: ( + ...args: SignalArgs extends [] ? [] : [...SignalArgs] + ) => void | Promise + ): this { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return super.on(event, listener as (...args: any[]) => void | Promise); + } + + /** + * Removes an event handler for a specific signal type. + * Ensures type safety by requiring the listener signature to match the signal type. + * + * @template K - The signal type (key of Signals interface) + * @param event - The event name to remove the listener from + * @param listener - The callback function to remove + * @returns The signal manager instance for chaining + * + * @example + * const themeHandler = (theme: string) => console.log(theme); + * signalManager().off('THEME_CHANGED', themeHandler); + */ + off( + event: K, + listener: ( + ...args: SignalArgs extends [] ? [] : [...SignalArgs] + ) => void | Promise + ): this { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return super.off(event, listener as (...args: any[]) => void | Promise); + } + + /** + * Emits a signal with type-safe arguments based on the signal type. + * Arguments are automatically validated against the Signals interface. + * + * @template K - The signal type (key of Signals interface) + * @param event - The event name to emit + * @param args - The arguments to pass to listeners, type checked against Signals interface + * @returns true if the event had listeners, false otherwise + * + * @example + * // Single argument emission + * signalManager().emit('THEME_CHANGED', 'dark'); + * + * // Tuple argument emission + * signalManager().emit('MARKER_CATEGORY_CLOSED', 'viewer1', 'category1'); + * + * // Void event emission + * signalManager().emit('RESET_ZOOM'); + */ + emit( + event: K, + ...args: SignalArgs extends [] ? [] : [...SignalArgs] + ): boolean { + return super.emit(event, ...args); + } +} + +let instance: SignalManager = new SignalManager(); + +export const setSignalManagerInstance = (sm: SignalManager): void => { + instance = sm; +}; + +export const signalManager = (): SignalManager => instance; diff --git a/base/src/signals/time-range-data-signal-payloads.tsx b/base/src/signals/time-range-data-signal-payloads.tsx new file mode 100644 index 00000000..fa331537 --- /dev/null +++ b/base/src/signals/time-range-data-signal-payloads.tsx @@ -0,0 +1,6 @@ +import { TimeRange } from '../utils/time-range'; + +export interface TimeRangeUpdatePayload { + experimentUUID: string; + timeRange?: TimeRange; +} diff --git a/base/src/trace-manager.ts b/base/src/trace-manager.ts new file mode 100644 index 00000000..a99eee43 --- /dev/null +++ b/base/src/trace-manager.ts @@ -0,0 +1,141 @@ +import { Trace } from 'tsp-typescript-client/lib/models/trace'; +import { ITspClient } 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 } from './signals/signal-manager'; + +export class TraceManager { + private fOpenTraces: Map = new Map(); + private fTspClient: ITspClient; + + constructor(tspClient: ITspClient) { + 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(); + const traces = tracesResponse.getModel(); + if (tracesResponse.isOk() && traces) { + openedTraces.push(...traces); + } + 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 name = traceName ? traceName : traceURI.replace(/\/$/, '').replace(/(.*\/)?/, ''); + + const tryOpen = async function (tspClient: ITspClient, 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++; + } + const trace = traceResponse.getModel(); + if (traceResponse.isOk() && trace) { + this.addTrace(trace); + signalManager().emit('TRACE_OPENED', trace); + 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; + } + + /** + * Delete the given trace on the server + * @param traceUUID Trace UUID + */ + async deleteTrace(traceUUID: string): Promise { + const traceToClose = this.fOpenTraces.get(traceUUID); + if (traceToClose) { + const deleteResponse = await this.fTspClient.deleteTrace(traceUUID); + if (deleteResponse.getStatusCode() !== 409) { + const deletedTrace = this.removeTrace(traceUUID); + if (deletedTrace) { + signalManager().emit('TRACE_DELETED', { trace: deletedTrace }); + } + } + } + } + + public addTrace(trace: Trace): void { + 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/tsp-client-provider.ts b/base/src/tsp-client-provider.ts new file mode 100644 index 00000000..5a7b93f5 --- /dev/null +++ b/base/src/tsp-client-provider.ts @@ -0,0 +1,15 @@ +import { ITspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; +import { ExperimentManager } from './experiment-manager'; +import { TraceManager } from './trace-manager'; + +export interface ITspClientProvider { + getTspClient(): ITspClient; + 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: ITspClient) => void): void; +} diff --git a/base/src/utils/convert-color-string-to-hex.ts b/base/src/utils/convert-color-string-to-hex.ts new file mode 100644 index 00000000..3cc46562 --- /dev/null +++ b/base/src/utils/convert-color-string-to-hex.ts @@ -0,0 +1,28 @@ +/** + * Converts a string representing a color into a number. Works with both RGB strings and hex number strings. Ignores alpha values. + * @param {string} rgb RGB or hex string for a color. + * @returns {number} Hex number of the input string. Ignores alpha value if present. + */ +export function convertColorStringToHexNumber(rgb: string): number { + let string = '0'; + rgb.trim(); + if (rgb[0] === '#') { + // We are working with hex string. + string = '0x' + rgb.slice(1); + } else if (rgb[0] === 'r') { + // Working with RGB String + const match = rgb.match(/\d+/g); + if (match) { + string = + '0x' + + match + .map(x => { + x = parseInt(x).toString(16); + return x.length === 1 ? '0' + x : x; + }) + .join(''); + string = string.slice(0, 8); + } + } + return Number(string); +} diff --git a/base/src/utils/time-range.ts b/base/src/utils/time-range.ts new file mode 100644 index 00000000..368c9ed9 --- /dev/null +++ b/base/src/utils/time-range.ts @@ -0,0 +1,93 @@ +export interface TimeRangeString { + start: string; + end: string; + offset?: string; +} + +export class TimeRange { + private start: bigint; + private end: bigint; + private offset: bigint | 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: bigint, end: bigint, offset?: bigint); + /** + * Constructor. + * @param timeRangeString string object returned by this.toString() + */ + constructor(timeRangeString: TimeRangeString); + /** + * Constructor. + * Default TimeRange with 0 for values + */ + constructor(); + constructor(a?: TimeRangeString | bigint, b?: bigint, c?: bigint) { + if (typeof a === 'bigint' && typeof b === 'bigint') { + this.start = a; + this.end = b; + this.offset = c; + } else if (typeof a === 'object') { + const timeRangeString: TimeRangeString = a; + const { start, end, offset } = timeRangeString; + this.start = BigInt(start); + this.end = BigInt(end); + this.offset = offset ? BigInt(offset) : undefined; + } else { + this.start = BigInt(0); + this.end = BigInt(0); + this.offset = undefined; + } + } + + /** + * Get the range start time. + * If an offset is present the return value is start + offset. + */ + public getStart(): bigint { + 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(): bigint { + if (this.offset !== undefined) { + return this.end + this.offset; + } + return this.end; + } + + /** + * Get range duration + */ + public getDuration(): bigint { + return this.end - this.start; + } + + /** + * Return the time offset + */ + public getOffset(): bigint | undefined { + return this.offset; + } + + /** + * Create a string object that can be JSON.stringified + */ + public toString(): TimeRangeString { + return { + start: this.start.toString(), + end: this.end.toString(), + offset: this.offset?.toString() + }; + } +} diff --git a/base/src/utils/value-hash.ts b/base/src/utils/value-hash.ts new file mode 100644 index 00000000..b4c28cc1 --- /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..1e65f61d --- /dev/null +++ b/base/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "composite": true, + "strict": true, + "sourceMap": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "module": "commonjs", + "target": "ES2017", + "downlevelIteration": true, + "rootDir": "src", + "outDir": "lib", + "declaration": true, + "skipLibCheck": true, + "jsx": "react" + }, + "include": [ + "src" + ], + "references": [] +} diff --git a/react-components/.eslintrc.js b/react-components/.eslintrc.js new file mode 100644 index 00000000..9605d889 --- /dev/null +++ b/react-components/.eslintrc.js @@ -0,0 +1,58 @@ +/** @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__', + '**/*/__mocks__', + 'jestSetup.ts', + 'jest-shim.ts' + ], + parserOptions: { + tsconfigRootDir: __dirname, + project: 'tsconfig.json', + projectFolderIgnoreList: [ + '/lib/' + ] + }, + overrides: [ + { + // Apply rule override only to files with the following extensions + files: ['*.tsx', '*.jsx'], + rules: { + '@typescript-eslint/ban-types': [ + 'error', + { + extendDefaults: true, + types: { + '{}': false, + }, + }, + ], + }, + }, + ] +}; diff --git a/react-components/LICENSE b/react-components/LICENSE new file mode 100644 index 00000000..74a365fe --- /dev/null +++ b/react-components/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019, 2021 Ericsson and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/react-components/README.md b/react-components/README.md new file mode 100644 index 00000000..b8f6daf9 --- /dev/null +++ b/react-components/README.md @@ -0,0 +1,15 @@ +# Description + +The Trace Viewer react-components package contains views and utilities for visualizing traces and logs via the Trace Server Protocol, connected a Trace Server application. While being initially used within the Theia Trace Viewer extension, its code base is independent to any Theia APIs and hence can be integrated in other web applications. + +## Styling + +The Trace Viewer react-components package does not define CSS styles for its components, but it provides CSS variables that can be map to custom CSS styles or variables. Any project that uses the package should define its own CSS styles for the components. All the mappable variables have the prefix `--trace-viewer`. + +An example (of integration with Theia) that contains all the mappable variables can be found in [here](https://github.com/eclipse-cdt-cloud/theia-trace-extension/blob/master/theia-extensions/viewer-prototype/style/trace-viewer.css). + +## Additional Information + +- [Theia Trace Viewer Extension git repository](https://github.com/eclipse-cdt-cloud/theia-trace-extension) +- [Trace Server Protocol git repository](https://github.com/eclipse-cdt-cloud/trace-server-protocol) +- [Reference Trace Server - Download (Eclipse Trace Compass)](https://download.eclipse.org/tracecompass.incubator/trace-server/rcp/) diff --git a/react-components/jest-shim.ts b/react-components/jest-shim.ts new file mode 100644 index 00000000..5ea18080 --- /dev/null +++ b/react-components/jest-shim.ts @@ -0,0 +1,6 @@ +import { TextEncoder } from 'util'; +/* + * Add TextEncoder from NodeJS util as it is + * no longer available for Jest tests after React 18. + */ +global.TextEncoder = TextEncoder; diff --git a/react-components/jest.config.json b/react-components/jest.config.json new file mode 100644 index 00000000..16e70c10 --- /dev/null +++ b/react-components/jest.config.json @@ -0,0 +1,26 @@ +{ + "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" + ], + "setupFiles": [ + "./jest-shim.ts" + ], + "setupFilesAfterEnv": [ + "jest-canvas-mock" + ] +} diff --git a/react-components/package.json b/react-components/package.json new file mode 100644 index 00000000..2a6405c8 --- /dev/null +++ b/react-components/package.json @@ -0,0 +1,75 @@ +{ + "name": "traceviewer-react-components", + "version": "0.9.1", + "description": "Trace Compass react components", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-cdt-cloud/theia-trace-extension" + }, + "bugs": { + "url": "https://github.com/eclipse-cdt-cloud/theia-trace-extension/issues" + }, + "homepage": "https://github.com/eclipse-cdt-cloud/theia-trace-extension", + "files": [ + "lib", + "src", + "style" + ], + "dependencies": { + "@ag-grid-community/core": "^32.0.1", + "@ag-grid-community/infinite-row-model": "^32.0.0", + "@ag-grid-community/react": "^32.0.0", + "@ag-grid-community/styles": "^32.0.0", + "@fortawesome/fontawesome-svg-core": "^1.2.17 <1.3.0", + "@fortawesome/free-solid-svg-icons": "^5.8.1", + "@fortawesome/react-fontawesome": "^0.2.2", + "@mui/material": "^5.10.14", + "@vscode/codicons": "^0.0.29", + "chart.js": "^2.8.0", + "d3": "^7.1.1", + "lodash": "^4.17.15", + "react-chartjs-2": "^2.7.6", + "react-contexify": "^5.0.0", + "react-grid-layout": "1.2.0", + "react-modal": "^3.8.1", + "react-virtualized": "^9.21.0", + "timeline-chart": "^0.4.2", + "traceviewer-base": "0.9.1", + "tsp-typescript-client": "^0.7.0" + }, + "devDependencies": { + "@testing-library/react": "^15.0.6", + "@types/chart.js": "^2.7.52", + "@types/d3": "^7.1.0", + "@types/jest": "^28.0.0", + "@types/lodash": "^4.14.142", + "@types/react-grid-layout": "^1.1.1", + "@types/react-modal": "^3.8.2", + "@types/react-test-renderer": "^18.3.0", + "@types/react-virtualized": "^9.21.1", + "@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", + "jest": "^28.1.3", + "jest-canvas-mock": "^2.4.0", + "jest-environment-jsdom": "^28.1.3", + "react-test-renderer": "^18.2.0", + "rimraf": "^5.0.0", + "ts-jest": "^28.0.8", + "typescript": "4.9.5" + }, + "scripts": { + "build": "tsc -b", + "clean": "rimraf lib *.tsbuildinfo", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test": "jest --config jest.config.json", + "watch": "tsc -b -w", + "format:write": "prettier --write ./src", + "format:check": "prettier --check ./src" + } +} diff --git a/react-components/src/components/__tests__/__snapshots__/table-renderer-components.test.tsx.snap b/react-components/src/components/__tests__/__snapshots__/table-renderer-components.test.tsx.snap new file mode 100644 index 00000000..6823af62 --- /dev/null +++ b/react-components/src/components/__tests__/__snapshots__/table-renderer-components.test.tsx.snap @@ -0,0 +1,704 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Cell renderer 1`] = `"test cell"`; + +exports[` Cell renderer with search selection 1`] = ` +Array [ + + + test + + , + + cell + , +] +`; + +exports[` Empty search filter renderer 1`] = ` +
+ +
+`; + +exports[` Loading renderer in loading 1`] = ` + +`; + +exports[` Loading renderer not loading 1`] = `"test cell"`; + +exports[` Renders AG-Grid table with provided props & state 1`] = ` +
+
+
+ +
+ +
+
+ + + ... + + +
+
+
+
+
+
+
+
+
+
+
+ +