From f9bb675dcce39bf69cdbfa0239ca7622f1ea2f31 Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Wed, 21 Dec 2022 07:54:08 -0800 Subject: [PATCH 01/16] VS Code project init commit and UI --- .eslintrc.cjs | 20 + .gitignore | 14 +- .vscode/launch.json | 21 + .vscode/tasks.json | 13 + CHANGELOG.md | 4 + package-lock.json | 2689 +++++++++++++++++++++++++++++ package.json | 87 + resources/images/pyrsia_small.svg | 27 + src/extension.ts | 34 + src/model/NodeConfig.ts | 28 + src/nodeProvider.ts | 36 + src/test/runTest.ts | 23 + src/test/suite/extension.test.ts | 12 + src/test/suite/index.ts | 38 + src/utilities/client.ts | 69 + src/utilities/util.ts | 11 + src/webview-ui/main.js | 36 + src/webview-ui/styles.css | 124 ++ src/webviews/NodeConfigView.ts | 339 ++++ src/webviews/NodeViewProvider.ts | 150 ++ tsconfig.json | 18 + 21 files changed, 3783 insertions(+), 10 deletions(-) create mode 100644 .eslintrc.cjs create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 CHANGELOG.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 resources/images/pyrsia_small.svg create mode 100644 src/extension.ts create mode 100644 src/model/NodeConfig.ts create mode 100644 src/nodeProvider.ts create mode 100644 src/test/runTest.ts create mode 100644 src/test/suite/extension.test.ts create mode 100644 src/test/suite/index.ts create mode 100644 src/utilities/client.ts create mode 100644 src/utilities/util.ts create mode 100644 src/webview-ui/main.js create mode 100644 src/webview-ui/styles.css create mode 100644 src/webviews/NodeConfigView.ts create mode 100644 src/webviews/NodeViewProvider.ts create mode 100644 tsconfig.json diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..15f7a7b --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,20 @@ +module.exports = { + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + root: true, + ignorePatterns: [ + "out", + "dist", + "**/*.d.ts", + ".eslintrc.cjs" + ], + rules: { + "@typescript-eslint/naming-convention": "warn", + "@typescript-eslint/semi": "warn", + "curly": "warn", + "eqeqeq": "warn", + "no-throw-literal": "warn", + "semi": "off" + }, + }; \ No newline at end of file diff --git a/.gitignore b/.gitignore index 088ba6b..cc4ec21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,4 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk +node_modules +.idea +out +.vscode-test diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e7af81c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // TODO + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "name": "Launch Extension", + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "", + "request": "launch", + "type": "extensionHost" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..13b7744 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,13 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "group": "build", + "problemMatcher": [], + "label": "npm: watch", + "detail": "tsc -watch -p ./" + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5bc00d7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log +## [Unreleased] + +- Initial release \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4fcaf42 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2689 @@ +{ + "name": "pyrsia-integration", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pyrsia-integration", + "version": "0.0.1", + "dependencies": { + "@vscode/webview-ui-toolkit": "^1.2.0", + "axios": "^1.2.1" + }, + "devDependencies": { + "@types/glob": "^8.0.0", + "@types/mocha": "^10.0.1", + "@types/node": "16.x", + "@types/vscode": "^1.73.1", + "@typescript-eslint/eslint-plugin": "^5.47.0", + "@typescript-eslint/parser": "^5.47.0", + "@vscode/test-electron": "^2.2.0", + "eslint": "^8.30.0", + "glob": "^8.0.3", + "mocha": "^10.1.0", + "typescript": "^4.9.4" + }, + "engines": { + "vscode": "^1.74.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.0.tgz", + "integrity": "sha512-7yfvXy6MWLgWSFsLhz5yH3iQ52St8cdUY6FoGieKkRDVxuxmrNuUetIuu6cmjNWwniUHiWXjxCr5tTXDrbYS5A==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@microsoft/fast-element": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.11.0.tgz", + "integrity": "sha512-VKJYMkS5zgzHHb66sY7AFpYv6IfFhXrjQcAyNgi2ivD65My1XOhtjfKez5ELcLFRJfgZNAxvI8kE69apXERTkw==" + }, + "node_modules/@microsoft/fast-foundation": { + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/@microsoft/fast-foundation/-/fast-foundation-2.47.0.tgz", + "integrity": "sha512-EyFuioaZQ9ngjUNRQi8R3dIPPsaNQdUOS+tP0G7b1MJRhXmQWIitBM6IeveQA6ZvXG6H21dqgrfEWlsYrUZ2sw==", + "dependencies": { + "@microsoft/fast-element": "^1.11.0", + "@microsoft/fast-web-utilities": "^5.4.1", + "tabbable": "^5.2.0", + "tslib": "^1.13.0" + } + }, + "node_modules/@microsoft/fast-react-wrapper": { + "version": "0.1.48", + "resolved": "https://registry.npmjs.org/@microsoft/fast-react-wrapper/-/fast-react-wrapper-0.1.48.tgz", + "integrity": "sha512-9NvEjru9Kn5ZKjomAMX6v+eF0DR+eDkxKDwDfi+Wb73kTbrNzcnmlwd4diN15ygH97kldgj2+lpvI4CKLQQWLg==", + "dependencies": { + "@microsoft/fast-element": "^1.9.0", + "@microsoft/fast-foundation": "^2.41.1" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/@microsoft/fast-web-utilities": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/fast-web-utilities/-/fast-web-utilities-5.4.1.tgz", + "integrity": "sha512-ReWYncndjV3c8D8iq9tp7NcFNc1vbVHvcBFPME2nNFKNbS1XCesYZGlIlf3ot5EmuOXPlrzUHOWzQ2vFpIkqDg==", + "dependencies": { + "exenv-es6": "^1.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/glob": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.0.0.tgz", + "integrity": "sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", + "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.18.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.10.tgz", + "integrity": "sha512-XU1+v7h81p7145ddPfjv7jtWvkSilpcnON3mQ+bDi9Yuf7OI56efOglXRyXWgQ57xH3fEQgh7WOJMncRHVew5w==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "dev": true + }, + "node_modules/@types/vscode": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.74.0.tgz", + "integrity": "sha512-LyeCIU3jb9d38w0MXFwta9r0Jx23ugujkAxdwLTNCyspdZTKUc43t7ppPbCiPoQ/Ivd/pnDFZrb4hWd45wrsgA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.47.0.tgz", + "integrity": "sha512-AHZtlXAMGkDmyLuLZsRpH3p4G/1iARIwc/T0vIem2YB+xW6pZaXYXzCBnZSF/5fdM97R9QqZWZ+h3iW10XgevQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.47.0", + "@typescript-eslint/type-utils": "5.47.0", + "@typescript-eslint/utils": "5.47.0", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.47.0.tgz", + "integrity": "sha512-udPU4ckK+R1JWCGdQC4Qa27NtBg7w020ffHqGyAK8pAgOVuNw7YaKXGChk+udh+iiGIJf6/E/0xhVXyPAbsczw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.47.0", + "@typescript-eslint/types": "5.47.0", + "@typescript-eslint/typescript-estree": "5.47.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.47.0.tgz", + "integrity": "sha512-dvJab4bFf7JVvjPuh3sfBUWsiD73aiftKBpWSfi3sUkysDQ4W8x+ZcFpNp7Kgv0weldhpmMOZBjx1wKN8uWvAw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.47.0", + "@typescript-eslint/visitor-keys": "5.47.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.47.0.tgz", + "integrity": "sha512-1J+DFFrYoDUXQE1b7QjrNGARZE6uVhBqIvdaXTe5IN+NmEyD68qXR1qX1g2u4voA+nCaelQyG8w30SAOihhEYg==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.47.0", + "@typescript-eslint/utils": "5.47.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.47.0.tgz", + "integrity": "sha512-eslFG0Qy8wpGzDdYKu58CEr3WLkjwC5Usa6XbuV89ce/yN5RITLe1O8e+WFEuxnfftHiJImkkOBADj58ahRxSg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.47.0.tgz", + "integrity": "sha512-LxfKCG4bsRGq60Sqqu+34QT5qT2TEAHvSCCJ321uBWywgE2dS0LKcu5u+3sMGo+Vy9UmLOhdTw5JHzePV/1y4Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.47.0", + "@typescript-eslint/visitor-keys": "5.47.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.47.0.tgz", + "integrity": "sha512-U9xcc0N7xINrCdGVPwABjbAKqx4GK67xuMV87toI+HUqgXj26m6RBp9UshEXcTrgCkdGYFzgKLt8kxu49RilDw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.47.0", + "@typescript-eslint/types": "5.47.0", + "@typescript-eslint/typescript-estree": "5.47.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.47.0.tgz", + "integrity": "sha512-ByPi5iMa6QqDXe/GmT/hR6MZtVPi0SqMQPDx15FczCBXJo/7M8T88xReOALAfpBLm+zxpPfmhuEvPb577JRAEg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.47.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vscode/test-electron": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.2.1.tgz", + "integrity": "sha512-DUdwSYVc9p/PbGveaq20dbAAXHfvdq4zQ24ILp6PKizOBxrOfMsOq8Vts5nMzeIo0CxtA/RxZLFyDv001PiUSg==", + "dev": true, + "dependencies": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "rimraf": "^3.0.2", + "unzipper": "^0.10.11" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vscode/webview-ui-toolkit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-1.2.0.tgz", + "integrity": "sha512-3ai3B2iFK0myqSnEgK9JZd1nKJIR1zgOlQbwSqHS9Y15cyO0diyjsKzcDFCnuBI1UG4vmekWp+xWSTbrh36kLw==", + "dependencies": { + "@microsoft/fast-element": "^1.6.2", + "@microsoft/fast-foundation": "^2.38.0", + "@microsoft/fast-react-wrapper": "^0.1.18" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", + "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dev": true, + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.30.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.30.0.tgz", + "integrity": "sha512-MGADB39QqYuzEGov+F/qb18r4i7DohCDOfatHaxI2iGlPuC65bwG2gxgO+7DkyL38dRFaRH7RaRAgU6JKL9rMQ==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.4.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exenv-es6": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exenv-es6/-/exenv-es6-1.1.1.tgz", + "integrity": "sha512-vlVu3N8d6yEMpMsEm+7sUBAI81aqYYuEvfK0jNqmdb/OPXzzH7QWDDnVjMvDSY47JdHEqx/dfC/q8WkfoTmpGQ==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", + "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globals": { + "version": "13.19.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", + "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-sdsl": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", + "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tabbable": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", + "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unzipper": { + "version": "0.10.11", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", + "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..38a0b22 --- /dev/null +++ b/package.json @@ -0,0 +1,87 @@ +{ + "name": "pyrsia-integration", + "displayName": "Pyrsia Integration", + "description": "Pyrsia Integration for VS Code", + "version": "0.0.1", + "engines": { + "vscode": "^1.74.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onView:pyrsia.node" + ], + "main": "./out/extension.js", + "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "package-explorer", + "title": "Pyrsia", + "icon": "resources/images/pyrsia_small.svg" + } + ] + }, + "views": { + "package-explorer": [ + { + "id": "pyrsia.node", + "name": "Node", + "type": "webview" + }, + { + "id": "pyrsia.node-config", + "name": "Configuration" + }, + { + "id": "pyrsia.integrations", + "name": "Integrations" + }, + { + "id": "pyrsia.help", + "name": "Help nad Feedback" + } + ] + }, + "commands": [ + { + "command": "pyrsia.startNode", + "title": "Start Pyrsia Node" + }, + { + "command": "pyrsia.stopNode", + "title": "Stop Pyrsia Node" + }, + { + "command": "pyrsia.node-config.tree.refresh", + "title": "Refresh" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "pretest": "npm run compile && npm run lint", + "lint": "eslint src --ext ts", + "test": "node ./out/test/runTest.js" + }, + "devDependencies": { + "@types/glob": "^8.0.0", + "@types/mocha": "^10.0.1", + "@types/node": "16.x", + "@types/vscode": "^1.73.1", + "@typescript-eslint/eslint-plugin": "^5.47.0", + "@typescript-eslint/parser": "^5.47.0", + "@vscode/test-electron": "^2.2.0", + "eslint": "^8.30.0", + "glob": "^8.0.3", + "mocha": "^10.1.0", + "typescript": "^4.9.4" + }, + "dependencies": { + "@vscode/webview-ui-toolkit": "^1.2.0", + "axios": "^1.2.1" + } +} diff --git a/resources/images/pyrsia_small.svg b/resources/images/pyrsia_small.svg new file mode 100644 index 0000000..0b432cc --- /dev/null +++ b/resources/images/pyrsia_small.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..a457e11 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,34 @@ +import * as vscode from 'vscode'; +import { NodeConfigView } from './webviews/NodeConfigView'; +import { NodeViewProvider } from './webviews/NodeViewProvider'; + +export function activate(context: vscode.ExtensionContext) { + + console.log('Pyrsia extension activated'); + + const nodeView = new NodeViewProvider(context); + const nodeConfigView = new NodeConfigView(context); + + //Notify the Node Config View when connected to node + nodeView.onDidConnect({ + onDidConnect() { + nodeConfigView.update(); + }, + }); + + // nodeView.onDidConnect(new NodeViewListener { + + // }); + + // let startNode = vscode.commands.registerCommand('pyrsia.isNodeHealthy', () => { + // nodeProvider.isNodeHealthy; + // }); + + // let stopNode = vscode.commands.registerCommand('pyrsia.stopNode', () => { + // nodeProvider.stop(); + // }); + + // context.subscriptions.push(startNode, stopNode); +} + +// export function deactivate() {} \ No newline at end of file diff --git a/src/model/NodeConfig.ts b/src/model/NodeConfig.ts new file mode 100644 index 0000000..4041289 --- /dev/null +++ b/src/model/NodeConfig.ts @@ -0,0 +1,28 @@ +export class NodeConfig { + private static readonly defaultHostname = "localhost"; + private static readonly defaultPort = "7888"; + + private _hostname: string; + private _port: string; + + constructor(hostname: string = NodeConfig.defaultHostname, port: string = NodeConfig.defaultPort) { + this._hostname = hostname; + this._port = port; + } + + get hostname(): string { + return this._hostname; + } + + set hostname(hostname: string) { + this._hostname = hostname; + } + + get port(): string { + return this._port; + } + + set port(port: string) { + this._port = port; + } +} \ No newline at end of file diff --git a/src/nodeProvider.ts b/src/nodeProvider.ts new file mode 100644 index 0000000..d93de5d --- /dev/null +++ b/src/nodeProvider.ts @@ -0,0 +1,36 @@ +import * as cp from 'child_process'; +import * as client from './utilities/client'; + +export class NodeProvider { + + nodeProcess: cp.ChildProcess; + pid: number | undefined; + + async isNodeHealthy() : Promise { + return await client.isNodeHealthy(); + } + + getHostname() : string { + return client.getNodeUrl(); + } + + async getStatus() : Promise { + return await client.getStatus(); + } + + // async start() { + + // this.pid = this.nodeProcess.pid; + // console.log(this.pid); + // vscode.window.showInformationMessage('Pyrsia Node Started'); + // if (!await client.isNodeHealth()) { + + // } + // } + + // stop() { + // this.nodeProcess.kill(); + // console.log(this.pid); + // vscode.window.showInformationMessage('Pyrsia Node Stopped'); + // } +} \ No newline at end of file diff --git a/src/test/runTest.ts b/src/test/runTest.ts new file mode 100644 index 0000000..27b3ceb --- /dev/null +++ b/src/test/runTest.ts @@ -0,0 +1,23 @@ +import * as path from 'path'; + +import { runTests } from '@vscode/test-electron'; + +async function main() { + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + + // The path to test runner + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, './suite/index'); + + // Download VS Code, unzip it and run the integration test + await runTests({ extensionDevelopmentPath, extensionTestsPath }); + } catch (err) { + console.error('Failed to run tests'); + process.exit(1); + } +} + +main(); diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts new file mode 100644 index 0000000..b13f0a9 --- /dev/null +++ b/src/test/suite/extension.test.ts @@ -0,0 +1,12 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; +// import * as myExtension from '../../extension'; + +suite('Extension Test Suite', () => { + vscode.window.showInformationMessage('Start all tests.'); + + test('Sample test', () => { + assert.strictEqual(-1, [1, 2, 3].indexOf(5)); + assert.strictEqual(-1, [1, 2, 3].indexOf(0)); + }); +}); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts new file mode 100644 index 0000000..7029e38 --- /dev/null +++ b/src/test/suite/index.ts @@ -0,0 +1,38 @@ +import * as path from 'path'; +import * as Mocha from 'mocha'; +import * as glob from 'glob'; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: 'tdd', + color: true + }); + + const testsRoot = path.resolve(__dirname, '..'); + + return new Promise((c, e) => { + glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } + + // Add files to the test suite + files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); + + try { + // Run the mocha test + mocha.run(failures => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + console.error(err); + e(err); + } + }); + }); +} diff --git a/src/utilities/client.ts b/src/utilities/client.ts new file mode 100644 index 0000000..fce55fb --- /dev/null +++ b/src/utilities/client.ts @@ -0,0 +1,69 @@ +import axios from "axios"; +import * as util from "./util"; + +// TODO This should be obtained from the pyrsia node config +export function getNodeUrl(): string { + return `http://${util.getNodeConfig().hostname}:${getNodePort()}`; +} + +// TODO This should be obtained from the pyrsia node config +function getNodePort(): string { + return util.getNodeConfig().port; +} + +type PingResponse = { + data: string[]; +}; + +type StatusResponse = { + data: string[]; +}; + +export async function isNodeHealthy(): Promise { + console.log('Check node health'); + const nodeUrl = `${getNodeUrl()}/v2`; + let status; + try { + ({ status } = await axios.get( + nodeUrl, + { + headers: { + accept: 'application/json', + }, + }, + )); + } catch (e) { + console.error(e); + } + + return status === 200; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function getStatus(): Promise { + console.log('Get node status'); + const nodeUrl = `${getNodeUrl()}/status`; + let data; + + try { + ({ data } = await axios.get( + nodeUrl, + { + headers: { + accept: 'application/json', + }, + }, + )); + } catch (e) { + console.error(e); + } + + return data; +} + +export async function getPeers(): Promise { + console.log('Get node peers'); + const data = await getStatus(); + + return data ? data.peers_count : "0"; +} \ No newline at end of file diff --git a/src/utilities/util.ts b/src/utilities/util.ts new file mode 100644 index 0000000..a270ab2 --- /dev/null +++ b/src/utilities/util.ts @@ -0,0 +1,11 @@ +import { Uri, Webview } from "vscode"; +import { NodeConfig } from "../model/NodeConfig"; + +export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { + return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); +} + +// TODO This should be obtained from the pyrsia node config +export function getNodeConfig() : NodeConfig { + return new NodeConfig(); // TODO Replace it with the configuration obtained from the ide cache/storage +} diff --git a/src/webview-ui/main.js b/src/webview-ui/main.js new file mode 100644 index 0000000..ac4ae52 --- /dev/null +++ b/src/webview-ui/main.js @@ -0,0 +1,36 @@ +const vscode = acquireVsCodeApi(); + +window.addEventListener("load", main); + +function main() { + const nodeConnectButton = document.getElementById("node-button-connect"); + nodeConnectButton.addEventListener("click", updateNode); + nodeConnectButton.style.color = newColor; +} + +function updateNode() { + vscode.postMessage({ + command: "node-update-view", + }); +} + +window.addEventListener('message', event => { + const message = event.data; // The json data that the extension sent + switch (message.type) { + case 'node-disconnected': + { + document.getElementById("node-connected").style.display = "none"; + document.getElementById("node-disconnected").style.display = "block"; + break; + } + case 'node-connected': + { + document.getElementById("node-connected").style.display = "block"; + document.getElementById("node-disconnected").style.display = "none"; + // document.getElementById("node-peer-count").innerHTML = message.nodeStatus.peers_count || "0"; + // document.getElementById("node-peer-id").innerHTML = message.nodeStatus.peer_id || ""; + // document.getElementById("node-peer-addresses").innerHTML = message.nodeStatus.peer_addrs || ""; + break; + } + } +}); \ No newline at end of file diff --git a/src/webview-ui/styles.css b/src/webview-ui/styles.css new file mode 100644 index 0000000..5737d89 --- /dev/null +++ b/src/webview-ui/styles.css @@ -0,0 +1,124 @@ +:root { + --container-padding: 20px; + --input-padding-vertical: 6px; + --input-padding-horizontal: 4px; + --input-margin-vertical: 4px; + --input-margin-horizontal: 0; + --margin-break-small: 10px; + --margin-break: 20px; + --margin-break-large: 40px; +} + +body { + padding: 0 var(--container-padding); + color: var(--vscode-foreground); + font-size: var(--vscode-font-size); + font-weight: var(--vscode-font-weight); + font-family: var(--vscode-font-family); + background-color: #252526; +} + +ol, +ul { + padding-left: var(--container-padding); +} + +body > *, +form > * { + margin-block-start: var(--input-margin-vertical); + margin-block-end: var(--input-margin-vertical); +} + +*:focus { + outline-color: var(--vscode-focusBorder) !important; +} + +a { + color: var(--vscode-textLink-foreground); +} + +a:hover, +a:active { + color: var(--vscode-textLink-activeForeground); +} + +code { + font-size: var(--vscode-editor-font-size); + font-family: var(--vscode-editor-font-family); +} + +button { + border: none; + padding: var(--input-padding-vertical) var(--input-padding-horizontal); + width: 100%; + text-align: center; + outline: 1px solid transparent; + outline-offset: 2px !important; + color: var(--vscode-button-foreground); + background: var(--vscode-button-background); + max-width: 300px; + margin:0 auto; + display:block; + +} + +button:hover { + cursor: pointer; + background: var(--vscode-button-hoverBackground); +} + +button:focus { + outline-color: var(--vscode-focusBorder); +} + +button.secondary { + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); +} + +button.secondary:hover { + background: var(--vscode-button-secondaryHoverBackground); +} + +input:not([type='checkbox']), +textarea { + display: block; + width: 100%; + border: none; + font-family: var(--vscode-font-family); + padding: var(--input-padding-vertical) var(--input-padding-horizontal); + color: var(--vscode-input-foreground); + outline-color: var(--vscode-input-border); + background-color: var(--vscode-input-background); +} + +input::placeholder, +textarea::placeholder { + color: var(--vscode-input-placeholderForeground); +} + +* { + margin: 2; +} + +/* hr { + margin-bottom: var(--margin-break-small); +} */ + +.break { + margin-bottom: var(--margin-break-small); +} + +.dimmer { + color: darkgrey; +} + +/* #node-container { + display: flex; + align-items: center; + justify-content: space-between; + background-color: var(--vscode-input-background); + padding: 1rem; + margin: 1rem 0; +} */ + diff --git a/src/webviews/NodeConfigView.ts b/src/webviews/NodeConfigView.ts new file mode 100644 index 0000000..c480b71 --- /dev/null +++ b/src/webviews/NodeConfigView.ts @@ -0,0 +1,339 @@ +// https://github.com/xojs/eslint-config-xo-typescript/issues/43 +/* eslint-disable @typescript-eslint/naming-convention */ +import * as vscode from 'vscode'; +import * as util from '../utilities/util'; +import * as client from '../utilities/client'; + +// TODO With branches +// enum NodeConfigProperty { +// Hostname = "hostname", // NO I18 +// Port = "port", // NO I18 +// HostnameValue = "hostnamevalue", // NO I18 +// PortValue = "portvalue", // NO I18 +// Peers = "peers", // NO I18 +// PeersValue = "peersvalue", // NO I18 +// } + +// TODO Without branches +enum NodeConfigProperty { + Hostname = "hostname", // NO I18 + Port = "port", // NO I18 + // HostnameValue = "hostnamevalue", // NO I18 + // PortValue = "portvalue", // NO I18 + Peers = "peers", // NO I18 + // PeersValue = "peersvalue", // NO I18 +} + +export class NodeConfigView { + private static readonly viewType: string = "pyrsia.node-config"; // NO I18 + private readonly treeViewProvider: NodeConfigTreeProvider; + + constructor(context: vscode.ExtensionContext) { + this.treeViewProvider = new NodeConfigTreeProvider(); + const view = vscode.window.createTreeView(NodeConfigView.viewType, { treeDataProvider: this.treeViewProvider, showCollapseAll: true }); + vscode.window.registerTreeDataProvider(NodeConfigView.viewType, this.treeViewProvider); + + context.subscriptions.push(view); + + vscode.commands.registerCommand('pyrsia.node-config.tree.refresh', () => { + this.treeViewProvider.update(); + }); + + view.onDidChangeVisibility(() => { + this.treeViewProvider.update(); + }); + } + + public update(): void { + this.treeViewProvider.update(); + } +} + + +// TODO This is the tree provider which returns branches +// class NodeConfigTreeProvider implements vscode.TreeDataProvider { + +// private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); +// readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + +// private treeItems: Map = new Map(); + +// async update() { +// for (const nodeProperty in NodeConfigProperty) { // TODO Why nodeProperty is 'string' type? Investigate +// const treeItem = this.treeItems.get(nodeProperty.toLocaleLowerCase()); +// if (treeItem) { +// await treeItem.update(); +// } +// } + +// this._onDidChangeTreeData.fire(); +// } + +// getTreeItem(id: string): vscode.TreeItem | Thenable { +// const treeItem = this.treeItems.get(id); +// if (!treeItem) { +// throw new Error(`Tree item ${id} doesn't exist.`); +// } + +// return treeItem; +// } + +// getChildren(parentId?: string | undefined): vscode.ProviderResult { +// let childrenArray: string[] = []; +// if (!parentId) { // Create all tree Items for the tree +// for (const nodeProperty in NodeConfigProperty) { // TODO Why nodeProperty is 'string' type? Investigate +// const treeItem = this.treeItems.get(nodeProperty.toLowerCase()); +// if (!treeItem) { +// const enumType = NodeConfigProperty[nodeProperty as keyof typeof NodeConfigProperty]; // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? +// this.treeItems.set(nodeProperty.toLocaleLowerCase(), NodeTreeItem.create(enumType)); // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? +// } +// } +// childrenArray = [... this.treeItems].map(([, value]) => { +// return value.isRoot() ? value.id : ""; +// }).filter(value => value !== ""); +// } else { // not tree root then get the particular id for the parentId +// const childId = NodeTreeItem.getChildrenId(parentId); +// const treeItem: NodeTreeItem = this.treeItems.get(childId) as NodeTreeItem; +// childrenArray = [treeItem.id]; +// } + +// return childrenArray; +// } +// } + +// class NodeTreeItem extends vscode.TreeItem { + +// constructor( +// public label: string, +// public readonly collapsibleState: vscode.TreeItemCollapsibleState, +// public readonly id: string, +// public readonly root: boolean, +// private readonly listener: NodeConfigListener, +// ) { +// super(label, collapsibleState); +// this.tooltip = this.label; +// } + +// static create(nodeProperty: NodeConfigProperty): NodeTreeItem { +// const property = this.properties[nodeProperty]; +// const collapsibleState = property.root ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None; +// return new NodeTreeItem( +// property.name, +// collapsibleState, +// property.id, +// property.root, +// property.listener, +// ); +// } + +// async update() { +// await this.listener.onUpdate(this); +// } + +// isRoot(): boolean { +// return this.root; +// } + +// static getChildrenId(parentId: string) { +// return `${parentId}value`; +// } + +// private static readonly properties = { +// [NodeConfigProperty.Hostname.toLowerCase()]: { +// name: "Hostname", +// id: "hostname", // NO I18 +// root: true, +// listener: { +// onUpdate: async () => { +// console.log(`Nothing to update - ${this.name}`); +// } +// }, +// }, [NodeConfigProperty.HostnameValue.toLowerCase()]: { +// name: "Getting node hostname...", +// id: "hostnamevalue", // NO I18 +// root: false, +// listener: { +// onUpdate: async (treeItem: NodeTreeItem) => { +// treeItem.label = util.getNodeConfig().hostname; +// } +// }, +// }, +// [NodeConfigProperty.Port.toLowerCase()]: { +// name: "Port", +// id: "port", // NO I18 +// root: true, +// listener: { +// onUpdate: async () => { +// console.log(`Nothing to update - ${this.name}`); +// } +// }, +// }, +// [NodeConfigProperty.PortValue.toLowerCase()]: { +// name: "Getting node port...", +// id: "portvalue", // NO I18 +// root: false, +// listener: { +// onUpdate: async (treeItem: NodeTreeItem) => { +// treeItem.label = util.getNodeConfig().port; +// } +// }, +// }, +// [NodeConfigProperty.Peers.toLowerCase()]: { +// name: "Peers Count", +// id: "peers", // NO I18 +// root: true, +// listener: { +// onUpdate: async () => { +// console.log(`Nothing to update - ${this.name}`); +// } +// }, +// }, +// [NodeConfigProperty.PeersValue.toLowerCase()]: { +// name: "Getting node peers...", +// id: "peersvalue", // NO I18 +// root: false, +// listener: { +// onUpdate: async (treeItem: NodeTreeItem) => { +// const peers = await client.getPeers(); +// treeItem.label = peers.toString(); +// treeItem.tooltip = peers.toString(); +// console.log(`Updating - ${this.name}`); +// } +// }, +// }, +// }; +// } + + +// TODO This is flat tree (no branches) +class NodeConfigTreeProvider implements vscode.TreeDataProvider { + + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + private treeItems: Map = new Map(); + + async update() { + for (const nodeProperty in NodeConfigProperty) { // TODO Why nodeProperty is 'string' type? Investigate + const treeItem = this.treeItems.get(nodeProperty.toLocaleLowerCase()); + if (treeItem) { + await treeItem.update(); + } + } + + this._onDidChangeTreeData.fire(); + } + + getTreeItem(id: string): vscode.TreeItem | Thenable { + const treeItem = this.treeItems.get(id); + if (!treeItem) { + throw new Error(`Tree item ${id} doesn't exist.`); + } + + return treeItem; + } + + getChildren(parentId?: string | undefined): vscode.ProviderResult { + let childrenArray: string[] = []; + if (!parentId) { // Create all tree Items for the tree + for (const nodeProperty in NodeConfigProperty) { // TODO Why nodeProperty is 'string' type? Investigate + const treeItem = this.treeItems.get(nodeProperty.toLowerCase()); + if (!treeItem) { + const enumType = NodeConfigProperty[nodeProperty as keyof typeof NodeConfigProperty]; // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? + this.treeItems.set(nodeProperty.toLocaleLowerCase(), NodeTreeItem.create(enumType)); // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? + } + } + childrenArray = [... this.treeItems].map(([, value]) => { + return value.isRoot() ? value.id : ""; + }).filter(value => value !== ""); + } else { // not tree root then get the particular id for the parentId + const childId = NodeTreeItem.getChildrenId(parentId); + const treeItem: NodeTreeItem = this.treeItems.get(childId) as NodeTreeItem; + childrenArray = [treeItem.id]; + } + + return childrenArray; + } +} + +class NodeTreeItem extends vscode.TreeItem { + + constructor( + public label: string, + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + public readonly id: string, + public readonly root: boolean, + private readonly listener: NodeConfigListener, + ) { + super(label, collapsibleState); + this.tooltip = this.label; + } + + static create(nodeProperty: NodeConfigProperty): NodeTreeItem { + const property = this.properties[nodeProperty]; + const collapsibleState = vscode.TreeItemCollapsibleState.None; + return new NodeTreeItem( + property.name, + collapsibleState, + property.id, + property.root, + property.listener, + ); + } + + async update() { + await this.listener.onUpdate(this); + } + + isRoot(): boolean { + return this.root; + } + + static getChildrenId(parentId: string) { + return `${parentId}value`; + } + + private static readonly properties = { + [NodeConfigProperty.Hostname.toLowerCase()]: { + name: "Host", + id: "hostname", // NO I18 + root: true, + listener: { + onUpdate: async (treeItem: NodeTreeItem) => { + const name = NodeTreeItem.properties[NodeConfigProperty.Hostname.toLowerCase()].name; + treeItem.label = `${name}: ${util.getNodeConfig().hostname}`; + } + } + }, + [NodeConfigProperty.Port.toLowerCase()]: { + name: "Port", + id: "port", // NO I18 + root: true, + listener: { + onUpdate: async (treeItem: NodeTreeItem) => { + const name = NodeTreeItem.properties[NodeConfigProperty.Port.toLowerCase()].name; + treeItem.label = `${name}: ${util.getNodeConfig().port}`; + } + }, + }, + [NodeConfigProperty.Peers.toLowerCase()]: { + name: "Peers Count", + id: "peers", // NO I18 + root: true, + listener: { + onUpdate: async (treeItem: NodeTreeItem) => { + const peers = await client.getPeers(); + const name = NodeTreeItem.properties[NodeConfigProperty.Peers.toLowerCase()].name; + treeItem.label = `${name}: ${peers.toString()}`; + treeItem.tooltip = `${name}: ${peers.toString()}`; + console.log(`Updating - ${name}`); + } + }, + }, + }; +} + + +interface NodeConfigListener { + onUpdate(treeItem: NodeTreeItem): void; +} \ No newline at end of file diff --git a/src/webviews/NodeViewProvider.ts b/src/webviews/NodeViewProvider.ts new file mode 100644 index 0000000..be0772e --- /dev/null +++ b/src/webviews/NodeViewProvider.ts @@ -0,0 +1,150 @@ +import * as vscode from 'vscode'; +import { NodeProvider } from "../nodeProvider"; +import { getUri } from "../utilities/util"; + +export class NodeViewProvider implements vscode.WebviewViewProvider { + public static readonly viewType = "pyrsia.node"; + + private nodeProvider; + private _view?: vscode.WebviewView; + private extensionUri: vscode.Uri; + private readonly onDidConnectListeners: Set = new Set(); + + constructor(private readonly context: vscode.ExtensionContext) { + this.nodeProvider = new NodeProvider(); + + vscode.window.registerWebviewViewProvider( + NodeViewProvider.viewType, + this, + ); + + this.extensionUri = context.extensionUri; + } + + public getWebView(): vscode.WebviewView { + return this._view as vscode.WebviewView; + } + + onDidConnect(listener: NodeViewListener): void { + this.onDidConnectListeners.add(listener); + } + + public resolveWebviewView( + view: vscode.WebviewView + ) { + this._view = view; + + view.webview.options = { + enableScripts: true, + }; + + view.webview.html = this.getWebviewContent(view.webview, this.extensionUri); + this.setWebviewMessageListener(view); + + view.onDidChangeVisibility(() :void => { + this.updateView(); + }); + + this.updateView(); + } + + private getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { + + const toolkitUri = getUri(webview, extensionUri, [ + "node_modules", + "@vscode", + "webview-ui-toolkit", + "dist", + "toolkit.js", + ]); + + const mainUri = getUri(webview, extensionUri, ["src", "webview-ui", "main.js"]); + const stylesUri = getUri(webview, extensionUri, ["src", "webview-ui", "styles.css"]); + const pyrsiaHostname = this.nodeProvider.getHostname(); + + return /*html*/ ` + + + + + + + + + + Node + + + +
+
+
🟢 Connected to ${pyrsiaHostname}
+
+ +
+
🔴 Failed connecting to ${pyrsiaHostname}
+
+
👉 Please make sure Pyrsia is installed, + + running and configured.. 👈 +
+
+ +
+ +
+ + + + `; + } + + private setWebviewMessageListener(view: vscode.WebviewView) { + view.webview.onDidReceiveMessage(async (message) => { + const command = message.command; + switch (command) { + case "node-update-view": { + this.updateView(); + } + } + }); + } + + private connected() { + const view = this._view; + if (view) { + let nodeStatus: unknown; + this.nodeProvider.getStatus().then((data) => { + nodeStatus = data; + }).finally(() => { + view.webview.postMessage({ type: 'node-connected', nodeStatus }); + for (const listener of this.onDidConnectListeners) { + listener.onDidConnect(); + } + }); + } + } + + private disconnected() { + if (this._view) { + this._view.webview.postMessage({ type: 'node-disconnected' }); + } + for (const listener of this.onDidConnectListeners) { + listener.onDidConnect(); + } + } + + public async updateView() { + const connected: boolean = await this.nodeProvider.isNodeHealthy(); + if (connected) { + this.connected(); + } else { + this.disconnected(); + } + } +} + +export interface NodeViewListener { + onDidConnect(): void; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9ea4441 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "strictPropertyInitialization": false, + "lib": [ + "ES2020" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true, /* enable all strict type-checking options */ + /* Additional Checks */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + "noUnusedParameters": true, /* Report errors on unused parameters. */ + } +} From f47c6f18decf52dd73b831f57d4bbed9582918ca Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Tue, 27 Dec 2022 16:46:18 -0800 Subject: [PATCH 02/16] API for integrations added (basic) --- package-lock.json | 14 +- package.json | 27 ++-- resources/images/docker_small.svg | 10 ++ src/extension.ts | 28 ++-- src/integrations/Docker.ts | 128 ++++++++++++++++++ src/integrations/Integration.ts | 28 ++++ src/utilities/client.ts | 6 +- src/utilities/fsUtils.ts | 17 +++ src/utilities/util.ts | 33 ++++- src/utilities/vscodeUtils.ts | 17 +++ src/webviews/NodeConfigView.ts | 23 ++-- src/webviews/NodeIntegrationsView.ts | 116 ++++++++++++++++ ...{NodeViewProvider.ts => NodeStatusView.ts} | 20 +-- 13 files changed, 411 insertions(+), 56 deletions(-) create mode 100644 resources/images/docker_small.svg create mode 100644 src/integrations/Docker.ts create mode 100644 src/integrations/Integration.ts create mode 100644 src/utilities/fsUtils.ts create mode 100644 src/utilities/vscodeUtils.ts create mode 100644 src/webviews/NodeIntegrationsView.ts rename src/webviews/{NodeViewProvider.ts => NodeStatusView.ts} (85%) diff --git a/package-lock.json b/package-lock.json index 4fcaf42..b2bcb84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "0.0.1", "dependencies": { "@vscode/webview-ui-toolkit": "^1.2.0", - "axios": "^1.2.1" + "axios": "^1.2.1", + "fs": "^0.0.1-security", + "os": "^0.1.2" }, "devDependencies": { "@types/glob": "^8.0.0", @@ -1269,6 +1271,11 @@ "node": ">= 6" } }, + "node_modules/fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2005,6 +2012,11 @@ "node": ">= 0.8.0" } }, + "node_modules/os": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/os/-/os-0.1.2.tgz", + "integrity": "sha512-ZoXJkvAnljwvc56MbvhtKVWmSkzV712k42Is2mA0+0KTSRakq5XXuXpjZjgAt9ctzl51ojhQWakQQpmOvXWfjQ==" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", diff --git a/package.json b/package.json index 38a0b22..ec12c16 100644 --- a/package.json +++ b/package.json @@ -27,16 +27,16 @@ "package-explorer": [ { "id": "pyrsia.node", - "name": "Node", + "name": "Node Status", "type": "webview" }, { "id": "pyrsia.node-config", - "name": "Configuration" + "name": "Node Configuration" }, { - "id": "pyrsia.integrations", - "name": "Integrations" + "id": "pyrsia.node-integrations", + "name": "Available Integrations" }, { "id": "pyrsia.help", @@ -44,20 +44,7 @@ } ] }, - "commands": [ - { - "command": "pyrsia.startNode", - "title": "Start Pyrsia Node" - }, - { - "command": "pyrsia.stopNode", - "title": "Stop Pyrsia Node" - }, - { - "command": "pyrsia.node-config.tree.refresh", - "title": "Refresh" - } - ] + "commands": [] }, "scripts": { "vscode:prepublish": "npm run compile", @@ -82,6 +69,8 @@ }, "dependencies": { "@vscode/webview-ui-toolkit": "^1.2.0", - "axios": "^1.2.1" + "axios": "^1.2.1", + "fs": "^0.0.1-security", + "os": "^0.1.2" } } diff --git a/resources/images/docker_small.svg b/resources/images/docker_small.svg new file mode 100644 index 0000000..d06ad89 --- /dev/null +++ b/resources/images/docker_small.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/extension.ts b/src/extension.ts index a457e11..2f56e28 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,14 +1,24 @@ import * as vscode from 'vscode'; import { NodeConfigView } from './webviews/NodeConfigView'; -import { NodeViewProvider } from './webviews/NodeViewProvider'; +import { NodeStatusViewProvider } from './webviews/NodeStatusView'; +import { NodeIntegrationsView } from './webviews/NodeIntegrationsView'; +import { Util } from './utilities/util'; -export function activate(context: vscode.ExtensionContext) { +export async function activate(context: vscode.ExtensionContext) { console.log('Pyrsia extension activated'); - const nodeView = new NodeViewProvider(context); + // initialize the util + Util.init(context); + + // Node status web view provider + const nodeView = new NodeStatusViewProvider(context); + // Node status config view const nodeConfigView = new NodeConfigView(context); + // Node status config view + new NodeIntegrationsView(context); + //Notify the Node Config View when connected to node nodeView.onDidConnect({ onDidConnect() { @@ -20,15 +30,17 @@ export function activate(context: vscode.ExtensionContext) { // }); - // let startNode = vscode.commands.registerCommand('pyrsia.isNodeHealthy', () => { - // nodeProvider.isNodeHealthy; + // const startNode = vscode.commands.registerCommand('pyrsia.startNode', (a) => { + // // nodeProvider.isNodeHealthy; + // console.log(a); // }); - // let stopNode = vscode.commands.registerCommand('pyrsia.stopNode', () => { - // nodeProvider.stop(); + // const stopNode = vscode.commands.registerCommand('pyrsia.stopNode', () => { + // //nodeProvider.stop(); + // console.log("something"); // }); - // context.subscriptions.push(startNode, stopNode); + //context.subscriptions.push(startNode, stopNode); } // export function deactivate() {} \ No newline at end of file diff --git a/src/integrations/Docker.ts b/src/integrations/Docker.ts new file mode 100644 index 0000000..ea825ff --- /dev/null +++ b/src/integrations/Docker.ts @@ -0,0 +1,128 @@ +import * as os from "os"; +import * as fsUtils from "../utilities/fsUtils"; +import { Integration, IntegrationTreeItem } from "./Integration"; +import * as path from 'path'; +import { Util } from "../utilities/util"; +import * as vscode from 'vscode'; + +export class Docker implements Integration { + + private static readonly dockerId: string = "docker"; + private readonly treeItems: Map = new Map(); + private static confMap: Map = new Map(); + private static pyrsiaConfigCode = ` "registry-mirrors": ["http://0.0.0.0:7888"] \n`; + + static readonly commandId = "pyrsia.openUpdateDockerConfFile"; + + static { + Docker.confMap.set(path.join(os.homedir(),".docker"), "config.json"); + } + + constructor(context: vscode.ExtensionContext) { + + // docker open and update configuration editor + const openDockerUpdateConfFile = vscode.commands.registerCommand(Docker.commandId, (confFilePath: string) => { + console.log(confFilePath); + const setting: vscode.Uri = vscode.Uri.parse(`${confFilePath}`); + + vscode.workspace.openTextDocument(setting).then((textDocument: vscode.TextDocument) => { + vscode.window.showTextDocument(textDocument, 1, false).then(textEditor => { + const ls = textDocument.lineCount; + textEditor.edit(edit => { + // TODO This is a prototype, not even close to the real solution. + const line = textDocument.lineAt(ls - 3); + edit.delete(line.range); + edit.insert(new vscode.Position(line.lineNumber, 0), `${line.text},`); + edit.insert(new vscode.Position(ls - 2, 0), Docker.pyrsiaConfigCode); + }); + textEditor.selection = new vscode.Selection(new vscode.Position(2, 2), new vscode.Position(2, Docker.pyrsiaConfigCode.length + 2)); + }); + }, (error: unknown) => { + console.error(error); + }); + + }); + + context.subscriptions.push(openDockerUpdateConfFile); + } + + getTreeItem(treeItemId: string): IntegrationTreeItem | undefined { + return this.treeItems.get(treeItemId); + } + + getId(): string { + return Docker.dockerId; + } + + async update(): Promise { + // find docker conf file (macos, linux) + for (const confPath of Docker.confMap.keys()) { + const fileName = Docker.confMap.get(confPath); + if (!fileName) { + throw new Error("Configuration file name cannot be null"); + } + const confFilePath = await fsUtils.findByName(confPath, fileName); + if (confFilePath) { + const treeItemId = `${this.getId()}-${confFilePath}`; + const label = `${confFilePath}`; + this.treeItems.set(treeItemId, new DockerTreeItem(label, treeItemId, confFilePath)); + } else { + console.log(`No configuration for 'Docker' - ${path.join(confPath, fileName)}`); + } + } + } + + getTreeItemChildren(): string[] { + const children: string[] = []; + for (const label of this.treeItems.keys()) { + children.push(label); + } + + return children; + } +} + +class DockerTreeItem extends IntegrationTreeItem { + + constructor( + public label: string, + public readonly id: string, + public readonly confFilePath: string, + ) { + super(label, id); + // const command = new class implements vscode.Command { + // title: string; + // command: string; + // tooltip?: string | undefined; + // arguments?: any[] | undefined; + // run() { + // console.log("Docker open File"); + // } + // }(); + // command.title = "Open Docker Configuration"; + // command.command = "command-open-docker-node"; + // command.tooltip = command.title; + // super.command = command; + this.command = { command: Docker.commandId, title: "Start Pyrsia", arguments: [confFilePath] }; + + } + + // command?: vscode.Command | undefined = new class implements vscode.Command { + // title: string; + // command: string; + // tooltip?: string | undefined; + // arguments?: any[] | undefined; + // run() { + // console.log("Docker open File"); + // } + // }(); + + update(): void { + throw new Error("Method not implemented."); + } + + iconPath = { + light: path.join(Util.getResourceImagePath(), "docker_small.svg"), + dark: path.join(Util.getResourceImagePath(), "docker_small.svg"), //TODO update to dark + }; +} \ No newline at end of file diff --git a/src/integrations/Integration.ts b/src/integrations/Integration.ts new file mode 100644 index 0000000..1bca65c --- /dev/null +++ b/src/integrations/Integration.ts @@ -0,0 +1,28 @@ +import * as vscode from 'vscode'; + +export interface Integration { + + getTreeItem(treeItemId: string): IntegrationTreeItem | undefined; + + getTreeItemChildren(): string[]; + + getId(): string; + + update(): void; + +} + +export abstract class IntegrationTreeItem extends vscode.TreeItem { + + constructor( + public label: string, + public readonly id: string, + ) { + super(label, vscode.TreeItemCollapsibleState.None); + this.tooltip = this.label; + } + + abstract update(): void; + +} + diff --git a/src/utilities/client.ts b/src/utilities/client.ts index fce55fb..48b9ab5 100644 --- a/src/utilities/client.ts +++ b/src/utilities/client.ts @@ -1,14 +1,14 @@ import axios from "axios"; -import * as util from "./util"; +import { Util } from "./util"; // TODO This should be obtained from the pyrsia node config export function getNodeUrl(): string { - return `http://${util.getNodeConfig().hostname}:${getNodePort()}`; + return `http://${Util.getNodeConfig().hostname}:${getNodePort()}`; } // TODO This should be obtained from the pyrsia node config function getNodePort(): string { - return util.getNodeConfig().port; + return Util.getNodeConfig().port; } type PingResponse = { diff --git a/src/utilities/fsUtils.ts b/src/utilities/fsUtils.ts new file mode 100644 index 0000000..9b352ba --- /dev/null +++ b/src/utilities/fsUtils.ts @@ -0,0 +1,17 @@ +import { readdir } from "fs/promises"; +import * as path from "path"; + +export async function findByName(dir: string, fileName: string): Promise { + let matchedFile: string | undefined = undefined; + + const dirFileNames = await readdir(dir); + + for (const dirFileName of dirFileNames) { + if (dirFileName === fileName) { + matchedFile = path.join(dir, dirFileName); + break; + } + } + + return matchedFile; +} diff --git a/src/utilities/util.ts b/src/utilities/util.ts index a270ab2..e0f9d8a 100644 --- a/src/utilities/util.ts +++ b/src/utilities/util.ts @@ -1,11 +1,32 @@ import { Uri, Webview } from "vscode"; import { NodeConfig } from "../model/NodeConfig"; +import path = require("path"); +import * as vscode from 'vscode'; -export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { - return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); -} +export class Util { + + private static resourcePath: string; + + static init(context: vscode.ExtensionContext): void { + Util.resourcePath = context.asAbsolutePath(path.join('resources')); + } -// TODO This should be obtained from the pyrsia node config -export function getNodeConfig() : NodeConfig { - return new NodeConfig(); // TODO Replace it with the configuration obtained from the ide cache/storage + static getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { + return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); + } + + // TODO This should be obtained from the pyrsia node config + static getNodeConfig(): NodeConfig { + return new NodeConfig(); // TODO Replace it with the configuration obtained from the ide cache/storage + } + + static getResourcePath(): string { + return Util.resourcePath; + } + + static getResourceImagePath(): string { + return path.join(Util.resourcePath, "images"); + } } + + diff --git a/src/utilities/vscodeUtils.ts b/src/utilities/vscodeUtils.ts new file mode 100644 index 0000000..0a715ca --- /dev/null +++ b/src/utilities/vscodeUtils.ts @@ -0,0 +1,17 @@ +import * as vscode from 'vscode'; + +export function openFileInEditor(filePath: string) : void { + + const setting: vscode.Uri = vscode.Uri.parse(`${filePath}`); + + vscode.workspace.openTextDocument(setting).then((a: vscode.TextDocument) => { + vscode.window.showTextDocument(a, 1, false).then(e => { + e.edit(edit => { + edit.insert(new vscode.Position(0, 0), "\\ TODO Pyrsia node config json"); + }); + }); + }, (error: unknown) => { + console.error(error); + }); + +} \ No newline at end of file diff --git a/src/webviews/NodeConfigView.ts b/src/webviews/NodeConfigView.ts index c480b71..0244877 100644 --- a/src/webviews/NodeConfigView.ts +++ b/src/webviews/NodeConfigView.ts @@ -1,7 +1,7 @@ // https://github.com/xojs/eslint-config-xo-typescript/issues/43 /* eslint-disable @typescript-eslint/naming-convention */ import * as vscode from 'vscode'; -import * as util from '../utilities/util'; +import { Util } from '../utilities/util'; import * as client from '../utilities/client'; // TODO With branches @@ -234,7 +234,7 @@ class NodeConfigTreeProvider implements vscode.TreeDataProvider { } getChildren(parentId?: string | undefined): vscode.ProviderResult { - let childrenArray: string[] = []; + let children: string[] = []; if (!parentId) { // Create all tree Items for the tree for (const nodeProperty in NodeConfigProperty) { // TODO Why nodeProperty is 'string' type? Investigate const treeItem = this.treeItems.get(nodeProperty.toLowerCase()); @@ -243,16 +243,16 @@ class NodeConfigTreeProvider implements vscode.TreeDataProvider { this.treeItems.set(nodeProperty.toLocaleLowerCase(), NodeTreeItem.create(enumType)); // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? } } - childrenArray = [... this.treeItems].map(([, value]) => { + children = [... this.treeItems].map(([, value]) => { return value.isRoot() ? value.id : ""; }).filter(value => value !== ""); } else { // not tree root then get the particular id for the parentId const childId = NodeTreeItem.getChildrenId(parentId); const treeItem: NodeTreeItem = this.treeItems.get(childId) as NodeTreeItem; - childrenArray = [treeItem.id]; + children = [treeItem.id]; } - return childrenArray; + return children; } } @@ -264,6 +264,7 @@ class NodeTreeItem extends vscode.TreeItem { public readonly id: string, public readonly root: boolean, private readonly listener: NodeConfigListener, + public readonly iconPath: vscode.ThemeIcon, ) { super(label, collapsibleState); this.tooltip = this.label; @@ -278,6 +279,7 @@ class NodeTreeItem extends vscode.TreeItem { property.id, property.root, property.listener, + property.iconPath, ); } @@ -295,15 +297,16 @@ class NodeTreeItem extends vscode.TreeItem { private static readonly properties = { [NodeConfigProperty.Hostname.toLowerCase()]: { - name: "Host", + name: "Node", id: "hostname", // NO I18 root: true, listener: { onUpdate: async (treeItem: NodeTreeItem) => { const name = NodeTreeItem.properties[NodeConfigProperty.Hostname.toLowerCase()].name; - treeItem.label = `${name}: ${util.getNodeConfig().hostname}`; + treeItem.label = `${name}: ${Util.getNodeConfig().hostname}`; } - } + }, + iconPath: new vscode.ThemeIcon("cloud"), }, [NodeConfigProperty.Port.toLowerCase()]: { name: "Port", @@ -312,9 +315,10 @@ class NodeTreeItem extends vscode.TreeItem { listener: { onUpdate: async (treeItem: NodeTreeItem) => { const name = NodeTreeItem.properties[NodeConfigProperty.Port.toLowerCase()].name; - treeItem.label = `${name}: ${util.getNodeConfig().port}`; + treeItem.label = `${name}: ${Util.getNodeConfig().port}`; } }, + iconPath: new vscode.ThemeIcon("ports-view-icon"), }, [NodeConfigProperty.Peers.toLowerCase()]: { name: "Peers Count", @@ -329,6 +333,7 @@ class NodeTreeItem extends vscode.TreeItem { console.log(`Updating - ${name}`); } }, + iconPath: new vscode.ThemeIcon("extensions-install-count"), }, }; } diff --git a/src/webviews/NodeIntegrationsView.ts b/src/webviews/NodeIntegrationsView.ts new file mode 100644 index 0000000..6b0ae1e --- /dev/null +++ b/src/webviews/NodeIntegrationsView.ts @@ -0,0 +1,116 @@ +// https://github.com/xojs/eslint-config-xo-typescript/issues/43 +/* eslint-disable @typescript-eslint/naming-convention */ +import * as vscode from 'vscode'; +import { Integration, IntegrationTreeItem } from '../integrations/Integration'; +import { Docker } from '../integrations/Docker'; +import path = require('path'); +import { Util } from '../utilities/util'; + +export class NodeIntegrationsView { + private static readonly viewType: string = "pyrsia.node-integrations"; // NO I18 + private readonly treeViewProvider: NodeIntegrationsTreeProvider; + + private readonly _view?: vscode.TreeView; + + constructor(context: vscode.ExtensionContext) { + + const test = path.join(Util.getResourcePath(),"docker_small.svg"); + console.log(test); + this.treeViewProvider = new NodeIntegrationsTreeProvider(); + + // add and update the Docker integration + this.treeViewProvider.addIntegration(new Docker(context)); + + this._view = vscode.window.createTreeView(NodeIntegrationsView.viewType, { treeDataProvider: this.treeViewProvider, showCollapseAll: true }); + + + this.treeViewProvider.update(); + + vscode.commands.registerCommand('pyrsia.node-integrations.tree.refresh', async () => { + this.treeViewProvider.update(); + }); + + this._view.onDidChangeVisibility(async () => { + this.treeViewProvider.update(); + }); + + this._view.onDidChangeSelection((event) => { + console.log(event); + }); + + // this.treeViewProvider.onDidChangeSelection((event) => { + // console.log(event); + // }); + + + //vscode.window.registerTreeDataProvider(NodeIntegrationsView.viewType, this.treeViewProvider); + + context.subscriptions.push(this._view); + + } + + async addIntegration(integration: Integration) : Promise { + this.treeViewProvider.addIntegration(integration); + } + + async update(): Promise { + if (this.treeViewProvider) { + await this.treeViewProvider.update(); + } + } +} + +class NodeIntegrationsTreeProvider implements vscode.TreeDataProvider { + + // on change tree data + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + // on visibility change + // private _onDidChangeVisibility: vscode.EventEmitter = new vscode.EventEmitter(); + // readonly onDidChangeVisibility: vscode.Event = this._onDidChangeVisibility.event; + + private readonly integrations: Set = new Set(); + + addIntegration(integration: Integration): void { + this.integrations.add(integration); + } + + getTreeItem(id: string): vscode.TreeItem | Thenable { + let treeItem: IntegrationTreeItem | undefined; + + for (const integration of this.integrations.values()) { + treeItem = integration.getTreeItem(id); + } + + if (!treeItem) { + throw new Error(`Tree item ${id} doesn't exist.`); + } + + return treeItem; + } + + getChildren(parentId?: string | undefined): vscode.ProviderResult { + console.log(parentId); + let children: string[] = []; + for (const integration of this.integrations) { + children = children.concat(integration.getTreeItemChildren()); + } + + return children; + } + + async update(): Promise { + for (const integration of this.integrations) { + await integration.update(); + } + this._onDidChangeTreeData.fire(undefined); + } + + resolveTreeItem?(item: vscode.TreeItem, element: string, token: vscode.CancellationToken): vscode.ProviderResult { + console.log(element); + console.log(token); + return item; + } + +} \ No newline at end of file diff --git a/src/webviews/NodeViewProvider.ts b/src/webviews/NodeStatusView.ts similarity index 85% rename from src/webviews/NodeViewProvider.ts rename to src/webviews/NodeStatusView.ts index be0772e..70d1e4d 100644 --- a/src/webviews/NodeViewProvider.ts +++ b/src/webviews/NodeStatusView.ts @@ -1,20 +1,20 @@ import * as vscode from 'vscode'; import { NodeProvider } from "../nodeProvider"; -import { getUri } from "../utilities/util"; +import { Util } from "../utilities/util"; -export class NodeViewProvider implements vscode.WebviewViewProvider { +export class NodeStatusViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = "pyrsia.node"; private nodeProvider; private _view?: vscode.WebviewView; private extensionUri: vscode.Uri; - private readonly onDidConnectListeners: Set = new Set(); + private readonly onDidConnectListeners: Set = new Set(); constructor(private readonly context: vscode.ExtensionContext) { this.nodeProvider = new NodeProvider(); vscode.window.registerWebviewViewProvider( - NodeViewProvider.viewType, + NodeStatusViewProvider.viewType, this, ); @@ -25,7 +25,7 @@ export class NodeViewProvider implements vscode.WebviewViewProvider { return this._view as vscode.WebviewView; } - onDidConnect(listener: NodeViewListener): void { + onDidConnect(listener: NodeStatusViewListener): void { this.onDidConnectListeners.add(listener); } @@ -41,7 +41,7 @@ export class NodeViewProvider implements vscode.WebviewViewProvider { view.webview.html = this.getWebviewContent(view.webview, this.extensionUri); this.setWebviewMessageListener(view); - view.onDidChangeVisibility(() :void => { + view.onDidChangeVisibility((): void => { this.updateView(); }); @@ -50,7 +50,7 @@ export class NodeViewProvider implements vscode.WebviewViewProvider { private getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { - const toolkitUri = getUri(webview, extensionUri, [ + const toolkitUri = Util.getUri(webview, extensionUri, [ "node_modules", "@vscode", "webview-ui-toolkit", @@ -58,8 +58,8 @@ export class NodeViewProvider implements vscode.WebviewViewProvider { "toolkit.js", ]); - const mainUri = getUri(webview, extensionUri, ["src", "webview-ui", "main.js"]); - const stylesUri = getUri(webview, extensionUri, ["src", "webview-ui", "styles.css"]); + const mainUri = Util.getUri(webview, extensionUri, ["src", "webview-ui", "main.js"]); + const stylesUri = Util.getUri(webview, extensionUri, ["src", "webview-ui", "styles.css"]); const pyrsiaHostname = this.nodeProvider.getHostname(); return /*html*/ ` @@ -145,6 +145,6 @@ export class NodeViewProvider implements vscode.WebviewViewProvider { } } -export interface NodeViewListener { +export interface NodeStatusViewListener { onDidConnect(): void; } From 7524d38bcbe7da14010dd1058233d43350d0b893 Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Tue, 3 Jan 2023 12:57:08 -0800 Subject: [PATCH 03/16] Help view added --- .eslintrc.cjs | 79 ++++- .gitignore | 1 + .vscode/settings.json | 13 + README.md | 81 ++++- package.json | 13 +- src/extension.ts | 43 ++- src/integrations/Docker.ts | 247 +++++++++------- src/integrations/Integration.ts | 14 +- src/model/NodeConfig.ts | 37 ++- src/nodeProvider.ts | 62 ++-- src/test/runTest.ts | 5 +- src/test/suite/index.ts | 26 +- src/utilities/client.ts | 104 +++---- src/utilities/fsUtils.ts | 22 +- src/utilities/util.ts | 46 +-- src/utilities/vscodeUtils.ts | 17 -- src/webviews/HelpView.ts | 145 +++++++++ src/webviews/NodeConfigView.ts | 425 ++++++++++++--------------- src/webviews/NodeIntegrationsView.ts | 46 +-- src/webviews/NodeStatusView.ts | 199 +++++++------ 20 files changed, 941 insertions(+), 684 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 src/utilities/vscodeUtils.ts create mode 100644 src/webviews/HelpView.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 15f7a7b..afc89e6 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -10,11 +10,78 @@ module.exports = { ".eslintrc.cjs" ], rules: { - "@typescript-eslint/naming-convention": "warn", - "@typescript-eslint/semi": "warn", - "curly": "warn", - "eqeqeq": "warn", - "no-throw-literal": "warn", - "semi": "off" + "@typescript-eslint/naming-convention": "error", + "@typescript-eslint/semi": "error", + "@typescript-eslint/no-unused-vars": "error", + "eqeqeq": "error", + "no-throw-literal": "error", + "semi": "warn", + "indent": ["error", "tab", { "SwitchCase": 1 }], + "no-unused-vars": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-non-null-assertion": "error", + "@typescript-eslint/no-var-requires": "error", + "@typescript-eslint/unified-signatures": "error", + "require-await": "error", + "no-return-await": "error", + "curly": "error", + "no-invalid-this": "error", + "@typescript-eslint/no-shadow": "error", + "no-fallthrough": "error", + "no-await-in-loop": "error", + "@typescript-eslint/array-type": "error", + "no-unreachable": "error", + "no-unsafe-negation": "error", + "no-unsafe-finally": "error", + "valid-jsdoc": "warn", + "valid-typeof": "error", + "consistent-return": "error", + "default-case": "error", + "no-multi-spaces": "warn", + "no-redeclare": "error", + "no-self-assign": "error", + "no-unused-labels": "error", + "no-useless-escape": "error", + "wrap-iife": "error", + "no-delete-var": "error", + "callback-return": "error", + "no-mixed-requires": "error", + "no-new-require": "error", + "no-path-concat": "error", + "no-process-exit": "error", + "no-sync": "error", + "comma-dangle": "warn", + "comma-style": "warn", + "computed-property-spacing": "warn", + "consistent-this": "warn", + "eol-last": "warn", + "func-name-matching": "warn", + "func-style": "warn", + "function-paren-newline": "warn", + "id-length": ["error", { "min": 2 }], + "jsx-quotes": "warn", + "key-spacing": "warn", + "keyword-spacing": "warn", + "linebreak-style": "warn", + "max-depth": "warn", + "max-len": ["warn", { "code": 120, "comments": 200 }], + "max-lines": "warn", + "max-nested-callbacks": "error", + "max-params": ["warn",{ "max": 8 }], + "new-cap": "warn", + "new-parens": "warn", + "no-bitwise": "warn", + "no-lonely-if": "warn", + "no-mixed-spaces-and-tabs": "warn", + "no-multiple-empty-lines": "warn", + "object-curly-newline": "warn", + "object-curly-spacing": ["warn", "always"], + "operator-linebreak": "warn", + "semi-style": "warn", + "sort-keys": "warn", + "require-jsdoc": "warn", + "no-useless-constructor": "warn", + "prefer-destructuring": "warn", + "no-var": "error" }, }; \ No newline at end of file diff --git a/.gitignore b/.gitignore index cc4ec21..8a6a127 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules .idea out .vscode-test +*.vsix \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6c31f9d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "cSpell.words": [ + "callstack", + "codespaces", + "configurenode", + "Pyrsia", + "pyrsiaintall", + "pyrsiaoverview", + "reportissue", + "VSIX", + "webviews" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index a96c938..cd963b8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,79 @@ -# pyrsia-vscode-extension -vs code extension to be used with Pyrsia +# Pyrsia VS Code Extension + +## Overview + +Pyrsia support for Microsoft VS Code (extension).**This is an early prototype and proof of concept, it’s not "production" ready.** + +## Requirements + +- VS Code 1.7 +- Node 19.x required + +## How to run and debug the extension + +- Open the repo folder in VS Code. +- Install the dependencies and compile the extension. + + ```sh + npm install + npm run watch + ``` + +- Press F5 to run the extension, a new VS Code instance will appear and should have the extension installed. + +## How to test the project (TBD) + +```sh +npm run test +``` + +## Before merging or creating PR + +- Run the tests and linter. + + ```sh + npm run lint + npm run test + ``` + +## How to package, install and uninstall Pyrsia extension in the IDE + +### Package and Install (side-load extension) + +- Install [Visual Studio Code Extensions](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#vsce) with the following command. + + ```sh + npm install -g @vscode/vsce + ``` + +- In the repository folder compile and package the extension as follows. + + ```sh + vsce package + ``` + +- If the packaging was successful the last line of the VSCE logs should contain the `vsix` file path, for example: + + ```sh + DONE Packaged: /home/john/repositories/pyrsia-vscode-extension/pyrsia-integration-0.0.1.vsix (960 files, 2.2MB) + ``` + +- Copy the `vsix` file path and install the extension as follows. + + ```sh + code --install-extension + ``` + +### Uninstall (side-load) + +- Find the extension in the list of the extensions (look for "pyrsia", for example `undefined_publisher.pyrsia-integration`). + + ```sh + code --list-extensions + ``` + +- Use the extension name from the list to uninstall the extension. + + ```sh + code --uninstall-extension + ``` diff --git a/package.json b/package.json index ec12c16..f1afa65 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,12 @@ { "name": "pyrsia-integration", - "displayName": "Pyrsia Integration", + "displayName": "Pyrsia", "description": "Pyrsia Integration for VS Code", "version": "0.0.1", + "repository": { + "type": "git", + "url": "https://github.com/pyrsia/pyrsia-vscode-extension" + }, "engines": { "vscode": "^1.74.0" }, @@ -25,14 +29,9 @@ }, "views": { "package-explorer": [ - { - "id": "pyrsia.node", - "name": "Node Status", - "type": "webview" - }, { "id": "pyrsia.node-config", - "name": "Node Configuration" + "name": "Node Status" }, { "id": "pyrsia.node-integrations", diff --git a/src/extension.ts b/src/extension.ts index 2f56e28..4ef6595 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,10 +1,11 @@ import * as vscode from 'vscode'; import { NodeConfigView } from './webviews/NodeConfigView'; -import { NodeStatusViewProvider } from './webviews/NodeStatusView'; +// import { NodeStatusViewProvider } from './webviews/NodeStatusView'; import { NodeIntegrationsView } from './webviews/NodeIntegrationsView'; import { Util } from './utilities/util'; +import { HelpView } from './webviews/HelpView'; -export async function activate(context: vscode.ExtensionContext) { +export const activate = (context: vscode.ExtensionContext) => { console.log('Pyrsia extension activated'); @@ -12,35 +13,25 @@ export async function activate(context: vscode.ExtensionContext) { Util.init(context); // Node status web view provider - const nodeView = new NodeStatusViewProvider(context); + //new NodeStatusViewProvider(context); + // Node status config view - const nodeConfigView = new NodeConfigView(context); + new NodeConfigView(context); // Node status config view new NodeIntegrationsView(context); - //Notify the Node Config View when connected to node - nodeView.onDidConnect({ - onDidConnect() { - nodeConfigView.update(); - }, - }); - - // nodeView.onDidConnect(new NodeViewListener { - - // }); - - // const startNode = vscode.commands.registerCommand('pyrsia.startNode', (a) => { - // // nodeProvider.isNodeHealthy; - // console.log(a); - // }); + // Node status config view + new HelpView(context); - // const stopNode = vscode.commands.registerCommand('pyrsia.stopNode', () => { - // //nodeProvider.stop(); - // console.log("something"); + //Notify the Node Config View when connected to node + // nodeView.onDidConnect({ + // onDidConnect() { + // nodeConfigView.update(); + // }, // }); +}; - //context.subscriptions.push(startNode, stopNode); -} - -// export function deactivate() {} \ No newline at end of file +export const deactivate = () => { + console.log("Pyrsia extension deactivated"); // TODO +}; diff --git a/src/integrations/Docker.ts b/src/integrations/Docker.ts index ea825ff..d06cf76 100644 --- a/src/integrations/Docker.ts +++ b/src/integrations/Docker.ts @@ -6,123 +6,144 @@ import { Util } from "../utilities/util"; import * as vscode from 'vscode'; export class Docker implements Integration { - - private static readonly dockerId: string = "docker"; - private readonly treeItems: Map = new Map(); - private static confMap: Map = new Map(); - private static pyrsiaConfigCode = ` "registry-mirrors": ["http://0.0.0.0:7888"] \n`; - - static readonly commandId = "pyrsia.openUpdateDockerConfFile"; - - static { - Docker.confMap.set(path.join(os.homedir(),".docker"), "config.json"); - } - - constructor(context: vscode.ExtensionContext) { - - // docker open and update configuration editor - const openDockerUpdateConfFile = vscode.commands.registerCommand(Docker.commandId, (confFilePath: string) => { - console.log(confFilePath); - const setting: vscode.Uri = vscode.Uri.parse(`${confFilePath}`); - - vscode.workspace.openTextDocument(setting).then((textDocument: vscode.TextDocument) => { - vscode.window.showTextDocument(textDocument, 1, false).then(textEditor => { - const ls = textDocument.lineCount; - textEditor.edit(edit => { - // TODO This is a prototype, not even close to the real solution. - const line = textDocument.lineAt(ls - 3); - edit.delete(line.range); - edit.insert(new vscode.Position(line.lineNumber, 0), `${line.text},`); - edit.insert(new vscode.Position(ls - 2, 0), Docker.pyrsiaConfigCode); - }); - textEditor.selection = new vscode.Selection(new vscode.Position(2, 2), new vscode.Position(2, Docker.pyrsiaConfigCode.length + 2)); - }); - }, (error: unknown) => { - console.error(error); - }); - - }); - - context.subscriptions.push(openDockerUpdateConfFile); - } - - getTreeItem(treeItemId: string): IntegrationTreeItem | undefined { - return this.treeItems.get(treeItemId); - } - - getId(): string { - return Docker.dockerId; - } - - async update(): Promise { - // find docker conf file (macos, linux) - for (const confPath of Docker.confMap.keys()) { - const fileName = Docker.confMap.get(confPath); - if (!fileName) { - throw new Error("Configuration file name cannot be null"); - } - const confFilePath = await fsUtils.findByName(confPath, fileName); - if (confFilePath) { - const treeItemId = `${this.getId()}-${confFilePath}`; - const label = `${confFilePath}`; - this.treeItems.set(treeItemId, new DockerTreeItem(label, treeItemId, confFilePath)); - } else { - console.log(`No configuration for 'Docker' - ${path.join(confPath, fileName)}`); - } - } - } - - getTreeItemChildren(): string[] { - const children: string[] = []; - for (const label of this.treeItems.keys()) { - children.push(label); - } - - return children; - } + + static readonly commandId = "pyrsia.openUpdateDockerConfFile"; + private static readonly integrationId: string = "docker"; + private static confMap: Map = new Map(); + private static pyrsiaConfigCode = ` "registry-mirrors": ["http://0.0.0.0:7888"] \n`; + + private readonly treeItems: Map = new Map(); + + static { + Docker.confMap.set(path.join(os.homedir(), ".docker"), "config.json"); + } + + constructor(context: vscode.ExtensionContext) { + + // docker open and update configuration editor command for the docker integration + const openDockerUpdateConfFile = vscode.commands.registerCommand(Docker.commandId, (confFilePath: string) => { + console.log(confFilePath); + const setting: vscode.Uri = vscode.Uri.parse(`${confFilePath}`); + + vscode.workspace.openTextDocument(setting).then((textDocument: vscode.TextDocument) => { + vscode.window.showTextDocument(textDocument, 1, false).then(async (textEditor) => { + const confirmOption = "Yes"; + const cancelOption = "No"; + const result = await vscode.window.showInformationMessage( + "Add Pyrsia to the Docker configuration?", + confirmOption, + cancelOption + ); + + if (result === confirmOption) { + const ls = textDocument.lineCount; + textEditor.edit(edit => { + // TODO This is a prototype, not even close to the real solution. + const line = textDocument.lineAt(ls - 3); + edit.delete(line.range); + edit.insert(new vscode.Position(line.lineNumber, 0), `${line.text},`); + edit.insert(new vscode.Position(ls - 2, 0), Docker.pyrsiaConfigCode); + }); + // TODO Update the selection? + textEditor.selection = new vscode.Selection( + new vscode.Position(2, 2), + new vscode.Position(2, Docker.pyrsiaConfigCode.length + 2) + ); + textDocument.save(); + } + }); + }, (error: unknown) => { + console.error(error); + }); + + }); + + context.subscriptions.push(openDockerUpdateConfFile); + } + + getTreeItem(treeItemId: string): IntegrationTreeItem | undefined { + return this.treeItems.get(treeItemId); + } + + getId(): string { + return Docker.integrationId; + } + + update(): void { + // find docker conf file (macos, linux) + for (const confPath of Docker.confMap.keys()) { + const fileName = Docker.confMap.get(confPath); + if (!fileName) { + throw new Error("Configuration file name cannot be null"); + } + fsUtils.findByName(confPath, fileName).then((confFilePath) => { + if (confFilePath) { + const treeItemId = `${this.getId()}-${confFilePath}`; + const label = `${confFilePath}`; + this.treeItems.set(treeItemId, new DockerTreeItem(label, treeItemId, confFilePath)); + } else { + console.log(`No configuration for 'Docker' - ${path.join(confPath, fileName)}`); + } + }); + } + } + + getTreeItemChildren(): string[] { + const children: string[] = []; + for (const label of this.treeItems.keys()) { + children.push(label); + } + + return children; + } } class DockerTreeItem extends IntegrationTreeItem { - constructor( + constructor( public label: string, public readonly id: string, - public readonly confFilePath: string, + public readonly confFilePath: string ) { - super(label, id); - // const command = new class implements vscode.Command { - // title: string; - // command: string; - // tooltip?: string | undefined; - // arguments?: any[] | undefined; - // run() { - // console.log("Docker open File"); - // } - // }(); - // command.title = "Open Docker Configuration"; - // command.command = "command-open-docker-node"; - // command.tooltip = command.title; - // super.command = command; - this.command = { command: Docker.commandId, title: "Start Pyrsia", arguments: [confFilePath] }; - - } - - // command?: vscode.Command | undefined = new class implements vscode.Command { - // title: string; - // command: string; - // tooltip?: string | undefined; - // arguments?: any[] | undefined; - // run() { - // console.log("Docker open File"); - // } - // }(); - - update(): void { - throw new Error("Method not implemented."); - } - - iconPath = { - light: path.join(Util.getResourceImagePath(), "docker_small.svg"), - dark: path.join(Util.getResourceImagePath(), "docker_small.svg"), //TODO update to dark - }; -} \ No newline at end of file + super(label, id); + // const command = new class implements vscode.Command { + // title: string; + // command: string; + // tooltip?: string | undefined; + // arguments?: any[] | undefined; + // run() { + // console.log("Docker open File"); + // } + // }(); + // command.title = "Open Docker Configuration"; + // command.command = "command-open-docker-node"; + // command.tooltip = command.title; + // super.command = command; + this.command = { + arguments: [confFilePath], + command: Docker.commandId, + title: "Open Docker configuration file" + }; + + } + + // command?: vscode.Command | undefined = new class implements vscode.Command { + // title: string; + // command: string; + // tooltip?: string | undefined; + // arguments?: any[] | undefined; + // run() { + // console.log("Docker open File"); + // } + // }(); + + update(): void { + throw new Error("Method not implemented."); + } + + // eslint-disable-next-line @typescript-eslint/member-ordering + iconPath = { + dark: path.join(Util.getResourceImagePath(), "docker_small.svg"), //TODO update to dark + light: path.join(Util.getResourceImagePath(), "docker_small.svg") + }; +} diff --git a/src/integrations/Integration.ts b/src/integrations/Integration.ts index 1bca65c..29eec79 100644 --- a/src/integrations/Integration.ts +++ b/src/integrations/Integration.ts @@ -2,27 +2,27 @@ import * as vscode from 'vscode'; export interface Integration { - getTreeItem(treeItemId: string): IntegrationTreeItem | undefined; + getTreeItem(treeItemId: string): IntegrationTreeItem | undefined; - getTreeItemChildren(): string[]; + getTreeItemChildren(): string[]; - getId(): string; + getId(): string; - update(): void; + update(): void; } export abstract class IntegrationTreeItem extends vscode.TreeItem { - constructor( + constructor( public label: string, - public readonly id: string, + public readonly id: string ) { super(label, vscode.TreeItemCollapsibleState.None); this.tooltip = this.label; } - abstract update(): void; + abstract update(): void; } diff --git a/src/model/NodeConfig.ts b/src/model/NodeConfig.ts index 4041289..953ae36 100644 --- a/src/model/NodeConfig.ts +++ b/src/model/NodeConfig.ts @@ -1,28 +1,27 @@ -export class NodeConfig { - private static readonly defaultHostname = "localhost"; - private static readonly defaultPort = "7888"; - private _hostname: string; - private _port: string; +import { URL } from "url"; - constructor(hostname: string = NodeConfig.defaultHostname, port: string = NodeConfig.defaultPort) { - this._hostname = hostname; - this._port = port; - } - - get hostname(): string { - return this._hostname; +export class NodeConfig { + private static readonly defaultNodeUrl = new URL("localhost:7888"); + private nodeUrl: URL; + + constructor(nodeUrl?: URL) { + this.nodeUrl = nodeUrl || NodeConfig.defaultNodeUrl; } - set hostname(hostname: string) { - this._hostname = hostname; + public get host(): string { + return this.nodeUrl.href; } - get port(): string { - return this._port; + public get url(): URL { + return this.nodeUrl; } - set port(port: string) { - this._port = port; + public set url(nodeUrl: URL | string | undefined) { + if (typeof nodeUrl === "string" ) { + this.nodeUrl = new URL(nodeUrl); + } else { + this.nodeUrl = nodeUrl || NodeConfig.defaultNodeUrl; + } } -} \ No newline at end of file +} diff --git a/src/nodeProvider.ts b/src/nodeProvider.ts index d93de5d..2fa7108 100644 --- a/src/nodeProvider.ts +++ b/src/nodeProvider.ts @@ -3,34 +3,34 @@ import * as client from './utilities/client'; export class NodeProvider { - nodeProcess: cp.ChildProcess; - pid: number | undefined; - - async isNodeHealthy() : Promise { - return await client.isNodeHealthy(); - } - - getHostname() : string { - return client.getNodeUrl(); - } - - async getStatus() : Promise { - return await client.getStatus(); - } - - // async start() { - - // this.pid = this.nodeProcess.pid; - // console.log(this.pid); - // vscode.window.showInformationMessage('Pyrsia Node Started'); - // if (!await client.isNodeHealth()) { - - // } - // } - - // stop() { - // this.nodeProcess.kill(); - // console.log(this.pid); - // vscode.window.showInformationMessage('Pyrsia Node Stopped'); - // } -} \ No newline at end of file + nodeProcess: cp.ChildProcess; + pid: number | undefined; + + isNodeHealthy(): Promise { + return client.isNodeHealthy(); + } + + getHostname(): string { + return client.getNodeUrl(); + } + + getStatus(): Promise { + return client.getStatus(); + } + + // async start() { + + // this.pid = this.nodeProcess.pid; + // console.log(this.pid); + // vscode.window.showInformationMessage('Pyrsia Node Started'); + // if (!await client.isNodeHealth()) { + + // } + // } + + // stop() { + // this.nodeProcess.kill(); + // console.log(this.pid); + // vscode.window.showInformationMessage('Pyrsia Node Stopped'); + // } +} diff --git a/src/test/runTest.ts b/src/test/runTest.ts index 27b3ceb..be466d0 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import { runTests } from '@vscode/test-electron'; -async function main() { +const main = async () => { try { // The folder containing the Extension Manifest package.json // Passed to `--extensionDevelopmentPath` @@ -16,8 +16,9 @@ async function main() { await runTests({ extensionDevelopmentPath, extensionTestsPath }); } catch (err) { console.error('Failed to run tests'); + // eslint-disable-next-line no-process-exit process.exit(1); } -} +}; main(); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index 7029e38..66acd05 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -2,37 +2,39 @@ import * as path from 'path'; import * as Mocha from 'mocha'; import * as glob from 'glob'; -export function run(): Promise { +export const run = (): Promise => { // Create the mocha test const mocha = new Mocha({ - ui: 'tdd', - color: true + color: true, + ui: 'tdd' }); const testsRoot = path.resolve(__dirname, '..'); - return new Promise((c, e) => { + return new Promise((callback, er) => { glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { if (err) { - return e(err); + return er(err); } // Add files to the test suite - files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); + files.forEach(file => mocha.addFile(path.resolve(testsRoot, file))); try { // Run the mocha test mocha.run(failures => { if (failures > 0) { - e(new Error(`${failures} tests failed.`)); + er(new Error(`${failures} tests failed.`)); } else { - c(); + // eslint-disable-next-line callback-return + callback(); } }); - } catch (err) { - console.error(err); - e(err); + } catch (error) { + console.error(error); + er(error); } + return null; }); }); -} +}; diff --git a/src/utilities/client.ts b/src/utilities/client.ts index 48b9ab5..f032f57 100644 --- a/src/utilities/client.ts +++ b/src/utilities/client.ts @@ -1,69 +1,69 @@ import axios from "axios"; import { Util } from "./util"; -// TODO This should be obtained from the pyrsia node config -export function getNodeUrl(): string { - return `http://${Util.getNodeConfig().hostname}:${getNodePort()}`; -} +export const getNodeUrl = ( ): string => { + let nodeUrl = Util.getNodeConfig().host; -// TODO This should be obtained from the pyrsia node config -function getNodePort(): string { - return Util.getNodeConfig().port; -} + if (!nodeUrl.toLowerCase().startsWith("http")) { + nodeUrl = `http://${nodeUrl}`; + } + + return nodeUrl; +}; type PingResponse = { - data: string[]; + data: string[]; }; type StatusResponse = { - data: string[]; + data: string[]; }; -export async function isNodeHealthy(): Promise { - console.log('Check node health'); - const nodeUrl = `${getNodeUrl()}/v2`; - let status; - try { - ({ status } = await axios.get( - nodeUrl, - { - headers: { - accept: 'application/json', - }, - }, - )); - } catch (e) { - console.error(e); - } +export const isNodeHealthy = async (): Promise => { + console.log('Check node health'); + const nodeUrl = `${getNodeUrl()}/v2`; + let status; + try { + ({ status } = await axios.get( + nodeUrl, + { + headers: { + accept: 'application/json' + } + } + )); + } catch (error) { + console.error(error); + } - return status === 200; -} + return status === 200; +}; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function getStatus(): Promise { - console.log('Get node status'); - const nodeUrl = `${getNodeUrl()}/status`; - let data; +export const getStatus = async (): Promise => { + console.log('Get node status'); + const nodeUrl = `${getNodeUrl()}/status`; + let data; - try { - ({ data } = await axios.get( - nodeUrl, - { - headers: { - accept: 'application/json', - }, - }, - )); - } catch (e) { - console.error(e); - } + try { + ({ data } = await axios.get( + nodeUrl, + { + headers: { + accept: 'application/json' + } + } + )); + } catch (error) { + console.error(error); + } - return data; -} + return data; +}; + +export const getPeers = async (): Promise => { + console.log('Get node peers'); + const data = await getStatus(); -export async function getPeers(): Promise { - console.log('Get node peers'); - const data = await getStatus(); - - return data ? data.peers_count : "0"; -} \ No newline at end of file + return data ? data.peers_count : "0"; +}; diff --git a/src/utilities/fsUtils.ts b/src/utilities/fsUtils.ts index 9b352ba..9e4a501 100644 --- a/src/utilities/fsUtils.ts +++ b/src/utilities/fsUtils.ts @@ -1,17 +1,17 @@ import { readdir } from "fs/promises"; import * as path from "path"; -export async function findByName(dir: string, fileName: string): Promise { - let matchedFile: string | undefined = undefined; +export const findByName = async (dir: string, fileName: string): Promise => { + let matchedFile: string | undefined = undefined; - const dirFileNames = await readdir(dir); + const dirFileNames = await readdir(dir); - for (const dirFileName of dirFileNames) { - if (dirFileName === fileName) { - matchedFile = path.join(dir, dirFileName); - break; - } - } + for (const dirFileName of dirFileNames) { + if (dirFileName === fileName) { + matchedFile = path.join(dir, dirFileName); + break; + } + } - return matchedFile; -} + return matchedFile; +}; diff --git a/src/utilities/util.ts b/src/utilities/util.ts index e0f9d8a..224ed47 100644 --- a/src/utilities/util.ts +++ b/src/utilities/util.ts @@ -5,28 +5,28 @@ import * as vscode from 'vscode'; export class Util { - private static resourcePath: string; - - static init(context: vscode.ExtensionContext): void { - Util.resourcePath = context.asAbsolutePath(path.join('resources')); - } - - static getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { - return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); - } - - // TODO This should be obtained from the pyrsia node config - static getNodeConfig(): NodeConfig { - return new NodeConfig(); // TODO Replace it with the configuration obtained from the ide cache/storage - } - - static getResourcePath(): string { - return Util.resourcePath; - } - - static getResourceImagePath(): string { - return path.join(Util.resourcePath, "images"); - } -} + private static resourcePath: string; + // TODO Replace it with real, persistance storage + private static config: NodeConfig = new NodeConfig(); + + static init(context: vscode.ExtensionContext): void { + Util.resourcePath = context.asAbsolutePath(path.join('resources')); + } + + static getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { + return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); + } + static getNodeConfig(): NodeConfig { + return this.config; // TODO Replace it with the configuration obtained from the ide cache/storage + } + + static getResourcePath(): string { + return Util.resourcePath; + } + + static getResourceImagePath(): string { + return path.join(Util.resourcePath, "images"); + } +} diff --git a/src/utilities/vscodeUtils.ts b/src/utilities/vscodeUtils.ts deleted file mode 100644 index 0a715ca..0000000 --- a/src/utilities/vscodeUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as vscode from 'vscode'; - -export function openFileInEditor(filePath: string) : void { - - const setting: vscode.Uri = vscode.Uri.parse(`${filePath}`); - - vscode.workspace.openTextDocument(setting).then((a: vscode.TextDocument) => { - vscode.window.showTextDocument(a, 1, false).then(e => { - e.edit(edit => { - edit.insert(new vscode.Position(0, 0), "\\ TODO Pyrsia node config json"); - }); - }); - }, (error: unknown) => { - console.error(error); - }); - -} \ No newline at end of file diff --git a/src/webviews/HelpView.ts b/src/webviews/HelpView.ts new file mode 100644 index 0000000..a52b2d4 --- /dev/null +++ b/src/webviews/HelpView.ts @@ -0,0 +1,145 @@ +// https://github.com/xojs/eslint-config-xo-typescript/issues/43 +/* eslint-disable @typescript-eslint/naming-convention */ +import * as vscode from 'vscode'; + +// Help list +enum HelpProperty { + Install = "install", + Tutorials = "tutorials", + Overview = "overview", + Github = "github", + Issue = "issue", +} + +export class HelpUtil { + public static readonly helpCommandId = "pyrsia.openHelpLink"; // NOI18 + public static readonly quickStartUrl = "https://pyrsia.io/docs/tutorials/quick-installation/"; // NOI18 +} + +export class HelpView { + private static readonly viewType: string = "pyrsia.help"; // NOI18 + private readonly treeViewProvider: HelpTreeProvider; + + + constructor(context: vscode.ExtensionContext) { + this.treeViewProvider = new HelpTreeProvider(); + const view = vscode.window.createTreeView( + HelpView.viewType, + { showCollapseAll: true, treeDataProvider: this.treeViewProvider } + ); + vscode.window.registerTreeDataProvider(HelpView.viewType, this.treeViewProvider); + + // create the open external help link command + const openHelpLink = vscode.commands.registerCommand(HelpUtil.helpCommandId, (helpUrl: string) => { + console.log(`Open ${helpUrl} using '${HelpUtil.helpCommandId}'`); // NOI18 + vscode.env.openExternal(vscode.Uri.parse(helpUrl)); + }); + // register the open external help link command + context.subscriptions.push(openHelpLink); + + // register the help view + context.subscriptions.push(view); + } +} + +class HelpTreeProvider implements vscode.TreeDataProvider { + + private treeItems: Map = new Map(); + + getTreeItem(id: string): vscode.TreeItem | Thenable { + const treeItem = this.treeItems.get(id); + if (!treeItem) { + throw new Error(`Tree item ${id} doesn't exist.`); + } + + return treeItem; + } + + getChildren(parentId?: string | undefined): vscode.ProviderResult { + let children: string[] = []; + if (!parentId) { // Create all tree Items for the tree + for (const nodeProperty in HelpProperty) { // TODO Why nodeProperty is 'string' type? Investigate + const treeItem = this.treeItems.get(nodeProperty.toLowerCase()); + if (!treeItem) { + // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? + const enumType = HelpProperty[nodeProperty as keyof typeof HelpProperty]; + // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? + this.treeItems.set(nodeProperty.toLocaleLowerCase(), HelpTreeItem.create(enumType)); + } + } + children = [... this.treeItems].map(([, value]) => { + return value.id; + }).filter(value => value !== ""); + } else { + const childId = HelpTreeItem.getChildrenId(parentId); + const treeItem: HelpTreeItem = this.treeItems.get(childId) as HelpTreeItem; + children = [treeItem.id]; + } + + return children; + } +} + +class HelpTreeItem extends vscode.TreeItem { + + private static readonly properties = { + [HelpProperty.Install.toLowerCase()]: { + iconPath: new vscode.ThemeIcon("getting-started-beginner"), // NOI18 + id: "install", // NOI18 + name: "Pyrsia Quick Installation", + url: HelpUtil.quickStartUrl + }, + [HelpProperty.Overview.toLowerCase()]: { + iconPath: new vscode.ThemeIcon("open-editors-view-icon"), // NOI18 + id: "overview", // NOI18 + name: "Read Pyrsia Documentation", + url: "https://pyrsia.io/docs/" // NOI18 + }, + [HelpProperty.Tutorials.toLowerCase()]: { + iconPath: new vscode.ThemeIcon("play-circle"), // NOI18 + id: "tutorials", // NOI18 + name: "Watch Pyrsia Tutorials", + url: "https://www.youtube.com/@pyrsiaoss/playlists" // NOI18 + }, + [HelpProperty.Github.toLowerCase()]: { + iconPath: new vscode.ThemeIcon("github"), // NOI18 + id: "github", // NOI18 + name: "Get Involved", + url: "https://github.com/pyrsia" // NOI18 + }, + [HelpProperty.Issue.toLowerCase()]: { + iconPath: new vscode.ThemeIcon("remote-explorer-report-issues"), // NOI18 + id: "issue", // NOI18 + name: "Report Issue", + url: "https://github.com/pyrsia/pyrsia/issues" // NOI18 + } + }; + + constructor( + public label: string, + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + public readonly id: string, + public readonly iconPath: vscode.ThemeIcon, + readonly helpUrl: string + ) { + super(label, collapsibleState); + this.command = { arguments: [helpUrl], command: HelpUtil.helpCommandId, title: "Open Pyrsia Help" }; + this.tooltip = this.label; + } + + static create(nodeProperty: HelpProperty): HelpTreeItem { + const property = this.properties[nodeProperty]; + const collapsibleState = vscode.TreeItemCollapsibleState.None; + return new HelpTreeItem( + property.name, + collapsibleState, + property.id, + property.iconPath, + property.url + ); + } + + static getChildrenId(parentId: string) { + return `${parentId}value`; + } +} diff --git a/src/webviews/NodeConfigView.ts b/src/webviews/NodeConfigView.ts index 0244877..cf40190 100644 --- a/src/webviews/NodeConfigView.ts +++ b/src/webviews/NodeConfigView.ts @@ -3,225 +3,146 @@ import * as vscode from 'vscode'; import { Util } from '../utilities/util'; import * as client from '../utilities/client'; +import { HelpUtil } from './HelpView'; // TODO With branches // enum NodeConfigProperty { -// Hostname = "hostname", // NO I18 -// Port = "port", // NO I18 -// HostnameValue = "hostnamevalue", // NO I18 -// PortValue = "portvalue", // NO I18 -// Peers = "peers", // NO I18 -// PeersValue = "peersvalue", // NO I18 +// Hostname = "hostname", // NOI18 +// Port = "port", // NOI18 +// HostnameValue = "hostnamevalue", // NOI18 +// PortValue = "portvalue", // NOI18 +// Peers = "peers", // NOI18 +// PeersValue = "peersvalue", // NOI18 // } // TODO Without branches enum NodeConfigProperty { - Hostname = "hostname", // NO I18 - Port = "port", // NO I18 - // HostnameValue = "hostnamevalue", // NO I18 - // PortValue = "portvalue", // NO I18 - Peers = "peers", // NO I18 - // PeersValue = "peersvalue", // NO I18 + Status = "status", + // Hostname = "hostname", // NOI18 + // Port = "port", // NOI18 + // HostnameValue = "hostnamevalue", // NOI18 + // PortValue = "portvalue", // NOI18 + Peers = "peers", // NOI18 + Error1 = "error1", + Error2 = "error2", + // PeersValue = "peersvalue", // NOI18 } export class NodeConfigView { - private static readonly viewType: string = "pyrsia.node-config"; // NO I18 + public static readonly configNodeCommandId = "pyrsia.configurenode"; + + private static readonly viewType: string = "pyrsia.node-config"; // NOI18 private readonly treeViewProvider: NodeConfigTreeProvider; + private readonly view; constructor(context: vscode.ExtensionContext) { this.treeViewProvider = new NodeConfigTreeProvider(); - const view = vscode.window.createTreeView(NodeConfigView.viewType, { treeDataProvider: this.treeViewProvider, showCollapseAll: true }); + this.view = vscode.window.createTreeView( + NodeConfigView.viewType, + { showCollapseAll: true, treeDataProvider: this.treeViewProvider } + ); vscode.window.registerTreeDataProvider(NodeConfigView.viewType, this.treeViewProvider); - context.subscriptions.push(view); + context.subscriptions.push(this.view); vscode.commands.registerCommand('pyrsia.node-config.tree.refresh', () => { this.treeViewProvider.update(); }); - view.onDidChangeVisibility(() => { + this.view.onDidChangeVisibility(() => { this.treeViewProvider.update(); }); + + // docker open and update configuration editor command for the docker integration + const configureNodeCommand = vscode.commands.registerCommand( + NodeConfigView.configNodeCommandId, + async () => { + const options: vscode.InputBoxOptions = { + prompt: "Update the Pyrsia node address (e.g. localhost:7888)", + validateInput(value) { + let errorMessage: string | undefined; + console.info(`Node configuration input: ${value}`); + if (!value.toLocaleLowerCase().startsWith("http")) { + value = `http://${value}`; + } + try { + new URL(value); + } catch (error) { + errorMessage = + "Incorrect Pyrsia node address, please provide a correct address (e.g localhost:7888)"; + } + + return errorMessage; + }, + value: Util.getNodeConfig().host + }; + + // show the input box so user can provide a new node address + const newNodeAddress: string | undefined = await vscode.window.showInputBox(options); + Util.getNodeConfig().url = newNodeAddress; + + // update the UI + this.update(); + } + ); + + context.subscriptions.push(configureNodeCommand); + + // trigger data and UI updates for the first time + setTimeout(() => { + this.update(); + }, 1000); + + // update the UI every minute + setInterval(() => { + this.update(); + }, 60000); } public update(): void { this.treeViewProvider.update(); + client.isNodeHealthy().then((healthy) => { + healthy ? this.view.title = "NODE STATUS 🟩" : this.view.title = "NODE STATUS 🟥"; + }); } } - -// TODO This is the tree provider which returns branches -// class NodeConfigTreeProvider implements vscode.TreeDataProvider { - -// private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); -// readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - -// private treeItems: Map = new Map(); - -// async update() { -// for (const nodeProperty in NodeConfigProperty) { // TODO Why nodeProperty is 'string' type? Investigate -// const treeItem = this.treeItems.get(nodeProperty.toLocaleLowerCase()); -// if (treeItem) { -// await treeItem.update(); -// } -// } - -// this._onDidChangeTreeData.fire(); -// } - -// getTreeItem(id: string): vscode.TreeItem | Thenable { -// const treeItem = this.treeItems.get(id); -// if (!treeItem) { -// throw new Error(`Tree item ${id} doesn't exist.`); -// } - -// return treeItem; -// } - -// getChildren(parentId?: string | undefined): vscode.ProviderResult { -// let childrenArray: string[] = []; -// if (!parentId) { // Create all tree Items for the tree -// for (const nodeProperty in NodeConfigProperty) { // TODO Why nodeProperty is 'string' type? Investigate -// const treeItem = this.treeItems.get(nodeProperty.toLowerCase()); -// if (!treeItem) { -// const enumType = NodeConfigProperty[nodeProperty as keyof typeof NodeConfigProperty]; // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? -// this.treeItems.set(nodeProperty.toLocaleLowerCase(), NodeTreeItem.create(enumType)); // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? -// } -// } -// childrenArray = [... this.treeItems].map(([, value]) => { -// return value.isRoot() ? value.id : ""; -// }).filter(value => value !== ""); -// } else { // not tree root then get the particular id for the parentId -// const childId = NodeTreeItem.getChildrenId(parentId); -// const treeItem: NodeTreeItem = this.treeItems.get(childId) as NodeTreeItem; -// childrenArray = [treeItem.id]; -// } - -// return childrenArray; -// } -// } - -// class NodeTreeItem extends vscode.TreeItem { - -// constructor( -// public label: string, -// public readonly collapsibleState: vscode.TreeItemCollapsibleState, -// public readonly id: string, -// public readonly root: boolean, -// private readonly listener: NodeConfigListener, -// ) { -// super(label, collapsibleState); -// this.tooltip = this.label; -// } - -// static create(nodeProperty: NodeConfigProperty): NodeTreeItem { -// const property = this.properties[nodeProperty]; -// const collapsibleState = property.root ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None; -// return new NodeTreeItem( -// property.name, -// collapsibleState, -// property.id, -// property.root, -// property.listener, -// ); -// } - -// async update() { -// await this.listener.onUpdate(this); -// } - -// isRoot(): boolean { -// return this.root; -// } - -// static getChildrenId(parentId: string) { -// return `${parentId}value`; -// } - -// private static readonly properties = { -// [NodeConfigProperty.Hostname.toLowerCase()]: { -// name: "Hostname", -// id: "hostname", // NO I18 -// root: true, -// listener: { -// onUpdate: async () => { -// console.log(`Nothing to update - ${this.name}`); -// } -// }, -// }, [NodeConfigProperty.HostnameValue.toLowerCase()]: { -// name: "Getting node hostname...", -// id: "hostnamevalue", // NO I18 -// root: false, -// listener: { -// onUpdate: async (treeItem: NodeTreeItem) => { -// treeItem.label = util.getNodeConfig().hostname; -// } -// }, -// }, -// [NodeConfigProperty.Port.toLowerCase()]: { -// name: "Port", -// id: "port", // NO I18 -// root: true, -// listener: { -// onUpdate: async () => { -// console.log(`Nothing to update - ${this.name}`); -// } -// }, -// }, -// [NodeConfigProperty.PortValue.toLowerCase()]: { -// name: "Getting node port...", -// id: "portvalue", // NO I18 -// root: false, -// listener: { -// onUpdate: async (treeItem: NodeTreeItem) => { -// treeItem.label = util.getNodeConfig().port; -// } -// }, -// }, -// [NodeConfigProperty.Peers.toLowerCase()]: { -// name: "Peers Count", -// id: "peers", // NO I18 -// root: true, -// listener: { -// onUpdate: async () => { -// console.log(`Nothing to update - ${this.name}`); -// } -// }, -// }, -// [NodeConfigProperty.PeersValue.toLowerCase()]: { -// name: "Getting node peers...", -// id: "peersvalue", // NO I18 -// root: false, -// listener: { -// onUpdate: async (treeItem: NodeTreeItem) => { -// const peers = await client.getPeers(); -// treeItem.label = peers.toString(); -// treeItem.tooltip = peers.toString(); -// console.log(`Updating - ${this.name}`); -// } -// }, -// }, -// }; -// } - - // TODO This is flat tree (no branches) class NodeConfigTreeProvider implements vscode.TreeDataProvider { - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + private _onDidChangeTreeData: vscode.EventEmitter = + new vscode.EventEmitter(); + + // eslint-disable-next-line @typescript-eslint/member-ordering readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - private treeItems: Map = new Map(); + private readonly treeItems: Map; + + constructor() { + this.treeItems = new Map(); + for (const nodeProperty in NodeConfigProperty) { // TODO Why nodeProperty is 'string' type? Investigate + const treeItem = this.treeItems.get(nodeProperty.toLowerCase()); + if (!treeItem) { + // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? + const enumType = NodeConfigProperty[nodeProperty as keyof typeof NodeConfigProperty]; + // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? + this.treeItems.set(nodeProperty.toLocaleLowerCase(), NodeTreeItem.create(enumType)); + } + } + } - async update() { + update() { for (const nodeProperty in NodeConfigProperty) { // TODO Why nodeProperty is 'string' type? Investigate const treeItem = this.treeItems.get(nodeProperty.toLocaleLowerCase()); if (treeItem) { - await treeItem.update(); + treeItem.update(); } } - this._onDidChangeTreeData.fire(); + // refresh the tree + setTimeout(() => { + this._onDidChangeTreeData.fire(); + }, 1000); } getTreeItem(id: string): vscode.TreeItem | Thenable { @@ -236,13 +157,6 @@ class NodeConfigTreeProvider implements vscode.TreeDataProvider { getChildren(parentId?: string | undefined): vscode.ProviderResult { let children: string[] = []; if (!parentId) { // Create all tree Items for the tree - for (const nodeProperty in NodeConfigProperty) { // TODO Why nodeProperty is 'string' type? Investigate - const treeItem = this.treeItems.get(nodeProperty.toLowerCase()); - if (!treeItem) { - const enumType = NodeConfigProperty[nodeProperty as keyof typeof NodeConfigProperty]; // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? - this.treeItems.set(nodeProperty.toLocaleLowerCase(), NodeTreeItem.create(enumType)); // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? - } - } children = [... this.treeItems].map(([, value]) => { return value.isRoot() ? value.id : ""; }).filter(value => value !== ""); @@ -258,13 +172,93 @@ class NodeConfigTreeProvider implements vscode.TreeDataProvider { class NodeTreeItem extends vscode.TreeItem { + // reusable icons + private static readonly emptyIcon = new vscode.ThemeIcon("non-icon"); + private static readonly rightArrowIcon = new vscode.ThemeIcon("arrow-right"); + private static readonly cloudIcon = new vscode.ThemeIcon("cloud"); + private static readonly brokenConnectionIcon = new vscode.ThemeIcon("alert"); + private static readonly peersCountIcon = new vscode.ThemeIcon("extensions-install-count"); + private static readonly properties = { + [NodeConfigProperty.Status.toLowerCase()]: { + iconPath: NodeTreeItem.cloudIcon, + id: "status", // NOI18 + listener: { + onUpdate: async (treeItem: NodeTreeItem) => { + const healthy: boolean = await client.isNodeHealthy(); + const { host } = Util.getNodeConfig(); + const status: string = healthy ? `Connected to '${host}'` : `Failed connecting to '${host}'`; + treeItem.label = status; + treeItem.iconPath = healthy ? NodeTreeItem.cloudIcon : NodeTreeItem.brokenConnectionIcon; + treeItem.command = { command: NodeConfigView.configNodeCommandId, title: "Configure Pyrsia Node" }; + } + }, + name: "Status", + root: true + }, + [NodeConfigProperty.Peers.toLowerCase()]: { + iconPath: NodeTreeItem.peersCountIcon, + id: "peers", // NOI18 + listener: { + onUpdate: async (treeItem: NodeTreeItem) => { + const health = await client.isNodeHealthy(); + if (health) { + const peers = await client.getPeers(); + const { name } = NodeTreeItem.properties[NodeConfigProperty.Peers.toLowerCase()]; + treeItem.label = `${name}: ${peers.toString()}`; + treeItem.iconPath = NodeTreeItem.peersCountIcon; + } else { // don't show the item content + treeItem.label = ""; + treeItem.iconPath = NodeTreeItem.emptyIcon; + } + } + }, + name: "Node peers", + root: true + }, + [NodeConfigProperty.Error1.toLowerCase()]: { + iconPath: NodeTreeItem.emptyIcon, + id: "error1", // NOI18 + listener: { + onUpdate: async (treeItem: NodeTreeItem) => { + const healthy: boolean = await client.isNodeHealthy(); + treeItem.label = healthy ? "" : "👋 Read how to install and configure Pyrsia"; + const iconPath = healthy ? NodeTreeItem.emptyIcon : NodeTreeItem.rightArrowIcon; + treeItem.iconPath = iconPath; + treeItem.command = healthy ? undefined : { + arguments: [HelpUtil.quickStartUrl], + command: HelpUtil.helpCommandId, + title: "Open Pyrsia Help" + }; + } + }, + name: "", + root: true + }, + [NodeConfigProperty.Error2.toLowerCase()]: { + iconPath: NodeTreeItem.emptyIcon, + id: "error2", // NOI18 + listener: { + onUpdate: async (treeItem: NodeTreeItem) => { + const healthy: boolean = await client.isNodeHealthy(); + treeItem.label = healthy ? "" : "👋 Update Pyrsia node configuration"; + treeItem.command = healthy ? undefined : { + command: NodeConfigView.configNodeCommandId, + title: "Configure Pyrsia Node" + }; + } + }, + name: "", + root: true + } + }; + constructor( public label: string, public readonly collapsibleState: vscode.TreeItemCollapsibleState, public readonly id: string, public readonly root: boolean, private readonly listener: NodeConfigListener, - public readonly iconPath: vscode.ThemeIcon, + public iconPath: vscode.ThemeIcon ) { super(label, collapsibleState); this.tooltip = this.label; @@ -274,71 +268,28 @@ class NodeTreeItem extends vscode.TreeItem { const property = this.properties[nodeProperty]; const collapsibleState = vscode.TreeItemCollapsibleState.None; return new NodeTreeItem( - property.name, + "Connecting to Pyrsia...", collapsibleState, property.id, property.root, property.listener, - property.iconPath, + property.iconPath ); } - async update() { - await this.listener.onUpdate(this); + static getChildrenId(parentId: string) { + return `${parentId}value`; } - isRoot(): boolean { - return this.root; + update() { + this.listener.onUpdate(this); } - static getChildrenId(parentId: string) { - return `${parentId}value`; + isRoot(): boolean { + return this.root; } - - private static readonly properties = { - [NodeConfigProperty.Hostname.toLowerCase()]: { - name: "Node", - id: "hostname", // NO I18 - root: true, - listener: { - onUpdate: async (treeItem: NodeTreeItem) => { - const name = NodeTreeItem.properties[NodeConfigProperty.Hostname.toLowerCase()].name; - treeItem.label = `${name}: ${Util.getNodeConfig().hostname}`; - } - }, - iconPath: new vscode.ThemeIcon("cloud"), - }, - [NodeConfigProperty.Port.toLowerCase()]: { - name: "Port", - id: "port", // NO I18 - root: true, - listener: { - onUpdate: async (treeItem: NodeTreeItem) => { - const name = NodeTreeItem.properties[NodeConfigProperty.Port.toLowerCase()].name; - treeItem.label = `${name}: ${Util.getNodeConfig().port}`; - } - }, - iconPath: new vscode.ThemeIcon("ports-view-icon"), - }, - [NodeConfigProperty.Peers.toLowerCase()]: { - name: "Peers Count", - id: "peers", // NO I18 - root: true, - listener: { - onUpdate: async (treeItem: NodeTreeItem) => { - const peers = await client.getPeers(); - const name = NodeTreeItem.properties[NodeConfigProperty.Peers.toLowerCase()].name; - treeItem.label = `${name}: ${peers.toString()}`; - treeItem.tooltip = `${name}: ${peers.toString()}`; - console.log(`Updating - ${name}`); - } - }, - iconPath: new vscode.ThemeIcon("extensions-install-count"), - }, - }; } - interface NodeConfigListener { onUpdate(treeItem: NodeTreeItem): void; -} \ No newline at end of file +} diff --git a/src/webviews/NodeIntegrationsView.ts b/src/webviews/NodeIntegrationsView.ts index 6b0ae1e..afe3878 100644 --- a/src/webviews/NodeIntegrationsView.ts +++ b/src/webviews/NodeIntegrationsView.ts @@ -3,34 +3,33 @@ import * as vscode from 'vscode'; import { Integration, IntegrationTreeItem } from '../integrations/Integration'; import { Docker } from '../integrations/Docker'; -import path = require('path'); -import { Util } from '../utilities/util'; export class NodeIntegrationsView { - private static readonly viewType: string = "pyrsia.node-integrations"; // NO I18 + private static readonly viewType: string = "pyrsia.node-integrations"; // NOI18 private readonly treeViewProvider: NodeIntegrationsTreeProvider; private readonly _view?: vscode.TreeView; constructor(context: vscode.ExtensionContext) { - const test = path.join(Util.getResourcePath(),"docker_small.svg"); - console.log(test); this.treeViewProvider = new NodeIntegrationsTreeProvider(); - + // add and update the Docker integration this.treeViewProvider.addIntegration(new Docker(context)); - - this._view = vscode.window.createTreeView(NodeIntegrationsView.viewType, { treeDataProvider: this.treeViewProvider, showCollapseAll: true }); - + this._view = vscode.window.createTreeView( + NodeIntegrationsView.viewType, + { showCollapseAll: true, treeDataProvider: this.treeViewProvider } + ); + + this.treeViewProvider.update(); - vscode.commands.registerCommand('pyrsia.node-integrations.tree.refresh', async () => { + vscode.commands.registerCommand('pyrsia.node-integrations.tree.refresh', () => { this.treeViewProvider.update(); }); - this._view.onDidChangeVisibility(async () => { + this._view.onDidChangeVisibility(() => { this.treeViewProvider.update(); }); @@ -46,10 +45,10 @@ export class NodeIntegrationsView { //vscode.window.registerTreeDataProvider(NodeIntegrationsView.viewType, this.treeViewProvider); context.subscriptions.push(this._view); - + } - async addIntegration(integration: Integration) : Promise { + addIntegration(integration: Integration): void { this.treeViewProvider.addIntegration(integration); } @@ -63,9 +62,11 @@ export class NodeIntegrationsView { class NodeIntegrationsTreeProvider implements vscode.TreeDataProvider { // on change tree data - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + private _onDidChangeTreeData: vscode.EventEmitter = + new vscode.EventEmitter(); + // eslint-disable-next-line @typescript-eslint/member-ordering readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - + // on visibility change // private _onDidChangeVisibility: vscode.EventEmitter = new vscode.EventEmitter(); // readonly onDidChangeVisibility: vscode.Event = this._onDidChangeVisibility.event; @@ -100,17 +101,20 @@ class NodeIntegrationsTreeProvider implements vscode.TreeDataProvider { return children; } - async update(): Promise { + update(): void { for (const integration of this.integrations) { - await integration.update(); + integration.update(); } this._onDidChangeTreeData.fire(undefined); } - - resolveTreeItem?(item: vscode.TreeItem, element: string, token: vscode.CancellationToken): vscode.ProviderResult { + + resolveTreeItem?( + item: vscode.TreeItem, + element: string, + token: vscode.CancellationToken + ): vscode.ProviderResult { console.log(element); console.log(token); return item; } - -} \ No newline at end of file +} diff --git a/src/webviews/NodeStatusView.ts b/src/webviews/NodeStatusView.ts index 70d1e4d..57896dc 100644 --- a/src/webviews/NodeStatusView.ts +++ b/src/webviews/NodeStatusView.ts @@ -3,66 +3,73 @@ import { NodeProvider } from "../nodeProvider"; import { Util } from "../utilities/util"; export class NodeStatusViewProvider implements vscode.WebviewViewProvider { - public static readonly viewType = "pyrsia.node"; + public static readonly viewType = "pyrsia.node"; - private nodeProvider; - private _view?: vscode.WebviewView; - private extensionUri: vscode.Uri; - private readonly onDidConnectListeners: Set = new Set(); + private nodeProvider; + private _view?: vscode.WebviewView; + private extensionUri: vscode.Uri; + private readonly onDidConnectListeners: Set = new Set(); - constructor(private readonly context: vscode.ExtensionContext) { - this.nodeProvider = new NodeProvider(); + constructor(private readonly context: vscode.ExtensionContext) { + this.nodeProvider = new NodeProvider(); - vscode.window.registerWebviewViewProvider( - NodeStatusViewProvider.viewType, - this, - ); + vscode.window.registerWebviewViewProvider( + NodeStatusViewProvider.viewType, + this + ); - this.extensionUri = context.extensionUri; - } + this.extensionUri = context.extensionUri; + } - public getWebView(): vscode.WebviewView { - return this._view as vscode.WebviewView; - } + public getWebView(): vscode.WebviewView { + return this._view as vscode.WebviewView; + } - onDidConnect(listener: NodeStatusViewListener): void { + onDidConnect(listener: NodeStatusViewListener): void { this.onDidConnectListeners.add(listener); } - public resolveWebviewView( - view: vscode.WebviewView - ) { - this._view = view; - - view.webview.options = { - enableScripts: true, - }; - - view.webview.html = this.getWebviewContent(view.webview, this.extensionUri); - this.setWebviewMessageListener(view); - - view.onDidChangeVisibility((): void => { - this.updateView(); - }); - - this.updateView(); - } - - private getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { - - const toolkitUri = Util.getUri(webview, extensionUri, [ - "node_modules", - "@vscode", - "webview-ui-toolkit", - "dist", - "toolkit.js", - ]); - - const mainUri = Util.getUri(webview, extensionUri, ["src", "webview-ui", "main.js"]); - const stylesUri = Util.getUri(webview, extensionUri, ["src", "webview-ui", "styles.css"]); - const pyrsiaHostname = this.nodeProvider.getHostname(); - - return /*html*/ ` + public resolveWebviewView(view: vscode.WebviewView) { + this._view = view; + + view.webview.options = { + enableScripts: true + }; + + view.webview.html = this.getWebviewContent(view.webview, this.extensionUri); + this.setWebviewMessageListener(view); + + view.onDidChangeVisibility((): void => { + this.updateView(); + }); + + this.updateView(); + } + + public async updateView() { + const connected: boolean = await this.nodeProvider.isNodeHealthy(); + if (connected) { + this.connected(); + } else { + this.disconnected(); + } + } + + private getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { + + const toolkitUri = Util.getUri(webview, extensionUri, [ + "node_modules", + "@vscode", + "webview-ui-toolkit", + "dist", + "toolkit.js" + ]); + + const mainUri = Util.getUri(webview, extensionUri, ["src", "webview-ui", "main.js"]); + const stylesUri = Util.getUri(webview, extensionUri, ["src", "webview-ui", "styles.css"]); + const pyrsiaHostname = this.nodeProvider.getHostname(); + + return /*html*/ ` @@ -86,7 +93,8 @@ export class NodeStatusViewProvider implements vscode.WebviewViewProvider {
👉 Please make sure Pyrsia is installed, - + running and configured.. 👈
@@ -98,53 +106,48 @@ export class NodeStatusViewProvider implements vscode.WebviewViewProvider { `; - } - - private setWebviewMessageListener(view: vscode.WebviewView) { - view.webview.onDidReceiveMessage(async (message) => { - const command = message.command; - switch (command) { - case "node-update-view": { - this.updateView(); - } - } - }); - } - - private connected() { - const view = this._view; - if (view) { - let nodeStatus: unknown; - this.nodeProvider.getStatus().then((data) => { - nodeStatus = data; - }).finally(() => { - view.webview.postMessage({ type: 'node-connected', nodeStatus }); - for (const listener of this.onDidConnectListeners) { - listener.onDidConnect(); - } - }); - } - } - - private disconnected() { - if (this._view) { - this._view.webview.postMessage({ type: 'node-disconnected' }); - } - for (const listener of this.onDidConnectListeners) { - listener.onDidConnect(); - } - } - - public async updateView() { - const connected: boolean = await this.nodeProvider.isNodeHealthy(); - if (connected) { - this.connected(); - } else { - this.disconnected(); - } - } + } + + private setWebviewMessageListener(view: vscode.WebviewView) { + view.webview.onDidReceiveMessage((message) => { + const { command } = message; + switch (command) { + case "node-update-view": { + this.updateView(); + break; + } + default: { + throw new Error(`Command ${command} not found`); + } + } + }); + } + + private connected() { + const view = this._view; + if (view) { + let nodeStatus: unknown; + this.nodeProvider.getStatus().then((data) => { + nodeStatus = data; + }).finally(() => { + view.webview.postMessage({ nodeStatus, type: 'node-connected' }); + for (const listener of this.onDidConnectListeners) { + listener.onDidConnect(); + } + }); + } + } + + private disconnected() { + if (this._view) { + this._view.webview.postMessage({ type: 'node-disconnected' }); + } + for (const listener of this.onDidConnectListeners) { + listener.onDidConnect(); + } + } } export interface NodeStatusViewListener { - onDidConnect(): void; + onDidConnect(): void; } From 4e19f0e47bf5ac951b26f372c1ddd9e2782f4a00 Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Mon, 9 Jan 2023 16:12:07 -0800 Subject: [PATCH 04/16] Init commit for the new Docker integration view. --- .eslintrc.cjs | 4 +- .vscode/launch.json | 11 +- .vscode/settings.json | 2 + package-lock.json | 326 ++++++++++- package.json | 74 ++- resources/images/docker_small.svg | 2 +- resources/images/pyrsia_color.svg | 15 + resources/images/pyrsia_small.svg | 27 - resources/images/pyrsia_white.svg | 15 + resources/images/pyrsia_white_small.svg | 8 + src/extension.ts | 28 +- src/integrations/Docker.ts | 149 ----- src/integrations/Integration.ts | 28 - src/integrations/api/Integration.ts | 28 + src/integrations/impl/DockerIntegration.ts | 523 ++++++++++++++++++ src/nodeProvider.ts | 4 +- src/utilities/{client.ts => pyrsiaClient.ts} | 42 ++ src/utilities/util.ts | 30 + ...ntegrationsView.ts => IntegrationsView.ts} | 48 +- src/webviews/NodeConfigView.ts | 52 +- 20 files changed, 1129 insertions(+), 287 deletions(-) create mode 100644 resources/images/pyrsia_color.svg delete mode 100644 resources/images/pyrsia_small.svg create mode 100644 resources/images/pyrsia_white.svg create mode 100644 resources/images/pyrsia_white_small.svg delete mode 100644 src/integrations/Docker.ts delete mode 100644 src/integrations/Integration.ts create mode 100644 src/integrations/api/Integration.ts create mode 100644 src/integrations/impl/DockerIntegration.ts rename src/utilities/{client.ts => pyrsiaClient.ts} (57%) rename src/webviews/{NodeIntegrationsView.ts => IntegrationsView.ts} (72%) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index afc89e6..b0cc4cb 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -64,8 +64,8 @@ module.exports = { "keyword-spacing": "warn", "linebreak-style": "warn", "max-depth": "warn", - "max-len": ["warn", { "code": 120, "comments": 200 }], - "max-lines": "warn", + "max-len": ["warn", { "code": 150, "comments": 200 }], + "max-lines": ["warn", { "max": 1000}], "max-nested-callbacks": "error", "max-params": ["warn",{ "max": 8 }], "new-cap": "warn", diff --git a/.vscode/launch.json b/.vscode/launch.json index e7af81c..5900042 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,7 +15,14 @@ ], "preLaunchTask": "", "request": "launch", - "type": "extensionHost" + "type": "extensionHost", + "env": { + "VSCODE_DEBUG_MODE": "true" + } } - ] + + ], + "env": { + "DEBUG": "DUPA" + }, } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c31f9d..a1a9848 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,9 @@ "cSpell.words": [ "callstack", "codespaces", + "configfile", "configurenode", + "dockerode", "Pyrsia", "pyrsiaintall", "pyrsiaoverview", diff --git a/package-lock.json b/package-lock.json index b2bcb84..ac70a52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,12 @@ "dependencies": { "@vscode/webview-ui-toolkit": "^1.2.0", "axios": "^1.2.1", + "dockerode": "3.3.4", "fs": "^0.0.1-security", "os": "^0.1.2" }, "devDependencies": { + "@types/dockerode": "^3.3.14", "@types/glob": "^8.0.0", "@types/mocha": "^10.0.1", "@types/node": "16.x", @@ -30,6 +32,11 @@ "vscode": "^1.74.0" } }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==" + }, "node_modules/@eslint/eslintrc": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.0.tgz", @@ -166,6 +173,26 @@ "node": ">= 6" } }, + "node_modules/@types/docker-modem": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.2.tgz", + "integrity": "sha512-qC7prjoEYR2QEe6SmCVfB1x3rfcQtUr1n4x89+3e0wSTMQ/KYCyf+/RAA9n2tllkkNc6//JMUZePdFRiGIWfaQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.14", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.14.tgz", + "integrity": "sha512-PUTwtySPzCbjZ/uqRMBWKHtLGqBAlhnLitzHuom19NEX0KBYsQXqbVlig+zbUgYQU1paDeQURXj7QNglh1RI6A==", + "dev": true, + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*" + } + }, "node_modules/@types/glob": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.0.0.tgz", @@ -206,6 +233,15 @@ "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", "dev": true }, + "node_modules/@types/ssh2": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.11.6.tgz", + "integrity": "sha512-8Mf6bhzYYBLEB/G6COux7DS/F5bCWwojv/qFo2yH/e4cLzAavJnxvFXrYW59iKfXdhG6OmzJcXDasgOb/s0rxw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/vscode": { "version": "1.74.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.74.0.tgz", @@ -537,6 +573,14 @@ "node": ">=8" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -558,6 +602,33 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -589,6 +660,29 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", @@ -623,6 +717,29 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-indexof-polyfill": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", @@ -641,6 +758,15 @@ "node": ">=0.2.0" } }, + "node_modules/buildcheck": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.3.tgz", + "integrity": "sha512-pziaA+p/wdVImfcbsZLNF32EiWyujlQLwolMqUQE8xpKNOH7KmZQaY8sXN7DGOEzPAElo9QTaeNRfGnf3iOJbA==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -729,6 +855,11 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -781,6 +912,20 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/cpu-features": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.4.tgz", + "integrity": "sha512-fKiZ/zp1mUwQbnzb9IghXtHtDoTMtNeb8oYGx6kX2SYfhnG0HNdBEBIzB9b5KlXu5DQPhfy3mInbBxFcgwAr3A==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "0.0.3", + "nan": "^2.15.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -799,7 +944,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -859,6 +1003,46 @@ "node": ">=8" } }, + "node_modules/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-h0Ow21gclbYsZ3mkHDfsYNDqtRhXS8fXr51bU0qr1dxgTMJj0XufbzX+jhNOvA8KuEEzn6JbvLVhXyv+fny9Uw==", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.11.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/docker-modem/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/dockerode": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.4.tgz", + "integrity": "sha512-3EUwuXnCU+RUlQEheDjmBE0B7q66PV9Rw5NiH1sXwINq0M9c5ERP9fxgkw36ZHOtzf4AGEEYySnkx/sACC9EgQ==", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "docker-modem": "^3.0.0", + "tar-fs": "~2.0.1" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -886,6 +1070,14 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1276,6 +1468,11 @@ "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1496,6 +1693,25 @@ "node": ">= 6" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -1543,8 +1759,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -1833,6 +2048,11 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "node_modules/mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", @@ -1950,8 +2170,13 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "optional": true }, "node_modules/nanoid": { "version": "3.3.3", @@ -1990,7 +2215,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -2127,6 +2351,15 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -2328,6 +2561,11 @@ } ] }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -2388,11 +2626,32 @@ "node": ">=8" } }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==" + }, + "node_modules/ssh2": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.11.0.tgz", + "integrity": "sha512-nfg0wZWGSsfUe/IBJkXVll3PEZ//YH2guww+mP88gTpuSU4FtZN7zu9JoeTGOyCNx2dTDtT9fOpWwlzyj4uOOw==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.4", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.4", + "nan": "^2.16.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -2400,8 +2659,7 @@ "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/string-width": { "version": "4.2.3", @@ -2458,6 +2716,45 @@ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==" }, + "node_modules/tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -2505,6 +2802,11 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2572,8 +2874,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/which": { "version": "2.0.2", @@ -2625,8 +2926,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/y18n": { "version": "5.0.8", diff --git a/package.json b/package.json index f1afa65..3aef2e2 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "Other" ], "activationEvents": [ - "onView:pyrsia.node" ], "main": "./out/extension.js", "contributes": { @@ -23,7 +22,7 @@ { "id": "package-explorer", "title": "Pyrsia", - "icon": "resources/images/pyrsia_small.svg" + "icon": "resources/images/pyrsia_white.svg" } ] }, @@ -35,7 +34,7 @@ }, { "id": "pyrsia.node-integrations", - "name": "Available Integrations" + "name": "Integrations" }, { "id": "pyrsia.help", @@ -43,7 +42,72 @@ } ] }, - "commands": [] + "commands": [ + { + "command": "pyrsia.replace-docker-images", + "title": "Replace Docker Images with Pyrsia images", + "shortTitle": "Replace images", + "icon": "$(refresh)" + }, + { + "command": "pyrsia.configure-node", + "title": "Update Pyrsia Node URL", + "shortTitle": "Update Pyrsia node URL", + "icon": "$(gear)" + }, + { + "command": "pyrsia.update-docker-conf", + "title": "Add Pyrsia to Docker Configuration", + "shortTitle": "Add Pyrsia to Docker configuration", + "icon": "$(gear)" + }, + { + "command": "pyrsia.request-docker-build", + "title": "Add Image to Pyrsia", + "shortTitle": "Add selected image to Pyrsia", + "icon": "$(server-environment)" + }, + { + "command": "pyrsia.open-docker-trans-log", + "title": "Open Pyrsia Transparency Log", + "shortTitle": "Open Pyrsia Transparency Log", + "icon": "$(file-code)" + } + ], + "menus": { + "view/item/context": [ + { + "command": "pyrsia.replace-docker-images", + "when": "view == pyrsia.node-integrations && viewItem == pyrsia.docker", + "group": "pyrsia-integration" + }, + { + "command": "pyrsia.configure-node", + "when": "view == pyrsia.node-config", + "group": "pyrsia-config" + }, + { + "command": "pyrsia.update-docker-conf", + "when": "view == pyrsia.node-integrations && viewItem == pyrsia.docker.configfile", + "group": "pyrsia-integration" + }, + { + "command": "pyrsia.request-docker-build", + "when": "view == pyrsia.node-integrations && viewItem == pyrsia.docker.not-pyrsia", + "group": "pyrsia-integration" + }, + { + "command": "pyrsia.request-docker-build", + "when": "view == pyrsia.node-integrations && viewItem == pyrsia.docker.not-pyrsia", + "group": "pyrsia-integration" + }, + { + "command": "pyrsia.open-docker-trans-log", + "when": "view == pyrsia.node-integrations && viewItem == pyrsia.docker.is-pyrsia", + "group": "pyrsia-integration" + } + ] + } }, "scripts": { "vscode:prepublish": "npm run compile", @@ -54,6 +118,7 @@ "test": "node ./out/test/runTest.js" }, "devDependencies": { + "@types/dockerode": "^3.3.14", "@types/glob": "^8.0.0", "@types/mocha": "^10.0.1", "@types/node": "16.x", @@ -69,6 +134,7 @@ "dependencies": { "@vscode/webview-ui-toolkit": "^1.2.0", "axios": "^1.2.1", + "dockerode": "3.3.4", "fs": "^0.0.1-security", "os": "^0.1.2" } diff --git a/resources/images/docker_small.svg b/resources/images/docker_small.svg index d06ad89..4ebd26e 100644 --- a/resources/images/docker_small.svg +++ b/resources/images/docker_small.svg @@ -1,6 +1,6 @@ - diff --git a/resources/images/pyrsia_color.svg b/resources/images/pyrsia_color.svg new file mode 100644 index 0000000..d60f56e --- /dev/null +++ b/resources/images/pyrsia_color.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/images/pyrsia_small.svg b/resources/images/pyrsia_small.svg deleted file mode 100644 index 0b432cc..0000000 --- a/resources/images/pyrsia_small.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - diff --git a/resources/images/pyrsia_white.svg b/resources/images/pyrsia_white.svg new file mode 100644 index 0000000..c33a89f --- /dev/null +++ b/resources/images/pyrsia_white.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/images/pyrsia_white_small.svg b/resources/images/pyrsia_white_small.svg new file mode 100644 index 0000000..32bd6de --- /dev/null +++ b/resources/images/pyrsia_white_small.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/extension.ts b/src/extension.ts index 4ef6595..299c028 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,25 +1,29 @@ -import * as vscode from 'vscode'; -import { NodeConfigView } from './webviews/NodeConfigView'; -// import { NodeStatusViewProvider } from './webviews/NodeStatusView'; -import { NodeIntegrationsView } from './webviews/NodeIntegrationsView'; -import { Util } from './utilities/util'; -import { HelpView } from './webviews/HelpView'; +import * as vscode from "vscode"; +import { NodeConfigView } from "./webviews/NodeConfigView"; +// import { NodeStatusViewProvider } from "./webviews/NodeStatusView"; +import { IntegrationsView as IntegrationsView } from "./webviews/IntegrationsView"; +import { Util } from "./utilities/util"; +import { HelpView } from "./webviews/HelpView"; +import { Integration } from "./integrations/api/Integration"; +import { DockerIntegration } from "./integrations/impl/DockerIntegration"; export const activate = (context: vscode.ExtensionContext) => { - - console.log('Pyrsia extension activated'); - // initialize the util Util.init(context); - // Node status web view provider + // Node status web view provider, this is debug only view, disabled for now //new NodeStatusViewProvider(context); + // Create integrations + const dockerIntegration: Integration = new DockerIntegration(context); + // Node status config view - new NodeConfigView(context); + const nodeConfigView = new NodeConfigView(context); + nodeConfigView.addIntegration(dockerIntegration); // Node status config view - new NodeIntegrationsView(context); + const integrationView = new IntegrationsView(context); + integrationView.addIntegration(dockerIntegration); // Node status config view new HelpView(context); diff --git a/src/integrations/Docker.ts b/src/integrations/Docker.ts deleted file mode 100644 index d06cf76..0000000 --- a/src/integrations/Docker.ts +++ /dev/null @@ -1,149 +0,0 @@ -import * as os from "os"; -import * as fsUtils from "../utilities/fsUtils"; -import { Integration, IntegrationTreeItem } from "./Integration"; -import * as path from 'path'; -import { Util } from "../utilities/util"; -import * as vscode from 'vscode'; - -export class Docker implements Integration { - - static readonly commandId = "pyrsia.openUpdateDockerConfFile"; - private static readonly integrationId: string = "docker"; - private static confMap: Map = new Map(); - private static pyrsiaConfigCode = ` "registry-mirrors": ["http://0.0.0.0:7888"] \n`; - - private readonly treeItems: Map = new Map(); - - static { - Docker.confMap.set(path.join(os.homedir(), ".docker"), "config.json"); - } - - constructor(context: vscode.ExtensionContext) { - - // docker open and update configuration editor command for the docker integration - const openDockerUpdateConfFile = vscode.commands.registerCommand(Docker.commandId, (confFilePath: string) => { - console.log(confFilePath); - const setting: vscode.Uri = vscode.Uri.parse(`${confFilePath}`); - - vscode.workspace.openTextDocument(setting).then((textDocument: vscode.TextDocument) => { - vscode.window.showTextDocument(textDocument, 1, false).then(async (textEditor) => { - const confirmOption = "Yes"; - const cancelOption = "No"; - const result = await vscode.window.showInformationMessage( - "Add Pyrsia to the Docker configuration?", - confirmOption, - cancelOption - ); - - if (result === confirmOption) { - const ls = textDocument.lineCount; - textEditor.edit(edit => { - // TODO This is a prototype, not even close to the real solution. - const line = textDocument.lineAt(ls - 3); - edit.delete(line.range); - edit.insert(new vscode.Position(line.lineNumber, 0), `${line.text},`); - edit.insert(new vscode.Position(ls - 2, 0), Docker.pyrsiaConfigCode); - }); - // TODO Update the selection? - textEditor.selection = new vscode.Selection( - new vscode.Position(2, 2), - new vscode.Position(2, Docker.pyrsiaConfigCode.length + 2) - ); - textDocument.save(); - } - }); - }, (error: unknown) => { - console.error(error); - }); - - }); - - context.subscriptions.push(openDockerUpdateConfFile); - } - - getTreeItem(treeItemId: string): IntegrationTreeItem | undefined { - return this.treeItems.get(treeItemId); - } - - getId(): string { - return Docker.integrationId; - } - - update(): void { - // find docker conf file (macos, linux) - for (const confPath of Docker.confMap.keys()) { - const fileName = Docker.confMap.get(confPath); - if (!fileName) { - throw new Error("Configuration file name cannot be null"); - } - fsUtils.findByName(confPath, fileName).then((confFilePath) => { - if (confFilePath) { - const treeItemId = `${this.getId()}-${confFilePath}`; - const label = `${confFilePath}`; - this.treeItems.set(treeItemId, new DockerTreeItem(label, treeItemId, confFilePath)); - } else { - console.log(`No configuration for 'Docker' - ${path.join(confPath, fileName)}`); - } - }); - } - } - - getTreeItemChildren(): string[] { - const children: string[] = []; - for (const label of this.treeItems.keys()) { - children.push(label); - } - - return children; - } -} - -class DockerTreeItem extends IntegrationTreeItem { - - constructor( - public label: string, - public readonly id: string, - public readonly confFilePath: string - ) { - super(label, id); - // const command = new class implements vscode.Command { - // title: string; - // command: string; - // tooltip?: string | undefined; - // arguments?: any[] | undefined; - // run() { - // console.log("Docker open File"); - // } - // }(); - // command.title = "Open Docker Configuration"; - // command.command = "command-open-docker-node"; - // command.tooltip = command.title; - // super.command = command; - this.command = { - arguments: [confFilePath], - command: Docker.commandId, - title: "Open Docker configuration file" - }; - - } - - // command?: vscode.Command | undefined = new class implements vscode.Command { - // title: string; - // command: string; - // tooltip?: string | undefined; - // arguments?: any[] | undefined; - // run() { - // console.log("Docker open File"); - // } - // }(); - - update(): void { - throw new Error("Method not implemented."); - } - - // eslint-disable-next-line @typescript-eslint/member-ordering - iconPath = { - dark: path.join(Util.getResourceImagePath(), "docker_small.svg"), //TODO update to dark - light: path.join(Util.getResourceImagePath(), "docker_small.svg") - }; -} diff --git a/src/integrations/Integration.ts b/src/integrations/Integration.ts deleted file mode 100644 index 29eec79..0000000 --- a/src/integrations/Integration.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as vscode from 'vscode'; - -export interface Integration { - - getTreeItem(treeItemId: string): IntegrationTreeItem | undefined; - - getTreeItemChildren(): string[]; - - getId(): string; - - update(): void; - -} - -export abstract class IntegrationTreeItem extends vscode.TreeItem { - - constructor( - public label: string, - public readonly id: string - ) { - super(label, vscode.TreeItemCollapsibleState.None); - this.tooltip = this.label; - } - - abstract update(): void; - -} - diff --git a/src/integrations/api/Integration.ts b/src/integrations/api/Integration.ts new file mode 100644 index 0000000..eda8efd --- /dev/null +++ b/src/integrations/api/Integration.ts @@ -0,0 +1,28 @@ +import * as vscode from 'vscode'; +// https://github.com/xojs/eslint-config-xo-typescript/issues/43 +/* eslint-disable @typescript-eslint/naming-convention */ + +// Help list +export enum Event { + IntegrationModelUpdate, + NodeConfigurationUpdate, +} + +export interface Integration { + + getTreeItem(treeItemId: string): IntegrationTreeItem | undefined; + + getTreeItemChildren(parentId?: string): string[]; + + getId(): string; + + update(event: Event): void; + +} + +export abstract class IntegrationTreeItem extends vscode.TreeItem { + + abstract update(context: never): void; + +} + diff --git a/src/integrations/impl/DockerIntegration.ts b/src/integrations/impl/DockerIntegration.ts new file mode 100644 index 0000000..2cb9df0 --- /dev/null +++ b/src/integrations/impl/DockerIntegration.ts @@ -0,0 +1,523 @@ +import * as os from "os"; +import * as fsUtils from "../../utilities/fsUtils"; +import { Event, Integration, IntegrationTreeItem } from "../api/Integration"; +import * as path from 'path'; +import { Util } from "../../utilities/util"; +import * as vscode from "vscode"; +import * as client from "../../utilities/pyrsiaClient"; +import { IntegrationsView } from "../../webviews/IntegrationsView"; + +export class DockerIntegration implements Integration { + //confirm options + static readonly confirmOption = "Yes"; + static readonly cancelOption = "No"; + + // command Ids + static readonly updateDockerConfCommandId = "pyrsia.update-docker-conf"; + static readonly replaceDockerImagesCommandId = "pyrsia.replace-docker-images"; + private static readonly openDockerTransparencyLog = "pyrsia.open-docker-trans-log"; + private static readonly integrationId: string = "pyrsia.docker"; + private static readonly requestBuildId: string = "pyrsia.request-docker-build"; + + // context values + // eslint-disable-next-line @typescript-eslint/member-ordering + static readonly imageNotPyrsia = `${DockerIntegration.integrationId}.not-pyrsia`; + // eslint-disable-next-line @typescript-eslint/member-ordering + static readonly imagePyrsia = `${DockerIntegration.integrationId}.is-pyrsia`; + + // tree item ids + // eslint-disable-next-line @typescript-eslint/member-ordering + static readonly configFileItemId: string = `${this.integrationId}.configfile`; + private static readonly configItemId: string = `${this.integrationId}.configs`; + private static readonly imagesItemId: string = `${this.integrationId}.images`; + private static readonly imageItemId: string = `${this.integrationId}.dockerimage`; + + // docker config files search path + private static confMap: Map = new Map(); + private static readonly registryMirrorsConfName = "registry-mirrors"; + + // item names + private static readonly dockerTreeItemName = "Docker"; + private static warningIconPath: vscode.ThemeIcon = new vscode.ThemeIcon("warning"); // NOI18 + + private readonly treeItems: Map = new Map(); + private readonly dockerIconPath: { light: string | vscode.Uri; dark: string | vscode.Uri; }; + + static { + DockerIntegration.confMap.set(path.join(os.homedir(), ".docker"), "daemon.json"); + } + + constructor(context: vscode.ExtensionContext) { + // get the icon paths + this.dockerIconPath = { + dark: path.join(Util.getResourceImagePath(), "docker_small.svg"), //TODO update to dark + light: path.join(Util.getResourceImagePath(), "docker_small.svg") //TODO update to light + }; + + // create and add the "non-dynamic" docker tree items. + this.createTreeItems(this.treeItems); + + // register the docker commands + this.registerCommands(context); + } + + private static getImageTreeId(imageName: string): string { + return `${DockerIntegration.imageItemId}.${imageName}`; + } + + async replaceImagesWithPyrsia(): Promise { + // look for docker images + const dockerClient = Util.getDockerClient(); + const images = await dockerClient.listImages(); + const allContainers = await dockerClient.listContainers({ "all": true }); + images.forEach(async (imageInfo) => { + const imageName = imageInfo.RepoTags?.join(); + // no image tags? => skip the image + if (!imageName) { + return; + } + // only update the images which are available in Pyrsia + const transImageLog: [] = await client.getDockerTransparencyLog(imageName); + await this.updateModel(); + if (transImageLog.length > 0) { + // remove the old images first + try { + // get all Pyrsia images relevant containers + const containers = allContainers.filter((container) => { + return container.Image === imageName; + }); + if (containers.length > 0) { + await vscode.window.showErrorMessage( + // eslint-disable-next-line max-len + `Replacing the docker images failed because '${imageName}' image + has containers, please remove the relevant docker containers and try again.`, + "Close" + ); + return; + } + // delete the image + dockerClient.getImage(imageInfo.Id).remove({ force: true }, (error) => { + if (error) { + vscode.window.showErrorMessage( + // eslint-disable-next-line max-len + `Replacing the docker images failed because '${imageName}' image + has containers, please remove the relevant docker containers and try again.`, + "Close" + ); + return; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dockerClient.pull(imageName, (err: string, stream: any) => { + console.log(err); + const onFinished = (error_: unknown, output: unknown) => { + console.info(`Error: ${error_}, ${output}`); + this.treeItems.delete(DockerIntegration.getImageTreeId(imageName)); + IntegrationsView.requestIntegrationsModelUpdate(); + IntegrationsView.requestIntegrationsViewUpdate(); + }; + + const onProgress = (event: unknown) => { + console.log(event); + const treeItem = this.treeItems.get(DockerIntegration.getImageTreeId(imageName)); + if (treeItem) { + treeItem.label = `Replacing '${imageName}' with Pyrsia image.`; + treeItem.iconPath = DockerImageTreeItem.iconPathPullDocker; + } + IntegrationsView.requestIntegrationsModelUpdate(); + IntegrationsView.requestIntegrationsViewUpdate(); + }; + dockerClient.modem.followProgress(stream, onFinished, onProgress); + }); + }); + } catch (err) { + Util.debugMessage(`Couldn't replace image: ${imageInfo.Labels}, error: ${err}`); + IntegrationsView.requestIntegrationsModelUpdate(); + IntegrationsView.requestIntegrationsViewUpdate(); + } + } + }); + } + + getTreeItemChildren(parentId?: string | undefined): string[] { + console.log(`${parentId}`); + + let children: string[] = []; + switch (parentId) { + case DockerIntegration.integrationId: + children.push(DockerIntegration.configItemId, DockerIntegration.imagesItemId); + break; + case DockerIntegration.configItemId: + // eslint-disable-next-line no-case-declarations + const configItems = [... this.treeItems].map(([, value]) => { + return value.id; + }).filter(id => id?.includes(DockerIntegration.configFileItemId)); + children = children.concat((configItems as string[])); + break; + case DockerIntegration.imagesItemId: + // eslint-disable-next-line no-case-declarations + const imageItems = [... this.treeItems].map(([, value]) => { + return value.id; + }).filter(id => id?.includes(DockerIntegration.imageItemId)); + children = children.concat((imageItems as string[])); + break; + default: + children.push(DockerIntegration.integrationId); + break; + } + + return children.sort(); + } + + getTreeItem(treeItemId: string): IntegrationTreeItem | undefined { + return this.treeItems.get(treeItemId); + } + + getId(): string { + return DockerIntegration.integrationId; + } + + async update(event: Event): Promise { + switch (event) { + case Event.IntegrationModelUpdate: { + await this.updateModel(); + break; + } + case Event.NodeConfigurationUpdate: { + this.treeItems.forEach((treeItem: vscode.TreeItem) => { + if (treeItem instanceof DockerConfigTreeItem) { + vscode.commands.executeCommand(DockerIntegration.updateDockerConfCommandId, treeItem.confFilePath); + } + }); + break; + } + default:{ + this.updateModel(); + } + } + IntegrationsView.requestIntegrationsViewUpdate(); + } + + private async updateModel() { + // check if the docker and node is up + let isDockerUp = true; + try { + await Util.getDockerClient().ping(); + } catch (error) { + isDockerUp = false; + } + const isPyrsiaNodeUp = await client.isNodeHealthy(); + // update the Docker tree item in case Docker or node is down + const dockerTreeItem = this.treeItems.get(DockerIntegration.integrationId); + if (dockerTreeItem && (!isDockerUp || !isPyrsiaNodeUp)) { + // create warning tree item and hide the rest of the tree items + dockerTreeItem.label = `${DockerIntegration.dockerTreeItemName} (Pyrsia node or Docker is disconnected)`; + dockerTreeItem.tooltip = "Please make sure that Docker service and Pyrsia node is up and configured"; + dockerTreeItem.iconPath = DockerIntegration.warningIconPath; + dockerTreeItem.collapsibleState = vscode.TreeItemCollapsibleState.None; + dockerTreeItem.command = undefined; + } else { + // recreate the docker tree item + this.createDockerTreeItem(); + + // find the docker conf file(s) (macos, linux). TODO Windows + for (const confPath of DockerIntegration.confMap.keys()) { + const fileName = DockerIntegration.confMap.get(confPath); + if (!fileName) { + throw new Error("Configuration file name cannot be null"); + } + fsUtils.findByName(confPath, fileName).then((confFilePath) => { + if (confFilePath) { + const id = `${DockerIntegration.configFileItemId}.${confFilePath}`; + const label = `${confFilePath}`; + this.treeItems.set( + id, + new DockerConfigTreeItem(label, id, confFilePath, vscode.TreeItemCollapsibleState.None) + ); + } else { + console.log(`No configuration for 'Docker' - ${path.join(confPath, fileName)}`); + } + }); + IntegrationsView.requestIntegrationsViewUpdate(); + } + + // look for docker images + const dockerClient = Util.getDockerClient(); + const images = await dockerClient.listImages(); + images.forEach(async (image) => { + const imageName = image.RepoTags?.join(); + if (imageName?.startsWith("")) { + return; + } + // no image tags? => skip the image + if (!imageName) { + return; + } + const id = DockerIntegration.getImageTreeId(imageName); + const imageItem = new DockerImageTreeItem( + id, + imageName + ); + // check if image exists in pyrsia + const transImageLog: [] = await client.getDockerTransparencyLog(imageName); + imageItem.update({ pyrsia: transImageLog.length > 0 }); + + // add item to the tree items map + this.treeItems.set(id, imageItem); + }); + IntegrationsView.requestIntegrationsViewUpdate(); + } + } + + private registerCommands(context: vscode.ExtensionContext) { + // docker command to open and update configuration editor command for the docker integration + const openDockerUpdateConfFile = vscode.commands.registerCommand( + DockerIntegration.updateDockerConfCommandId, + (args: DockerConfigTreeItem | string) => { + let confFilePath; + if (args instanceof DockerConfigTreeItem) { // arg as tree item + // eslint-disable-next-line prefer-destructuring + confFilePath = args.confFilePath; + } else if (typeof args === "string") { // arg as view item name (tree item name) + const treeItem = this.treeItems.get(args); + if (treeItem instanceof DockerConfigTreeItem) { + confFilePath = treeItem?.confFilePath; + } else { + console.error(`Docker update command - tree item not found ${args}`); + } + } else { + console.error(`Docker update command - tree item not found ${args}`); + } + const setting: vscode.Uri = vscode.Uri.parse(`${confFilePath}`); + vscode.workspace.openTextDocument(setting).then((textDocument: vscode.TextDocument) => { + // Get the docker config as JSON object + const dockerConfigJson = JSON.parse(textDocument.getText()); + // Get the current node configuration + const { host } = Util.getNodeConfig(); + // Check if the docker config has to be updated. + let registryMirrors: string[] = dockerConfigJson[DockerIntegration.registryMirrorsConfName]; + let updateConfig = false; + if (registryMirrors) { + updateConfig = !!registryMirrors.find((mirror: string) => { + return mirror.includes(host); + }); + } + vscode.window.showTextDocument(textDocument, 1, false).then(async (textEditor) => { + if (updateConfig) { + // the docker config doesn't have to be updated, exit + return; + } + const confirmOption = "Yes"; + const cancelOption = "No"; + const result = await vscode.window.showInformationMessage( + "Add Pyrsia to the Docker configuration?", + confirmOption, + cancelOption + ); + + if (result === confirmOption) { + textEditor.edit(edit => { + if (!registryMirrors) { // no mirrors found, add one for the pyrsia node + registryMirrors = []; + dockerConfigJson[DockerIntegration.registryMirrorsConfName] = registryMirrors; + } + // update document only when the docker config was updated + registryMirrors.push(host); + const updateDockerConfText = JSON.stringify(dockerConfigJson, null, 2); + edit.replace( + new vscode.Range( + textDocument.lineAt(0).range.start, + textDocument.lineAt(textDocument.lineCount - 1).range.end + ), + updateDockerConfText + ); + // select the changes + const hostStartLocation = updateDockerConfText.lastIndexOf(host); + textEditor.selection = new vscode.Selection( + new vscode.Position(hostStartLocation, hostStartLocation), + new vscode.Position(hostStartLocation, hostStartLocation + host.length) + ); + textDocument.save(); + vscode.window.showWarningMessage("Please restart Docker to apply the configuration changes."); + }); + } + }); + }, (error: unknown) => { + console.error(error); + }); + } + ); + + // docker command to open and update configuration editor command for the docker integration + const replaceDockerWithPyrsiaImages = vscode.commands.registerCommand( + DockerIntegration.replaceDockerImagesCommandId, + async () => { + const result = await vscode.window.showInformationMessage( + "👋 Are you sure you'd like to replace all local docker images with the Pyrsia images?", + DockerIntegration.confirmOption, + DockerIntegration.cancelOption + ); + + if (result === DockerIntegration.confirmOption) { + this.replaceImagesWithPyrsia(); + } + } + ); + + // request docker image build + const requestDockerImageBuild = vscode.commands.registerCommand( + DockerIntegration.requestBuildId, + async (id: string) => { + const treeItem = this.treeItems.get(id); + if (treeItem && typeof treeItem.label === "string") { + // ask if request the build + const result = await vscode.window.showInformationMessage( + `👋 Are you sure you'd like to add '${treeItem.label}' to Pyrsia?`, + DockerIntegration.confirmOption, + DockerIntegration.cancelOption + ); + + if (result === DockerIntegration.confirmOption) { + const buildId = await client.requestDockerBuild(treeItem.label); + let message; + if (buildId) { + message = `A request to add '${treeItem.label}' was successful, ID: ${buildId}`; + } else { + message = `A request to add '${treeItem.label}' was unsuccessful.`; + } + // show the result message + vscode.window.showInformationMessage(message); + } + } + } + ); + + // open docker image transparency log command + const openDockerTransparencyLog = vscode.commands.registerCommand( + DockerIntegration.openDockerTransparencyLog, + async (id: string) => { + const treeItem = this.treeItems.get(id); + if (treeItem instanceof DockerImageTreeItem) { + const transLogs: string[] = await client.getDockerTransparencyLog(treeItem.label); + if (!transLogs) { + return; + } + // parse and format the logs + const formattedTransLogsJson = JSON.stringify({ [treeItem.label]: transLogs }, null, 2); + // open and create a new editor and insert the transparency log + const textDocument = await vscode.workspace.openTextDocument({ content: formattedTransLogsJson, language: "json" }); + await vscode.window.showTextDocument(textDocument); + } else { + console.error(`Docker update command - tree item not found ${id}`); + return; + } + } + ); + + // subscribe the commands + context.subscriptions.push( + openDockerUpdateConfFile, // update docker file command + replaceDockerWithPyrsiaImages, // replace docker images with pyrsia images + requestDockerImageBuild, // request docker image build + openDockerTransparencyLog // opens docker image transparency log in editor + ); + } + + private createDockerTreeItem() { + // create "Docker" tree item + const dockerTreeItem = new DockerTreeItem(DockerIntegration.integrationId, "Docker", this.dockerIconPath); + dockerTreeItem.contextValue = DockerIntegration.integrationId; + this.treeItems.set(DockerIntegration.integrationId, dockerTreeItem); + } + + private createTreeItems(treeItems: Map) { + // create "Docker" tree item + this.createDockerTreeItem(); + + // create Docker "Configuration" tree item + treeItems.set( + DockerIntegration.configItemId, + new DockerTreeItem( + DockerIntegration.configItemId, + "Configuration", + new vscode.ThemeIcon("gear") // NOI18 + ) + ); + + // create docker "Images" tree item + const imagesTreeItem = new DockerTreeItem( + DockerIntegration.imagesItemId, + "Images", + new vscode.ThemeIcon("folder-library") // NOI18 + ); + imagesTreeItem.contextValue = DockerIntegration.integrationId; + treeItems.set( + DockerIntegration.imagesItemId, + imagesTreeItem + ); + } +} + +class DockerConfigTreeItem extends IntegrationTreeItem { + private static iconPath: vscode.ThemeIcon = new vscode.ThemeIcon("go-to-file"); // NOI18 + + constructor( + public label: string, + public readonly id: string, + public readonly confFilePath: string, + public readonly collapsableState: vscode.TreeItemCollapsibleState + ) { + super(label, collapsableState); + + this.command = { + arguments: [this], + command: DockerIntegration.updateDockerConfCommandId, + title: "Open Docker configuration file" + }; + + this.iconPath = DockerConfigTreeItem.iconPath; + this.contextValue = DockerIntegration.configFileItemId; + } + + update(): void { + throw new Error("Method not implemented."); + } +} + +class DockerTreeItem extends IntegrationTreeItem { + + constructor( + public readonly id: string, + public readonly label: string, + public readonly iconPath?: { light: string | vscode.Uri; dark: string | vscode.Uri; } | vscode.ThemeIcon | undefined + ) { + super(label, vscode.TreeItemCollapsibleState.Expanded); + } + + update(): void { + console.log("Nothing to update in Docker integration tree item"); + } +} + +class DockerImageTreeItem extends IntegrationTreeItem { + static readonly iconPathPullDocker: vscode.ThemeIcon = new vscode.ThemeIcon("sync"); // NOI18 + private static readonly defaultIconPath: vscode.ThemeIcon = new vscode.ThemeIcon("archive"); // NOI18 + readonly pyrsiaIconPath: { dark: string, light: string }; + + constructor( + public readonly id: string, + public readonly label: string + ) { + super(label, vscode.TreeItemCollapsibleState.None); + this.iconPath = DockerImageTreeItem.defaultIconPath; + this.pyrsiaIconPath = { + dark: path.join(Util.getResourceImagePath(), "pyrsia_white.svg"), //TODO update to dark + light: path.join(Util.getResourceImagePath(), "pyrsia_white.svg") //TODO update to light + }; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + update(context: any): void { + this.iconPath = context.pyrsia ? this.pyrsiaIconPath : DockerImageTreeItem.defaultIconPath; + this.contextValue = context.pyrsia ? DockerIntegration.imagePyrsia : DockerIntegration.imageNotPyrsia; + } +} diff --git a/src/nodeProvider.ts b/src/nodeProvider.ts index 2fa7108..0007aa1 100644 --- a/src/nodeProvider.ts +++ b/src/nodeProvider.ts @@ -1,5 +1,5 @@ -import * as cp from 'child_process'; -import * as client from './utilities/client'; +import * as cp from "child_process"; +import * as client from "./utilities/pyrsiaClient"; export class NodeProvider { diff --git a/src/utilities/client.ts b/src/utilities/pyrsiaClient.ts similarity index 57% rename from src/utilities/client.ts rename to src/utilities/pyrsiaClient.ts index f032f57..8d0ae8a 100644 --- a/src/utilities/client.ts +++ b/src/utilities/pyrsiaClient.ts @@ -19,6 +19,10 @@ type StatusResponse = { data: string[]; }; +type TransparencyLogResponse = { + data: string[]; +}; + export const isNodeHealthy = async (): Promise => { console.log('Check node health'); const nodeUrl = `${getNodeUrl()}/v2`; @@ -61,6 +65,44 @@ export const getStatus = async (): Promise => { return data; }; +export const getDockerTransparencyLog = async (imageName: string): Promise<[]> => { + console.log(`Get docker image transparency log info for ${imageName}`); + const nodeUrl = `${getNodeUrl()}/inspect/docker`; + let data; + + try { + ({ data } = await axios.post( + nodeUrl, + { + image: imageName + } + )); + } catch (error) { + throw new Error("getDockerTransparencyLog error" + error); + } + + return (data as unknown as []); +}; + +export const requestDockerBuild = async (imageName: string): Promise<[]> => { + console.log(`Request build for docker image: ${imageName}`); + const nodeUrl = `${getNodeUrl()}/build/docker`; + let data; + + try { + ({ data } = await axios.post( + nodeUrl, + { + image: imageName + } + )); + } catch (error) { + throw new Error("requestDockerBuild error" + error); + } + + return (data as unknown as []); +}; + export const getPeers = async (): Promise => { console.log('Get node peers'); const data = await getStatus(); diff --git a/src/utilities/util.ts b/src/utilities/util.ts index 224ed47..a6e82b9 100644 --- a/src/utilities/util.ts +++ b/src/utilities/util.ts @@ -2,12 +2,14 @@ import { Uri, Webview } from "vscode"; import { NodeConfig } from "../model/NodeConfig"; import path = require("path"); import * as vscode from 'vscode'; +import * as DockerClient from "dockerode"; export class Util { private static resourcePath: string; // TODO Replace it with real, persistance storage private static config: NodeConfig = new NodeConfig(); + private static dockerClient: DockerClient; static init(context: vscode.ExtensionContext): void { Util.resourcePath = context.asAbsolutePath(path.join('resources')); @@ -28,5 +30,33 @@ export class Util { static getResourceImagePath(): string { return path.join(Util.resourcePath, "images"); } + + /** + * Get Docker Client (singleton). + * @returns {DockerClient} DockerClient + */ + static getDockerClient(): DockerClient { + if (!this.dockerClient) { + const dockerConfig: DockerClient.DockerOptions = { socketPath: '/var/run/docker.sock' }; //TODO Should be configurable. + this.dockerClient = new DockerClient(dockerConfig); + } + + return this.dockerClient; + } + + static isDebugMode() { + return process.env.VSCODE_DEBUG_MODE === "true"; + } + + static debugMessage(message: string) { + if (this.isDebugMode()) { + vscode.window.showErrorMessage(message); + } + console.debug(message); + } + + static sleep(milliseconds: number): Promise { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); + } } diff --git a/src/webviews/NodeIntegrationsView.ts b/src/webviews/IntegrationsView.ts similarity index 72% rename from src/webviews/NodeIntegrationsView.ts rename to src/webviews/IntegrationsView.ts index afe3878..f259f03 100644 --- a/src/webviews/NodeIntegrationsView.ts +++ b/src/webviews/IntegrationsView.ts @@ -1,51 +1,52 @@ // https://github.com/xojs/eslint-config-xo-typescript/issues/43 /* eslint-disable @typescript-eslint/naming-convention */ import * as vscode from 'vscode'; -import { Integration, IntegrationTreeItem } from '../integrations/Integration'; -import { Docker } from '../integrations/Docker'; +import { Integration, IntegrationTreeItem, Event } from '../integrations/api/Integration'; -export class NodeIntegrationsView { +export class IntegrationsView { + private static readonly refreshIntegrationModelCommandId: string = "pyrsia.integrations.model.update"; + private static readonly refreshIntegrationViewCommandId: string = "pyrsia.integrations.view.update"; private static readonly viewType: string = "pyrsia.node-integrations"; // NOI18 + private readonly treeViewProvider: NodeIntegrationsTreeProvider; - private readonly _view?: vscode.TreeView; constructor(context: vscode.ExtensionContext) { this.treeViewProvider = new NodeIntegrationsTreeProvider(); - // add and update the Docker integration - this.treeViewProvider.addIntegration(new Docker(context)); - this._view = vscode.window.createTreeView( - NodeIntegrationsView.viewType, + IntegrationsView.viewType, { showCollapseAll: true, treeDataProvider: this.treeViewProvider } ); - this.treeViewProvider.update(); - vscode.commands.registerCommand('pyrsia.node-integrations.tree.refresh', () => { + vscode.commands.registerCommand(IntegrationsView.refreshIntegrationModelCommandId, () => { this.treeViewProvider.update(); }); + vscode.commands.registerCommand(IntegrationsView.refreshIntegrationViewCommandId, () => { + this.treeViewProvider.refreshTreeView(); + }); + this._view.onDidChangeVisibility(() => { this.treeViewProvider.update(); }); - this._view.onDidChangeSelection((event) => { - console.log(event); + this._view.onDidChangeSelection(() => { + this.treeViewProvider.update(); }); - // this.treeViewProvider.onDidChangeSelection((event) => { - // console.log(event); - // }); - - - //vscode.window.registerTreeDataProvider(NodeIntegrationsView.viewType, this.treeViewProvider); - context.subscriptions.push(this._view); + } + + static requestIntegrationsModelUpdate(): void { + vscode.commands.executeCommand(this.refreshIntegrationModelCommandId); + } + static requestIntegrationsViewUpdate(): void { + vscode.commands.executeCommand(this.refreshIntegrationViewCommandId); } addIntegration(integration: Integration): void { @@ -95,7 +96,7 @@ class NodeIntegrationsTreeProvider implements vscode.TreeDataProvider { console.log(parentId); let children: string[] = []; for (const integration of this.integrations) { - children = children.concat(integration.getTreeItemChildren()); + children = children.concat(integration.getTreeItemChildren(parentId)); } return children; @@ -103,9 +104,8 @@ class NodeIntegrationsTreeProvider implements vscode.TreeDataProvider { update(): void { for (const integration of this.integrations) { - integration.update(); + integration.update(Event.IntegrationModelUpdate); } - this._onDidChangeTreeData.fire(undefined); } resolveTreeItem?( @@ -117,4 +117,8 @@ class NodeIntegrationsTreeProvider implements vscode.TreeDataProvider { console.log(token); return item; } + + refreshTreeView(): void { + this._onDidChangeTreeData.fire(undefined); + } } diff --git a/src/webviews/NodeConfigView.ts b/src/webviews/NodeConfigView.ts index cf40190..456d1a9 100644 --- a/src/webviews/NodeConfigView.ts +++ b/src/webviews/NodeConfigView.ts @@ -1,39 +1,27 @@ // https://github.com/xojs/eslint-config-xo-typescript/issues/43 /* eslint-disable @typescript-eslint/naming-convention */ -import * as vscode from 'vscode'; -import { Util } from '../utilities/util'; -import * as client from '../utilities/client'; -import { HelpUtil } from './HelpView'; - -// TODO With branches -// enum NodeConfigProperty { -// Hostname = "hostname", // NOI18 -// Port = "port", // NOI18 -// HostnameValue = "hostnamevalue", // NOI18 -// PortValue = "portvalue", // NOI18 -// Peers = "peers", // NOI18 -// PeersValue = "peersvalue", // NOI18 -// } +import * as vscode from "vscode"; +import { Util } from "../utilities/util"; +import * as client from "../utilities/pyrsiaClient"; +import { HelpUtil } from "./HelpView"; +import { Event, Integration } from "../integrations/api/Integration"; +import { IntegrationsView } from "./IntegrationsView"; // TODO Without branches enum NodeConfigProperty { - Status = "status", - // Hostname = "hostname", // NOI18 - // Port = "port", // NOI18 - // HostnameValue = "hostnamevalue", // NOI18 - // PortValue = "portvalue", // NOI18 + Status = "status", // NOI18 Peers = "peers", // NOI18 - Error1 = "error1", - Error2 = "error2", - // PeersValue = "peersvalue", // NOI18 + Error1 = "error1", // NOI18 + Error2 = "error2", // NOI18 } export class NodeConfigView { - public static readonly configNodeCommandId = "pyrsia.configurenode"; + public static readonly configNodeCommandId = "pyrsia.configure-node"; private static readonly viewType: string = "pyrsia.node-config"; // NOI18 private readonly treeViewProvider: NodeConfigTreeProvider; private readonly view; + private readonly integrations: Set = new Set(); constructor(context: vscode.ExtensionContext) { this.treeViewProvider = new NodeConfigTreeProvider(); @@ -45,7 +33,7 @@ export class NodeConfigView { context.subscriptions.push(this.view); - vscode.commands.registerCommand('pyrsia.node-config.tree.refresh', () => { + vscode.commands.registerCommand("pyrsia.node-config.tree.refresh", () => { this.treeViewProvider.update(); }); @@ -83,6 +71,10 @@ export class NodeConfigView { // update the UI this.update(); + // notify the integrations update the update + this.notifyNodeConfigUpdated(); + IntegrationsView.requestIntegrationsModelUpdate(); + IntegrationsView.requestIntegrationsViewUpdate(); } ); @@ -105,6 +97,16 @@ export class NodeConfigView { healthy ? this.view.title = "NODE STATUS 🟩" : this.view.title = "NODE STATUS 🟥"; }); } + + addIntegration(integration: Integration): void { + this.integrations.add(integration); + } + + private notifyNodeConfigUpdated() { + for (const integration of this.integrations) { + integration.update(Event.NodeConfigurationUpdate); + } + } } // TODO This is flat tree (no branches) @@ -186,7 +188,7 @@ class NodeTreeItem extends vscode.TreeItem { onUpdate: async (treeItem: NodeTreeItem) => { const healthy: boolean = await client.isNodeHealthy(); const { host } = Util.getNodeConfig(); - const status: string = healthy ? `Connected to '${host}'` : `Failed connecting to '${host}'`; + const status: string = healthy ? `Connected to Pyrsia node '${host}'` : `Failed connecting to Pyrsia node: '${host}'`; treeItem.label = status; treeItem.iconPath = healthy ? NodeTreeItem.cloudIcon : NodeTreeItem.brokenConnectionIcon; treeItem.command = { command: NodeConfigView.configNodeCommandId, title: "Configure Pyrsia Node" }; From 396eaa06e5ee48150844ad649a246c43af5ae583 Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Thu, 19 Jan 2023 14:33:41 -0800 Subject: [PATCH 05/16] The images refresh command added and the refresh bug fixes. --- package.json | 13 ++++ src/integrations/impl/DockerIntegration.ts | 80 +++++++++++++--------- src/webviews/IntegrationsView.ts | 17 +++-- src/webviews/NodeConfigView.ts | 3 +- 4 files changed, 75 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 3aef2e2..09e0cb5 100644 --- a/package.json +++ b/package.json @@ -72,9 +72,22 @@ "title": "Open Pyrsia Transparency Log", "shortTitle": "Open Pyrsia Transparency Log", "icon": "$(file-code)" + }, + { + "command": "pyrsia.integrations.update", + "title": "Refresh", + "shortTitle": "Refresh", + "icon": "$(refresh)" } ], "menus": { + "view/title": [ + { + "command": "pyrsia.integrations.update", + "when": "view == pyrsia.node-integrations", + "group": "navigation" + } + ], "view/item/context": [ { "command": "pyrsia.replace-docker-images", diff --git a/src/integrations/impl/DockerIntegration.ts b/src/integrations/impl/DockerIntegration.ts index 2cb9df0..ac9c446 100644 --- a/src/integrations/impl/DockerIntegration.ts +++ b/src/integrations/impl/DockerIntegration.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/member-ordering */ import * as os from "os"; import * as fsUtils from "../../utilities/fsUtils"; import { Event, Integration, IntegrationTreeItem } from "../api/Integration"; @@ -20,25 +21,22 @@ export class DockerIntegration implements Integration { private static readonly requestBuildId: string = "pyrsia.request-docker-build"; // context values - // eslint-disable-next-line @typescript-eslint/member-ordering - static readonly imageNotPyrsia = `${DockerIntegration.integrationId}.not-pyrsia`; - // eslint-disable-next-line @typescript-eslint/member-ordering - static readonly imagePyrsia = `${DockerIntegration.integrationId}.is-pyrsia`; + static readonly imageNotPyrsiaContextValue = `${DockerIntegration.integrationId}.not-pyrsia`; + static readonly imagePyrsiaContextValue = `${DockerIntegration.integrationId}.is-pyrsia`; + static readonly imageUpdatingContextValue = `${DockerIntegration.integrationId}.updating`; // tree item ids - // eslint-disable-next-line @typescript-eslint/member-ordering - static readonly configFileItemId: string = `${this.integrationId}.configfile`; + static readonly configFileItemIdPrefix: string = `${this.integrationId}.configfile`; + private static readonly imageItemIdPrefix: string = `${this.integrationId}.dockerimage`; private static readonly configItemId: string = `${this.integrationId}.configs`; private static readonly imagesItemId: string = `${this.integrationId}.images`; - private static readonly imageItemId: string = `${this.integrationId}.dockerimage`; - // docker config files search path private static confMap: Map = new Map(); private static readonly registryMirrorsConfName = "registry-mirrors"; // item names private static readonly dockerTreeItemName = "Docker"; - private static warningIconPath: vscode.ThemeIcon = new vscode.ThemeIcon("warning"); // NOI18 + private static readonly warningIconPath: vscode.ThemeIcon = new vscode.ThemeIcon("warning"); // NOI18 private readonly treeItems: Map = new Map(); private readonly dockerIconPath: { light: string | vscode.Uri; dark: string | vscode.Uri; }; @@ -61,8 +59,8 @@ export class DockerIntegration implements Integration { this.registerCommands(context); } - private static getImageTreeId(imageName: string): string { - return `${DockerIntegration.imageItemId}.${imageName}`; + private static getTreeItemImageId(imageName: string): string { + return `${DockerIntegration.imageItemIdPrefix}.${imageName}`; } async replaceImagesWithPyrsia(): Promise { @@ -78,7 +76,7 @@ export class DockerIntegration implements Integration { } // only update the images which are available in Pyrsia const transImageLog: [] = await client.getDockerTransparencyLog(imageName); - await this.updateModel(); + await this.updateModel(false); if (transImageLog.length > 0) { // remove the old images first try { @@ -110,20 +108,24 @@ export class DockerIntegration implements Integration { dockerClient.pull(imageName, (err: string, stream: any) => { console.log(err); const onFinished = (error_: unknown, output: unknown) => { - console.info(`Error: ${error_}, ${output}`); - this.treeItems.delete(DockerIntegration.getImageTreeId(imageName)); - IntegrationsView.requestIntegrationsModelUpdate(); - IntegrationsView.requestIntegrationsViewUpdate(); + if (error_) { + console.error(error_); + } + console.log(output); + this.treeItems.delete(DockerIntegration.getTreeItemImageId(imageName)); + IntegrationsView.requestIntegrationsUpdate(); }; const onProgress = (event: unknown) => { console.log(event); - const treeItem = this.treeItems.get(DockerIntegration.getImageTreeId(imageName)); + const treeItem = this.treeItems.get(DockerIntegration.getTreeItemImageId(imageName)); if (treeItem) { - treeItem.label = `Replacing '${imageName}' with Pyrsia image.`; + treeItem.label = `Updating '${imageName}' with Pyrsia image.`; treeItem.iconPath = DockerImageTreeItem.iconPathPullDocker; + treeItem.contextValue = DockerIntegration.imageUpdatingContextValue; } - IntegrationsView.requestIntegrationsModelUpdate(); + // let the model update know that the pulling is in progress which means some images might be not be present yet + this.updateModel(true); IntegrationsView.requestIntegrationsViewUpdate(); }; dockerClient.modem.followProgress(stream, onFinished, onProgress); @@ -131,8 +133,7 @@ export class DockerIntegration implements Integration { }); } catch (err) { Util.debugMessage(`Couldn't replace image: ${imageInfo.Labels}, error: ${err}`); - IntegrationsView.requestIntegrationsModelUpdate(); - IntegrationsView.requestIntegrationsViewUpdate(); + IntegrationsView.requestIntegrationsUpdate(); } } }); @@ -150,14 +151,14 @@ export class DockerIntegration implements Integration { // eslint-disable-next-line no-case-declarations const configItems = [... this.treeItems].map(([, value]) => { return value.id; - }).filter(id => id?.includes(DockerIntegration.configFileItemId)); + }).filter(id => id?.includes(DockerIntegration.configFileItemIdPrefix)); children = children.concat((configItems as string[])); break; case DockerIntegration.imagesItemId: // eslint-disable-next-line no-case-declarations const imageItems = [... this.treeItems].map(([, value]) => { return value.id; - }).filter(id => id?.includes(DockerIntegration.imageItemId)); + }).filter(id => id?.includes(DockerIntegration.imageItemIdPrefix)); children = children.concat((imageItems as string[])); break; default: @@ -179,7 +180,7 @@ export class DockerIntegration implements Integration { async update(event: Event): Promise { switch (event) { case Event.IntegrationModelUpdate: { - await this.updateModel(); + await this.updateModel(false); break; } case Event.NodeConfigurationUpdate: { @@ -191,13 +192,13 @@ export class DockerIntegration implements Integration { break; } default:{ - this.updateModel(); + this.updateModel(false); } } IntegrationsView.requestIntegrationsViewUpdate(); } - private async updateModel() { + private async updateModel(pullingInProgress: boolean) { // check if the docker and node is up let isDockerUp = true; try { @@ -227,7 +228,7 @@ export class DockerIntegration implements Integration { } fsUtils.findByName(confPath, fileName).then((confFilePath) => { if (confFilePath) { - const id = `${DockerIntegration.configFileItemId}.${confFilePath}`; + const id = `${DockerIntegration.configFileItemIdPrefix}.${confFilePath}`; const label = `${confFilePath}`; this.treeItems.set( id, @@ -237,12 +238,13 @@ export class DockerIntegration implements Integration { console.log(`No configuration for 'Docker' - ${path.join(confPath, fileName)}`); } }); - IntegrationsView.requestIntegrationsViewUpdate(); } - // look for docker images + // get the local docker images const dockerClient = Util.getDockerClient(); const images = await dockerClient.listImages(); + const currentImages: string[] = []; + images.forEach(async (image) => { const imageName = image.RepoTags?.join(); if (imageName?.startsWith("")) { @@ -252,11 +254,12 @@ export class DockerIntegration implements Integration { if (!imageName) { return; } - const id = DockerIntegration.getImageTreeId(imageName); + const id = DockerIntegration.getTreeItemImageId(imageName); const imageItem = new DockerImageTreeItem( id, imageName ); + currentImages.push(id); // check if image exists in pyrsia const transImageLog: [] = await client.getDockerTransparencyLog(imageName); imageItem.update({ pyrsia: transImageLog.length > 0 }); @@ -264,6 +267,19 @@ export class DockerIntegration implements Integration { // add item to the tree items map this.treeItems.set(id, imageItem); }); + + // remove deleted images + if (!pullingInProgress) { + for (const key of this.treeItems.keys()) { + // skip non image tree items + if (!key.startsWith(DockerIntegration.imageItemIdPrefix)) { + continue; + } + if (!currentImages.includes(key)) { + this.treeItems.delete(key); + } + } + } IntegrationsView.requestIntegrationsViewUpdate(); } } @@ -475,7 +491,7 @@ class DockerConfigTreeItem extends IntegrationTreeItem { }; this.iconPath = DockerConfigTreeItem.iconPath; - this.contextValue = DockerIntegration.configFileItemId; + this.contextValue = DockerIntegration.configFileItemIdPrefix; } update(): void { @@ -518,6 +534,6 @@ class DockerImageTreeItem extends IntegrationTreeItem { // eslint-disable-next-line @typescript-eslint/no-explicit-any update(context: any): void { this.iconPath = context.pyrsia ? this.pyrsiaIconPath : DockerImageTreeItem.defaultIconPath; - this.contextValue = context.pyrsia ? DockerIntegration.imagePyrsia : DockerIntegration.imageNotPyrsia; + this.contextValue = context.pyrsia ? DockerIntegration.imagePyrsiaContextValue : DockerIntegration.imageNotPyrsiaContextValue; } } diff --git a/src/webviews/IntegrationsView.ts b/src/webviews/IntegrationsView.ts index f259f03..49bf177 100644 --- a/src/webviews/IntegrationsView.ts +++ b/src/webviews/IntegrationsView.ts @@ -1,11 +1,12 @@ // https://github.com/xojs/eslint-config-xo-typescript/issues/43 /* eslint-disable @typescript-eslint/naming-convention */ import * as vscode from 'vscode'; -import { Integration, IntegrationTreeItem, Event } from '../integrations/api/Integration'; +import { Integration, IntegrationTreeItem, Event } from '../integrations/api/Integration'; // NOI18 export class IntegrationsView { - private static readonly refreshIntegrationModelCommandId: string = "pyrsia.integrations.model.update"; - private static readonly refreshIntegrationViewCommandId: string = "pyrsia.integrations.view.update"; + private static readonly refreshIntegrationModelCommandId: string = "pyrsia.integrations.model.update"; // NOI18 + private static readonly refreshIntegrationViewCommandId: string = "pyrsia.integrations.view.update"; // NOI18 + private static readonly refreshIntegrationCommandId: string = "pyrsia.integrations.update"; // NOI18 private static readonly viewType: string = "pyrsia.node-integrations"; // NOI18 private readonly treeViewProvider: NodeIntegrationsTreeProvider; @@ -30,6 +31,11 @@ export class IntegrationsView { this.treeViewProvider.refreshTreeView(); }); + vscode.commands.registerCommand(IntegrationsView.refreshIntegrationCommandId, () => { + this.treeViewProvider.update(); + this.treeViewProvider.refreshTreeView(); + }); + this._view.onDidChangeVisibility(() => { this.treeViewProvider.update(); }); @@ -49,6 +55,10 @@ export class IntegrationsView { vscode.commands.executeCommand(this.refreshIntegrationViewCommandId); } + static requestIntegrationsUpdate(): void { + vscode.commands.executeCommand(this.refreshIntegrationCommandId); + } + addIntegration(integration: Integration): void { this.treeViewProvider.addIntegration(integration); } @@ -61,7 +71,6 @@ export class IntegrationsView { } class NodeIntegrationsTreeProvider implements vscode.TreeDataProvider { - // on change tree data private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); diff --git a/src/webviews/NodeConfigView.ts b/src/webviews/NodeConfigView.ts index 456d1a9..435c8fd 100644 --- a/src/webviews/NodeConfigView.ts +++ b/src/webviews/NodeConfigView.ts @@ -73,8 +73,7 @@ export class NodeConfigView { this.update(); // notify the integrations update the update this.notifyNodeConfigUpdated(); - IntegrationsView.requestIntegrationsModelUpdate(); - IntegrationsView.requestIntegrationsViewUpdate(); + IntegrationsView.requestIntegrationsUpdate(); } ); From 76f393957f19f2d0f51661172733e924912bd310 Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Thu, 19 Jan 2023 18:59:41 -0800 Subject: [PATCH 06/16] Storage layer implemented --- .vscode/launch.json | 10 +- .vscode/settings.json | 2 + CHANGELOG.md | 4 +- README.md | 7 +- package.json | 48 +++-- ...docker_small.svg => docker_small_dark.svg} | 0 resources/images/pyrsia_color.svg | 15 -- resources/images/pyrsia_white_small.svg | 8 - src/api/Integration.ts | 44 ++++ src/api/NodeConfig.ts | 38 ++++ src/extension.ts | 28 +-- .../{impl => }/DockerIntegration.ts | 196 ++++++++++-------- src/integrations/api/Integration.ts | 28 --- src/model/NodeConfig.ts | 27 --- src/nodeProvider.ts | 36 ---- src/test/suite/extension.test.ts | 5 +- src/utilities/fsUtils.ts | 17 -- src/utilities/pyrsiaClient.ts | 50 +++-- src/utilities/util.ts | 165 ++++++++++++--- src/webview-ui/main.js | 36 ---- src/webview-ui/styles.css | 124 ----------- src/webviews/HelpView.ts | 55 +++-- src/webviews/IntegrationsView.ts | 64 +++--- src/webviews/NodeConfigView.ts | 119 ++++++----- src/webviews/NodeStatusView.ts | 153 -------------- 25 files changed, 557 insertions(+), 722 deletions(-) rename resources/images/{docker_small.svg => docker_small_dark.svg} (100%) delete mode 100644 resources/images/pyrsia_color.svg delete mode 100644 resources/images/pyrsia_white_small.svg create mode 100644 src/api/Integration.ts create mode 100644 src/api/NodeConfig.ts rename src/integrations/{impl => }/DockerIntegration.ts (73%) delete mode 100644 src/integrations/api/Integration.ts delete mode 100644 src/model/NodeConfig.ts delete mode 100644 src/nodeProvider.ts delete mode 100644 src/utilities/fsUtils.ts delete mode 100644 src/webview-ui/main.js delete mode 100644 src/webview-ui/styles.css delete mode 100644 src/webviews/NodeStatusView.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 5900042..5e5afd6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,8 +1,5 @@ { - // TODO - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + // https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { @@ -21,8 +18,5 @@ } } - ], - "env": { - "DEBUG": "DUPA" - }, + ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index a1a9848..9943c44 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,8 @@ "pyrsiaoverview", "reportissue", "VSIX", + "warningconnection", + "warningupdatenode", "webviews" ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bc00d7..6f9ee7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ # Change Log ## [Unreleased] +# JFrog MS Teams App Changelog -- Initial release \ No newline at end of file +## [0.0.1] - Apr 25th, 2022 +* Initial API, views and the basic Pyrsia node support. diff --git a/README.md b/README.md index cd963b8..e1f9d9b 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,15 @@ npm run test - Run the tests and linter. ```sh + npm run compile npm run lint npm run test ``` ## How to package, install and uninstall Pyrsia extension in the IDE +The Pyrsia extension is not available in the VS Code store yet, it's necessary to manually install the extension as described below. + ### Package and Install (side-load extension) - Install [Visual Studio Code Extensions](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#vsce) with the following command. @@ -46,7 +49,7 @@ npm run test npm install -g @vscode/vsce ``` -- In the repository folder compile and package the extension as follows. +- In the extension repository folder package the extension as follows. ```sh vsce package @@ -55,7 +58,7 @@ npm run test - If the packaging was successful the last line of the VSCE logs should contain the `vsix` file path, for example: ```sh - DONE Packaged: /home/john/repositories/pyrsia-vscode-extension/pyrsia-integration-0.0.1.vsix (960 files, 2.2MB) + DONE Packaged: /home/joed/repositories/pyrsia-vscode-extension/pyrsia-integration-0.0.1.vsix (960 files, 2.2MB) ``` - Copy the `vsix` file path and install the extension as follows. diff --git a/package.json b/package.json index 09e0cb5..c406e8a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "pyrsia-integration", + "name": "pyrsia-extension", "displayName": "Pyrsia", - "description": "Pyrsia Integration for VS Code", + "description": "Pyrsia Extension for VS Code", "version": "0.0.1", "repository": { "type": "git", @@ -44,31 +44,31 @@ }, "commands": [ { - "command": "pyrsia.replace-docker-images", - "title": "Replace Docker Images with Pyrsia images", + "command": "pyrsia.docker.replace-images", + "title": "Reload Docker Images with Pyrsia", "shortTitle": "Replace images", "icon": "$(refresh)" }, { - "command": "pyrsia.configure-node", + "command": "pyrsia.node-config.update-url", "title": "Update Pyrsia Node URL", "shortTitle": "Update Pyrsia node URL", "icon": "$(gear)" }, { - "command": "pyrsia.update-docker-conf", - "title": "Add Pyrsia to Docker Configuration", - "shortTitle": "Add Pyrsia to Docker configuration", + "command": "pyrsia.docker.update-config", + "title": "Open Docker Configuration", + "shortTitle": "Open and Update Docker Configuration", "icon": "$(gear)" }, { - "command": "pyrsia.request-docker-build", + "command": "pyrsia.docker.request-build", "title": "Add Image to Pyrsia", "shortTitle": "Add selected image to Pyrsia", "icon": "$(server-environment)" }, { - "command": "pyrsia.open-docker-trans-log", + "command": "pyrsia.docker.open-trans-log", "title": "Open Pyrsia Transparency Log", "shortTitle": "Open Pyrsia Transparency Log", "icon": "$(file-code)" @@ -78,6 +78,12 @@ "title": "Refresh", "shortTitle": "Refresh", "icon": "$(refresh)" + }, + { + "command": "pyrsia.node-config.update-view", + "title": "Refresh", + "shortTitle": "Refresh", + "icon": "$(refresh)" } ], "menus": { @@ -86,36 +92,36 @@ "command": "pyrsia.integrations.update", "when": "view == pyrsia.node-integrations", "group": "navigation" + }, + { + "command": "pyrsia.node-config.update-view", + "when": "view == pyrsia.node-config", + "group": "navigation" } ], "view/item/context": [ { - "command": "pyrsia.replace-docker-images", + "command": "pyrsia.docker.replace-images", "when": "view == pyrsia.node-integrations && viewItem == pyrsia.docker", "group": "pyrsia-integration" }, { - "command": "pyrsia.configure-node", + "command": "pyrsia.node-config.update-url", "when": "view == pyrsia.node-config", "group": "pyrsia-config" }, { - "command": "pyrsia.update-docker-conf", - "when": "view == pyrsia.node-integrations && viewItem == pyrsia.docker.configfile", - "group": "pyrsia-integration" - }, - { - "command": "pyrsia.request-docker-build", - "when": "view == pyrsia.node-integrations && viewItem == pyrsia.docker.not-pyrsia", + "command": "pyrsia.docker.update-config", + "when": "view == pyrsia.node-integrations && viewItem == pyrsia.docker.config-file", "group": "pyrsia-integration" }, { - "command": "pyrsia.request-docker-build", + "command": "pyrsia.docker.request-build", "when": "view == pyrsia.node-integrations && viewItem == pyrsia.docker.not-pyrsia", "group": "pyrsia-integration" }, { - "command": "pyrsia.open-docker-trans-log", + "command": "pyrsia.docker.open-trans-log", "when": "view == pyrsia.node-integrations && viewItem == pyrsia.docker.is-pyrsia", "group": "pyrsia-integration" } diff --git a/resources/images/docker_small.svg b/resources/images/docker_small_dark.svg similarity index 100% rename from resources/images/docker_small.svg rename to resources/images/docker_small_dark.svg diff --git a/resources/images/pyrsia_color.svg b/resources/images/pyrsia_color.svg deleted file mode 100644 index d60f56e..0000000 --- a/resources/images/pyrsia_color.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/resources/images/pyrsia_white_small.svg b/resources/images/pyrsia_white_small.svg deleted file mode 100644 index 32bd6de..0000000 --- a/resources/images/pyrsia_white_small.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/api/Integration.ts b/src/api/Integration.ts new file mode 100644 index 0000000..a9f07f0 --- /dev/null +++ b/src/api/Integration.ts @@ -0,0 +1,44 @@ +// https://github.com/xojs/eslint-config-xo-typescript/issues/43 +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as vscode from 'vscode'; + +// Integration events +export enum Event { + IntegrationModelUpdate, + NodeConfigurationUpdate, +} + +/** +* Integration interface (e.g. DockerIntegration.ts, maven, etc). +* It allows plugging a new tree into and commands the integration view. +*/ +export interface Integration { + + /** + * Get a tree item for the provided tree item ID. + * @param {string} treeItemId tree item id + * @returns {IntegrationTreeItem} integration tree item + */ + getTreeItem(treeItemId: string): IntegrationTreeItem | undefined; + + /** + * Returns an array of children ids (treeItemId). + * @param {string} parentId + * @returns {string[]} array of children IDs (treeItemId) + */ + getTreeItemChildren(parentId?: string): string[]; + + /** + * This method is called by the external source and notifies the integration about a specific event. + * @param {Integration.Event} event + */ + update(event: Event): void; +} + +/** +* Integration Tree Item class +*/ +export abstract class IntegrationTreeItem extends vscode.TreeItem { + abstract update(context: never): void; +} diff --git a/src/api/NodeConfig.ts b/src/api/NodeConfig.ts new file mode 100644 index 0000000..2b72edb --- /dev/null +++ b/src/api/NodeConfig.ts @@ -0,0 +1,38 @@ + +/** + * This interface is used to define most of the configs related to the Pyrsia node. + * There is on implementation of this interface that can be used as example (NodeConfigImpl.ts) + */ +export interface NodeConfig { + + get defaultUrl(): URL; + + /** + * Getter, provides the node supported protocol (e.g http || https) + * @returns {string} protocol + */ + get protocol(): string; + + /** + * Getter, returns the host name as string (e.g 'localhost:7888'). + * @returns {string} host + */ + get host(): string; + + /** + * Getter, returns Pyrsia nodes's URL. + * @returns {URL} url + */ + get url(): URL; + + /** + * Setter, used to set node's url. + * @param {URL | string | undefined} url + */ + set url(nodeUrl: URL | string | undefined); + + /** + * Getter, returns the hostname but also always includes the supported protocol (e.g. http://localhost:7888) + */ + get hostWithProtocol(): string; +} diff --git a/src/extension.ts b/src/extension.ts index 299c028..42e02e2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,40 +2,30 @@ import * as vscode from "vscode"; import { NodeConfigView } from "./webviews/NodeConfigView"; // import { NodeStatusViewProvider } from "./webviews/NodeStatusView"; import { IntegrationsView as IntegrationsView } from "./webviews/IntegrationsView"; -import { Util } from "./utilities/util"; +import { Util } from "./utilities/Util"; import { HelpView } from "./webviews/HelpView"; -import { Integration } from "./integrations/api/Integration"; -import { DockerIntegration } from "./integrations/impl/DockerIntegration"; +import { Integration } from "./api/Integration"; +import { DockerIntegration } from "./integrations/DockerIntegration"; export const activate = (context: vscode.ExtensionContext) => { - // initialize the util + // Init the extension utils Util.init(context); - // Node status web view provider, this is debug only view, disabled for now - //new NodeStatusViewProvider(context); - - // Create integrations + // Create docker integration const dockerIntegration: Integration = new DockerIntegration(context); - // Node status config view + // Create the node config view const nodeConfigView = new NodeConfigView(context); nodeConfigView.addIntegration(dockerIntegration); - // Node status config view + // Create the integrations view const integrationView = new IntegrationsView(context); integrationView.addIntegration(dockerIntegration); - // Node status config view + // Create Help view new HelpView(context); - - //Notify the Node Config View when connected to node - // nodeView.onDidConnect({ - // onDidConnect() { - // nodeConfigView.update(); - // }, - // }); }; export const deactivate = () => { - console.log("Pyrsia extension deactivated"); // TODO + console.debug("Pyrsia extension deactivated"); }; diff --git a/src/integrations/impl/DockerIntegration.ts b/src/integrations/DockerIntegration.ts similarity index 73% rename from src/integrations/impl/DockerIntegration.ts rename to src/integrations/DockerIntegration.ts index ac9c446..5d2d8dd 100644 --- a/src/integrations/impl/DockerIntegration.ts +++ b/src/integrations/DockerIntegration.ts @@ -1,82 +1,105 @@ /* eslint-disable @typescript-eslint/member-ordering */ import * as os from "os"; -import * as fsUtils from "../../utilities/fsUtils"; import { Event, Integration, IntegrationTreeItem } from "../api/Integration"; import * as path from 'path'; -import { Util } from "../../utilities/util"; +import { Util } from "../utilities/Util"; import * as vscode from "vscode"; -import * as client from "../../utilities/pyrsiaClient"; -import { IntegrationsView } from "../../webviews/IntegrationsView"; +import * as client from "../utilities/pyrsiaClient"; +import { IntegrationsView } from "../webviews/IntegrationsView"; +/** + * Implements Docker support for Pyrsia. + */ export class DockerIntegration implements Integration { - //confirm options + //dialog options static readonly confirmOption = "Yes"; static readonly cancelOption = "No"; + static readonly closeOption = "Close"; // command Ids - static readonly updateDockerConfCommandId = "pyrsia.update-docker-conf"; - static readonly replaceDockerImagesCommandId = "pyrsia.replace-docker-images"; - private static readonly openDockerTransparencyLog = "pyrsia.open-docker-trans-log"; - private static readonly integrationId: string = "pyrsia.docker"; - private static readonly requestBuildId: string = "pyrsia.request-docker-build"; + static readonly updateDockerConfCommandId = "pyrsia.docker.update-config"; // NOI18N + static readonly reloadDockerImagesCommandId = "pyrsia.docker.replace-images"; // NOI18N + private static readonly openDockerTransparencyLog = "pyrsia.docker.open-trans-log"; // NOI18N + private static readonly integrationId: string = "pyrsia.docker"; // NOI18N + private static readonly requestBuildId: string = "pyrsia.docker.request-build"; // NOI18N // context values - static readonly imageNotPyrsiaContextValue = `${DockerIntegration.integrationId}.not-pyrsia`; - static readonly imagePyrsiaContextValue = `${DockerIntegration.integrationId}.is-pyrsia`; - static readonly imageUpdatingContextValue = `${DockerIntegration.integrationId}.updating`; + static readonly imageNotPyrsiaContextValue = "pyrsia.docker.not-pyrsia"; // NOI18N + static readonly imagePyrsiaContextValue = "pyrsia.docker.is-pyrsia"; // NOI18N + static readonly imageUpdatingContextValue = "pyrsia.docker.updating"; // NOI18N // tree item ids - static readonly configFileItemIdPrefix: string = `${this.integrationId}.configfile`; - private static readonly imageItemIdPrefix: string = `${this.integrationId}.dockerimage`; - private static readonly configItemId: string = `${this.integrationId}.configs`; - private static readonly imagesItemId: string = `${this.integrationId}.images`; - // docker config files search path - private static confMap: Map = new Map(); - private static readonly registryMirrorsConfName = "registry-mirrors"; + static readonly configFileItemIdPrefix: string = "pyrsia.docker.config-file"; + private static readonly imageItemIdPrefix: string = "pyrsia.docker.docker-image"; + private static readonly configItemId: string = "pyrsia.docker.configs"; + private static readonly imagesItemId: string = "pyrsia.docker.images"; + + // pre defined docker config files (this is where we look for the docker config) + private static dockerConfigPathsMap: Map = new Map(); + // Used in the docker configuration logic (the property we have to update) + private static readonly registryMirrorsName = "registry-mirrors"; - // item names - private static readonly dockerTreeItemName = "Docker"; - private static readonly warningIconPath: vscode.ThemeIcon = new vscode.ThemeIcon("warning"); // NOI18 - + // 'static' tree items props + private static readonly mainTreeItemName = "Docker"; + private static readonly configTreeItemName = "Configuration"; + private static readonly imagesTreeItemName = "Images"; + private static readonly warningIconPath: vscode.ThemeIcon = new vscode.ThemeIcon("warning"); // NOI18N + private readonly mainTreeItemIconPath: { light: string | vscode.Uri; dark: string | vscode.Uri; }; + + // this is where we store all of the tree items private readonly treeItems: Map = new Map(); - private readonly dockerIconPath: { light: string | vscode.Uri; dark: string | vscode.Uri; }; static { - DockerIntegration.confMap.set(path.join(os.homedir(), ".docker"), "daemon.json"); + // TODO add support for windows! + DockerIntegration.dockerConfigPathsMap.set(path.join(os.homedir(), ".docker"), "daemon.json"); } constructor(context: vscode.ExtensionContext) { - // get the icon paths - this.dockerIconPath = { - dark: path.join(Util.getResourceImagePath(), "docker_small.svg"), //TODO update to dark - light: path.join(Util.getResourceImagePath(), "docker_small.svg") //TODO update to light + // get the icon info for the main tree item (Docker) + this.mainTreeItemIconPath = { + dark: path.join(Util.getResourceImagePath(), "docker_small_dark.svg"), + light: path.join(Util.getResourceImagePath(), "docker_small_dark.svg") //TODO create the "light" icon }; // create and add the "non-dynamic" docker tree items. - this.createTreeItems(this.treeItems); + this.createBaseTreeItems(); // register the docker commands this.registerCommands(context); } + /** + * Returns the image tree item ID (only docker images) + * @param {string} imageName - docker image name (tags) + * @returns {string} - tree item ID + */ private static getTreeItemImageId(imageName: string): string { return `${DockerIntegration.imageItemIdPrefix}.${imageName}`; } + /** + * This function replaces the local docker images and pulls them again, + * preferable from Pyrsia if properly configured and if available in Pyrsia. + * @return {Promise} void + */ async replaceImagesWithPyrsia(): Promise { - // look for docker images + // TODO This logic should check two conditions and fail if not met + // 1) If docker is configured to use Pyrsia node + // 2) If the node is listed as docker's proxy (registry) + + // get the local docker images const dockerClient = Util.getDockerClient(); const images = await dockerClient.listImages(); const allContainers = await dockerClient.listContainers({ "all": true }); - images.forEach(async (imageInfo) => { + for (const imageInfo of images) { const imageName = imageInfo.RepoTags?.join(); - // no image tags? => skip the image + // no image tags? => skip it if (!imageName) { - return; + continue; } - // only update the images which are available in Pyrsia + // only update the images which are available in Pyrsia + // eslint-disable-next-line no-await-in-loop const transImageLog: [] = await client.getDockerTransparencyLog(imageName); - await this.updateModel(false); if (transImageLog.length > 0) { // remove the old images first try { @@ -84,64 +107,73 @@ export class DockerIntegration implements Integration { const containers = allContainers.filter((container) => { return container.Image === imageName; }); + + // if the image has containers go to hell (skip it and warn the user) if (containers.length > 0) { - await vscode.window.showErrorMessage( - // eslint-disable-next-line max-len - `Replacing the docker images failed because '${imageName}' image - has containers, please remove the relevant docker containers and try again.`, - "Close" + vscode.window.showErrorMessage( + `Reloading the '${imageName}' docker image + failed because it has container(s) attached, please remove the container(s) and try again.`, + DockerIntegration.closeOption ); - return; + continue; } - // delete the image + + // delete the image first before pulling dockerClient.getImage(imageInfo.Id).remove({ force: true }, (error) => { if (error) { + // something went wrong, warn the user then go to the next image vscode.window.showErrorMessage( - // eslint-disable-next-line max-len - `Replacing the docker images failed because '${imageName}' image - has containers, please remove the relevant docker containers and try again.`, - "Close" + `Reloading the '${imageName}' docker image + failed, Error: ${error}`, + DockerIntegration.closeOption ); return; } + // pull the deleted image and show the progress (icons in the Integrations view) // eslint-disable-next-line @typescript-eslint/no-explicit-any dockerClient.pull(imageName, (err: string, stream: any) => { console.log(err); + + // this one is executed when the pulling is done const onFinished = (error_: unknown, output: unknown) => { if (error_) { console.error(error_); } console.log(output); + // delete the tree item representing the image, it will be recreated on the next update this.treeItems.delete(DockerIntegration.getTreeItemImageId(imageName)); + // request the view (UI) update IntegrationsView.requestIntegrationsUpdate(); }; + // this method is periodically called as the image is being pulled const onProgress = (event: unknown) => { console.log(event); const treeItem = this.treeItems.get(DockerIntegration.getTreeItemImageId(imageName)); if (treeItem) { - treeItem.label = `Updating '${imageName}' with Pyrsia image.`; + treeItem.label = `Pulling '${imageName}'`; treeItem.iconPath = DockerImageTreeItem.iconPathPullDocker; treeItem.contextValue = DockerIntegration.imageUpdatingContextValue; } - // let the model update know that the pulling is in progress which means some images might be not be present yet + // request the model the view (UI) update this.updateModel(true); IntegrationsView.requestIntegrationsViewUpdate(); }; + // request the view (UI) update dockerClient.modem.followProgress(stream, onFinished, onProgress); }); }); } catch (err) { + // pulling unsuccessfully, show the error (only in the debug mode) Util.debugMessage(`Couldn't replace image: ${imageInfo.Labels}, error: ${err}`); + // request the view (UI) update IntegrationsView.requestIntegrationsUpdate(); } } - }); + } } getTreeItemChildren(parentId?: string | undefined): string[] { - console.log(`${parentId}`); - let children: string[] = []; switch (parentId) { case DockerIntegration.integrationId: @@ -173,10 +205,6 @@ export class DockerIntegration implements Integration { return this.treeItems.get(treeItemId); } - getId(): string { - return DockerIntegration.integrationId; - } - async update(event: Event): Promise { switch (event) { case Event.IntegrationModelUpdate: { @@ -211,22 +239,23 @@ export class DockerIntegration implements Integration { const dockerTreeItem = this.treeItems.get(DockerIntegration.integrationId); if (dockerTreeItem && (!isDockerUp || !isPyrsiaNodeUp)) { // create warning tree item and hide the rest of the tree items - dockerTreeItem.label = `${DockerIntegration.dockerTreeItemName} (Pyrsia node or Docker is disconnected)`; + dockerTreeItem.label = `${DockerIntegration.mainTreeItemName} (Pyrsia Node or Docker is unavailable)`; dockerTreeItem.tooltip = "Please make sure that Docker service and Pyrsia node is up and configured"; dockerTreeItem.iconPath = DockerIntegration.warningIconPath; dockerTreeItem.collapsibleState = vscode.TreeItemCollapsibleState.None; dockerTreeItem.command = undefined; + dockerTreeItem.contextValue = undefined; } else { // recreate the docker tree item this.createDockerTreeItem(); // find the docker conf file(s) (macos, linux). TODO Windows - for (const confPath of DockerIntegration.confMap.keys()) { - const fileName = DockerIntegration.confMap.get(confPath); + for (const confPath of DockerIntegration.dockerConfigPathsMap.keys()) { + const fileName = DockerIntegration.dockerConfigPathsMap.get(confPath); if (!fileName) { throw new Error("Configuration file name cannot be null"); } - fsUtils.findByName(confPath, fileName).then((confFilePath) => { + Util.findFile(confPath, fileName).then((confFilePath) => { if (confFilePath) { const id = `${DockerIntegration.configFileItemIdPrefix}.${confFilePath}`; const label = `${confFilePath}`; @@ -245,14 +274,14 @@ export class DockerIntegration implements Integration { const images = await dockerClient.listImages(); const currentImages: string[] = []; - images.forEach(async (image) => { + for (const image of images) { const imageName = image.RepoTags?.join(); if (imageName?.startsWith("")) { - return; + continue; } // no image tags? => skip the image if (!imageName) { - return; + continue; } const id = DockerIntegration.getTreeItemImageId(imageName); const imageItem = new DockerImageTreeItem( @@ -261,12 +290,13 @@ export class DockerIntegration implements Integration { ); currentImages.push(id); // check if image exists in pyrsia + // eslint-disable-next-line no-await-in-loop const transImageLog: [] = await client.getDockerTransparencyLog(imageName); imageItem.update({ pyrsia: transImageLog.length > 0 }); // add item to the tree items map this.treeItems.set(id, imageItem); - }); + } // remove deleted images if (!pullingInProgress) { @@ -308,9 +338,9 @@ export class DockerIntegration implements Integration { // Get the docker config as JSON object const dockerConfigJson = JSON.parse(textDocument.getText()); // Get the current node configuration - const { host } = Util.getNodeConfig(); + const host = Util.getNodeConfig().hostWithProtocol; // Check if the docker config has to be updated. - let registryMirrors: string[] = dockerConfigJson[DockerIntegration.registryMirrorsConfName]; + let registryMirrors: string[] = dockerConfigJson[DockerIntegration.registryMirrorsName]; let updateConfig = false; if (registryMirrors) { updateConfig = !!registryMirrors.find((mirror: string) => { @@ -334,7 +364,7 @@ export class DockerIntegration implements Integration { textEditor.edit(edit => { if (!registryMirrors) { // no mirrors found, add one for the pyrsia node registryMirrors = []; - dockerConfigJson[DockerIntegration.registryMirrorsConfName] = registryMirrors; + dockerConfigJson[DockerIntegration.registryMirrorsName] = registryMirrors; } // update document only when the docker config was updated registryMirrors.push(host); @@ -365,10 +395,10 @@ export class DockerIntegration implements Integration { // docker command to open and update configuration editor command for the docker integration const replaceDockerWithPyrsiaImages = vscode.commands.registerCommand( - DockerIntegration.replaceDockerImagesCommandId, + DockerIntegration.reloadDockerImagesCommandId, async () => { const result = await vscode.window.showInformationMessage( - "👋 Are you sure you'd like to replace all local docker images with the Pyrsia images?", + "👋 This operation will attempt to replace the local Docker images with images hosted by Pyrsia? Would you like to continue?", DockerIntegration.confirmOption, DockerIntegration.cancelOption ); @@ -440,41 +470,43 @@ export class DockerIntegration implements Integration { private createDockerTreeItem() { // create "Docker" tree item - const dockerTreeItem = new DockerTreeItem(DockerIntegration.integrationId, "Docker", this.dockerIconPath); + const dockerTreeItem = new DockerTreeItem(DockerIntegration.integrationId, "Docker", this.mainTreeItemIconPath); dockerTreeItem.contextValue = DockerIntegration.integrationId; this.treeItems.set(DockerIntegration.integrationId, dockerTreeItem); } - private createTreeItems(treeItems: Map) { + private createBaseTreeItems() { // create "Docker" tree item this.createDockerTreeItem(); // create Docker "Configuration" tree item - treeItems.set( + this.treeItems.set( DockerIntegration.configItemId, new DockerTreeItem( DockerIntegration.configItemId, - "Configuration", - new vscode.ThemeIcon("gear") // NOI18 + DockerIntegration.configTreeItemName, + new vscode.ThemeIcon("gear") // NOI18N ) ); // create docker "Images" tree item const imagesTreeItem = new DockerTreeItem( DockerIntegration.imagesItemId, - "Images", - new vscode.ThemeIcon("folder-library") // NOI18 + DockerIntegration.imagesTreeItemName, + new vscode.ThemeIcon("folder-library") // NOI18N ); imagesTreeItem.contextValue = DockerIntegration.integrationId; - treeItems.set( + this.treeItems.set( DockerIntegration.imagesItemId, imagesTreeItem ); } } +// Docker Tree Item which represents the Docker configuration files class DockerConfigTreeItem extends IntegrationTreeItem { - private static iconPath: vscode.ThemeIcon = new vscode.ThemeIcon("go-to-file"); // NOI18 + // icon (path) + private static iconPath: vscode.ThemeIcon = new vscode.ThemeIcon("go-to-file"); // NOI18N constructor( public label: string, @@ -487,7 +519,7 @@ class DockerConfigTreeItem extends IntegrationTreeItem { this.command = { arguments: [this], command: DockerIntegration.updateDockerConfCommandId, - title: "Open Docker configuration file" + title: "Open Docker Configuration File" }; this.iconPath = DockerConfigTreeItem.iconPath; @@ -515,8 +547,8 @@ class DockerTreeItem extends IntegrationTreeItem { } class DockerImageTreeItem extends IntegrationTreeItem { - static readonly iconPathPullDocker: vscode.ThemeIcon = new vscode.ThemeIcon("sync"); // NOI18 - private static readonly defaultIconPath: vscode.ThemeIcon = new vscode.ThemeIcon("archive"); // NOI18 + static readonly iconPathPullDocker: vscode.ThemeIcon = new vscode.ThemeIcon("sync"); // NOI18N + private static readonly defaultIconPath: vscode.ThemeIcon = new vscode.ThemeIcon("archive"); // NOI18N readonly pyrsiaIconPath: { dark: string, light: string }; constructor( diff --git a/src/integrations/api/Integration.ts b/src/integrations/api/Integration.ts deleted file mode 100644 index eda8efd..0000000 --- a/src/integrations/api/Integration.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as vscode from 'vscode'; -// https://github.com/xojs/eslint-config-xo-typescript/issues/43 -/* eslint-disable @typescript-eslint/naming-convention */ - -// Help list -export enum Event { - IntegrationModelUpdate, - NodeConfigurationUpdate, -} - -export interface Integration { - - getTreeItem(treeItemId: string): IntegrationTreeItem | undefined; - - getTreeItemChildren(parentId?: string): string[]; - - getId(): string; - - update(event: Event): void; - -} - -export abstract class IntegrationTreeItem extends vscode.TreeItem { - - abstract update(context: never): void; - -} - diff --git a/src/model/NodeConfig.ts b/src/model/NodeConfig.ts deleted file mode 100644 index 953ae36..0000000 --- a/src/model/NodeConfig.ts +++ /dev/null @@ -1,27 +0,0 @@ - -import { URL } from "url"; - -export class NodeConfig { - private static readonly defaultNodeUrl = new URL("localhost:7888"); - private nodeUrl: URL; - - constructor(nodeUrl?: URL) { - this.nodeUrl = nodeUrl || NodeConfig.defaultNodeUrl; - } - - public get host(): string { - return this.nodeUrl.href; - } - - public get url(): URL { - return this.nodeUrl; - } - - public set url(nodeUrl: URL | string | undefined) { - if (typeof nodeUrl === "string" ) { - this.nodeUrl = new URL(nodeUrl); - } else { - this.nodeUrl = nodeUrl || NodeConfig.defaultNodeUrl; - } - } -} diff --git a/src/nodeProvider.ts b/src/nodeProvider.ts deleted file mode 100644 index 0007aa1..0000000 --- a/src/nodeProvider.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as cp from "child_process"; -import * as client from "./utilities/pyrsiaClient"; - -export class NodeProvider { - - nodeProcess: cp.ChildProcess; - pid: number | undefined; - - isNodeHealthy(): Promise { - return client.isNodeHealthy(); - } - - getHostname(): string { - return client.getNodeUrl(); - } - - getStatus(): Promise { - return client.getStatus(); - } - - // async start() { - - // this.pid = this.nodeProcess.pid; - // console.log(this.pid); - // vscode.window.showInformationMessage('Pyrsia Node Started'); - // if (!await client.isNodeHealth()) { - - // } - // } - - // stop() { - // this.nodeProcess.kill(); - // console.log(this.pid); - // vscode.window.showInformationMessage('Pyrsia Node Stopped'); - // } -} diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index b13f0a9..b971424 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -2,10 +2,11 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; // import * as myExtension from '../../extension'; -suite('Extension Test Suite', () => { +// TODO - Tests +suite('Pyrsia Extension Test Suite', () => { vscode.window.showInformationMessage('Start all tests.'); - test('Sample test', () => { + test('Test', () => { assert.strictEqual(-1, [1, 2, 3].indexOf(5)); assert.strictEqual(-1, [1, 2, 3].indexOf(0)); }); diff --git a/src/utilities/fsUtils.ts b/src/utilities/fsUtils.ts deleted file mode 100644 index 9e4a501..0000000 --- a/src/utilities/fsUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { readdir } from "fs/promises"; -import * as path from "path"; - -export const findByName = async (dir: string, fileName: string): Promise => { - let matchedFile: string | undefined = undefined; - - const dirFileNames = await readdir(dir); - - for (const dirFileName of dirFileNames) { - if (dirFileName === fileName) { - matchedFile = path.join(dir, dirFileName); - break; - } - } - - return matchedFile; -}; diff --git a/src/utilities/pyrsiaClient.ts b/src/utilities/pyrsiaClient.ts index 8d0ae8a..48068a0 100644 --- a/src/utilities/pyrsiaClient.ts +++ b/src/utilities/pyrsiaClient.ts @@ -1,15 +1,7 @@ import axios from "axios"; -import { Util } from "./util"; +import { Util } from "./Util"; -export const getNodeUrl = ( ): string => { - let nodeUrl = Util.getNodeConfig().host; - - if (!nodeUrl.toLowerCase().startsWith("http")) { - nodeUrl = `http://${nodeUrl}`; - } - - return nodeUrl; -}; +// Methods to get various info from a Pyrsia node type PingResponse = { data: string[]; @@ -23,16 +15,20 @@ type TransparencyLogResponse = { data: string[]; }; +/** + * Checks if Pyrsia node is up + * @returns {Promise} 'true' if Pyrsia node is up + */ export const isNodeHealthy = async (): Promise => { - console.log('Check node health'); - const nodeUrl = `${getNodeUrl()}/v2`; + console.debug('Check node health'); + const nodeUrl = `${Util.getNodeConfig().hostWithProtocol}/v2`; // NOI18N let status; try { ({ status } = await axios.get( nodeUrl, { headers: { - accept: 'application/json' + accept: 'application/json' // NOI18N } } )); @@ -43,12 +39,15 @@ export const isNodeHealthy = async (): Promise => { return status === 200; }; +/** + * Returns Pyrsia node status. + * @returns {Promise} Returns the node status + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export const getStatus = async (): Promise => { - console.log('Get node status'); - const nodeUrl = `${getNodeUrl()}/status`; + console.debug('Get node status'); + const nodeUrl = `${Util.getNodeConfig().hostWithProtocol}/status`; let data; - try { ({ data } = await axios.get( nodeUrl, @@ -65,9 +64,14 @@ export const getStatus = async (): Promise => { return data; }; +/** + * Returns Pyrsia Transparency Log. + * @params {string} docker image name + * @returns {Promise<[]>} docker transparency log + */ export const getDockerTransparencyLog = async (imageName: string): Promise<[]> => { console.log(`Get docker image transparency log info for ${imageName}`); - const nodeUrl = `${getNodeUrl()}/inspect/docker`; + const nodeUrl = `${Util.getNodeConfig().hostWithProtocol}/inspect/docker`; let data; try { @@ -84,11 +88,15 @@ export const getDockerTransparencyLog = async (imageName: string): Promise<[]> = return (data as unknown as []); }; +/** + * Request docker build (adds a new docker image to Pyrsia) + * @params {string} docker image name (tags) + * @param imageName + */ export const requestDockerBuild = async (imageName: string): Promise<[]> => { console.log(`Request build for docker image: ${imageName}`); - const nodeUrl = `${getNodeUrl()}/build/docker`; + const nodeUrl = `${Util.getNodeConfig().hostWithProtocol}/build/docker`; let data; - try { ({ data } = await axios.post( nodeUrl, @@ -103,6 +111,10 @@ export const requestDockerBuild = async (imageName: string): Promise<[]> => { return (data as unknown as []); }; +/** + * Returns number of connected peers. + * @returns {Pyrsia} number of peers + */ export const getPeers = async (): Promise => { console.log('Get node peers'); const data = await getStatus(); diff --git a/src/utilities/util.ts b/src/utilities/util.ts index a6e82b9..3b3c461 100644 --- a/src/utilities/util.ts +++ b/src/utilities/util.ts @@ -1,62 +1,175 @@ -import { Uri, Webview } from "vscode"; -import { NodeConfig } from "../model/NodeConfig"; import path = require("path"); -import * as vscode from 'vscode'; import * as DockerClient from "dockerode"; +import * as vscode from "vscode"; +import { NodeConfig } from "../api/NodeConfig"; +import { readdir } from "fs/promises"; +/** + * Utility static method (don't create instances) + */ export class Util { - private static resourcePath: string; - // TODO Replace it with real, persistance storage - private static config: NodeConfig = new NodeConfig(); + private static config: NodeConfig; private static dockerClient: DockerClient; - static init(context: vscode.ExtensionContext): void { - Util.resourcePath = context.asAbsolutePath(path.join('resources')); - } - - static getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { - return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); + /** + * It's called once to pass the init values. + * @param context + */ + public static init(context: vscode.ExtensionContext): void { + if (this.config) { + throw new Error("Utils class is already initialized") + } + // set the resource path + Util.resourcePath = context.asAbsolutePath(path.join('resources')); // NOI18N + // load the configuration from the context (context is used to store the node configuration - e.g URL) + this.config = new NodeConfigImpl(context.workspaceState); } - static getNodeConfig(): NodeConfig { - return this.config; // TODO Replace it with the configuration obtained from the ide cache/storage + /** + * Get the node configuration. + * @returns {NodeConfig} returns the node config + */ + public static getNodeConfig(): NodeConfig { + return Util.config; } - static getResourcePath(): string { + /** + * Returns the resource path. + * @returns {string} resource path folder path + */ + public static getResourcePath(): string { return Util.resourcePath; } - static getResourceImagePath(): string { - return path.join(Util.resourcePath, "images"); + /** + * Returns the image resource path folder + * @returns {string} image resource folder path + */ + public static getResourceImagePath(): string { + return path.join(Util.resourcePath, "images"); // NOI18N } /** - * Get Docker Client (singleton). - * @returns {DockerClient} DockerClient + * Returns docker client, at this point we only support '/var/run/docker.sock'. + * @returns {DockerClient} docker client */ - static getDockerClient(): DockerClient { + public static getDockerClient(): DockerClient { if (!this.dockerClient) { - const dockerConfig: DockerClient.DockerOptions = { socketPath: '/var/run/docker.sock' }; //TODO Should be configurable. + //TODO The docker client should be configurable but for now we only support Docker Desktop. + const dockerConfig: DockerClient.DockerOptions = { socketPath: '/var/run/docker.sock' }; // NOI18N this.dockerClient = new DockerClient(dockerConfig); } return this.dockerClient; } - - static isDebugMode() { + + /** + * Checks if in the debug mode. + * @returns {boolean} Boolean true if in the debug mode + */ + public static isDebugMode(): boolean { return process.env.VSCODE_DEBUG_MODE === "true"; } - static debugMessage(message: string) { + /** + * It shows an error message (IDE notification). + * @param {string} message error message + * @returns {void} + */ + public static debugMessage(message: string): void { if (this.isDebugMode()) { vscode.window.showErrorMessage(message); } console.debug(message); } - - static sleep(milliseconds: number): Promise { + + /** + * Sleeps for the given amount of time. + * @param {number} milliseconds sf + * @returns {Promise} Promise + */ + public static sleep(milliseconds: number): Promise { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } + + /** + * Searches for a file in the given dir recursively. + * @async + * @param {string} dir path + * @param {string} fileName searched filename + * @returns {string} file path | unknown + */ + public static async findFile(dir: string, fileName: string): Promise { + const dirFileNames = await readdir(dir); + let matchedFile: string | undefined = undefined; + for (const dirFileName of dirFileNames) { + if (dirFileName === fileName) { + matchedFile = path.join(dir, dirFileName); + break; + } + } + + return matchedFile; + } } +/** + * Private NodeConfig implementation. + */ +class NodeConfigImpl implements NodeConfig { + // the node supported protocol + private static readonly protocol = "http"; // NOI18N + // default node URL + private static readonly defaultNodeUrl = new URL("localhost:7888"); // NOI18N + // the configuration ket, it uses to store configuration in context.workspaceState + private static readonly nodeUrlKey: string = "PYRSIA_NODE_URL_KEY"; // NOI18N + + private nodeUrl: URL; + private workspaceState; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(workspaceState: any) { + this.workspaceState = workspaceState; + const nodeUrl = workspaceState.get(NodeConfigImpl.nodeUrlKey); + this.url = nodeUrl; + } + + get defaultUrl(): URL { + return NodeConfigImpl.defaultNodeUrl; + } + + get hostWithProtocol(): string { + let host = this.nodeUrl.href; + if (!host.toLocaleLowerCase().startsWith(NodeConfigImpl.protocol)) { + host = `${NodeConfigImpl.protocol}://${host}`; + } + + return host; + } + + get protocol(): string { + return NodeConfigImpl.protocol; + } + + get host(): string { + return this.nodeUrl.href; + } + + get url(): URL { + return this.nodeUrl; + } + + set url(nodeUrl: URL | string | undefined) { + if (!nodeUrl) { + console.warn(`The node config wasn't updated because the provided URL is ${nodeUrl}`); // NOI18N + return; + } + if (typeof nodeUrl === "string" ) { // NOI18N + this.nodeUrl = new URL(nodeUrl); + } else { + this.nodeUrl = nodeUrl || NodeConfigImpl.defaultNodeUrl; + } + this.workspaceState.update(NodeConfigImpl.nodeUrlKey, this.nodeUrl); + } +} diff --git a/src/webview-ui/main.js b/src/webview-ui/main.js deleted file mode 100644 index ac4ae52..0000000 --- a/src/webview-ui/main.js +++ /dev/null @@ -1,36 +0,0 @@ -const vscode = acquireVsCodeApi(); - -window.addEventListener("load", main); - -function main() { - const nodeConnectButton = document.getElementById("node-button-connect"); - nodeConnectButton.addEventListener("click", updateNode); - nodeConnectButton.style.color = newColor; -} - -function updateNode() { - vscode.postMessage({ - command: "node-update-view", - }); -} - -window.addEventListener('message', event => { - const message = event.data; // The json data that the extension sent - switch (message.type) { - case 'node-disconnected': - { - document.getElementById("node-connected").style.display = "none"; - document.getElementById("node-disconnected").style.display = "block"; - break; - } - case 'node-connected': - { - document.getElementById("node-connected").style.display = "block"; - document.getElementById("node-disconnected").style.display = "none"; - // document.getElementById("node-peer-count").innerHTML = message.nodeStatus.peers_count || "0"; - // document.getElementById("node-peer-id").innerHTML = message.nodeStatus.peer_id || ""; - // document.getElementById("node-peer-addresses").innerHTML = message.nodeStatus.peer_addrs || ""; - break; - } - } -}); \ No newline at end of file diff --git a/src/webview-ui/styles.css b/src/webview-ui/styles.css deleted file mode 100644 index 5737d89..0000000 --- a/src/webview-ui/styles.css +++ /dev/null @@ -1,124 +0,0 @@ -:root { - --container-padding: 20px; - --input-padding-vertical: 6px; - --input-padding-horizontal: 4px; - --input-margin-vertical: 4px; - --input-margin-horizontal: 0; - --margin-break-small: 10px; - --margin-break: 20px; - --margin-break-large: 40px; -} - -body { - padding: 0 var(--container-padding); - color: var(--vscode-foreground); - font-size: var(--vscode-font-size); - font-weight: var(--vscode-font-weight); - font-family: var(--vscode-font-family); - background-color: #252526; -} - -ol, -ul { - padding-left: var(--container-padding); -} - -body > *, -form > * { - margin-block-start: var(--input-margin-vertical); - margin-block-end: var(--input-margin-vertical); -} - -*:focus { - outline-color: var(--vscode-focusBorder) !important; -} - -a { - color: var(--vscode-textLink-foreground); -} - -a:hover, -a:active { - color: var(--vscode-textLink-activeForeground); -} - -code { - font-size: var(--vscode-editor-font-size); - font-family: var(--vscode-editor-font-family); -} - -button { - border: none; - padding: var(--input-padding-vertical) var(--input-padding-horizontal); - width: 100%; - text-align: center; - outline: 1px solid transparent; - outline-offset: 2px !important; - color: var(--vscode-button-foreground); - background: var(--vscode-button-background); - max-width: 300px; - margin:0 auto; - display:block; - -} - -button:hover { - cursor: pointer; - background: var(--vscode-button-hoverBackground); -} - -button:focus { - outline-color: var(--vscode-focusBorder); -} - -button.secondary { - color: var(--vscode-button-secondaryForeground); - background: var(--vscode-button-secondaryBackground); -} - -button.secondary:hover { - background: var(--vscode-button-secondaryHoverBackground); -} - -input:not([type='checkbox']), -textarea { - display: block; - width: 100%; - border: none; - font-family: var(--vscode-font-family); - padding: var(--input-padding-vertical) var(--input-padding-horizontal); - color: var(--vscode-input-foreground); - outline-color: var(--vscode-input-border); - background-color: var(--vscode-input-background); -} - -input::placeholder, -textarea::placeholder { - color: var(--vscode-input-placeholderForeground); -} - -* { - margin: 2; -} - -/* hr { - margin-bottom: var(--margin-break-small); -} */ - -.break { - margin-bottom: var(--margin-break-small); -} - -.dimmer { - color: darkgrey; -} - -/* #node-container { - display: flex; - align-items: center; - justify-content: space-between; - background-color: var(--vscode-input-background); - padding: 1rem; - margin: 1rem 0; -} */ - diff --git a/src/webviews/HelpView.ts b/src/webviews/HelpView.ts index a52b2d4..95a5c8b 100644 --- a/src/webviews/HelpView.ts +++ b/src/webviews/HelpView.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as vscode from 'vscode'; -// Help list +// Help list enum enum HelpProperty { Install = "install", Tutorials = "tutorials", @@ -11,16 +11,22 @@ enum HelpProperty { Issue = "issue", } +/** + * Help Util class, don't create instances of this class. + * TODO Do we need this one? + */ export class HelpUtil { - public static readonly helpCommandId = "pyrsia.openHelpLink"; // NOI18 - public static readonly quickStartUrl = "https://pyrsia.io/docs/tutorials/quick-installation/"; // NOI18 + public static readonly helpCommandId = "pyrsia.openHelpLink"; // NOI18N + public static readonly quickStartUrl = "https://pyrsia.io/docs/tutorials/quick-installation/"; // NOI18N } +/** + * Pyrsia Help view. + */ export class HelpView { - private static readonly viewType: string = "pyrsia.help"; // NOI18 + private static readonly viewType: string = "pyrsia.help"; // NOI18N private readonly treeViewProvider: HelpTreeProvider; - constructor(context: vscode.ExtensionContext) { this.treeViewProvider = new HelpTreeProvider(); const view = vscode.window.createTreeView( @@ -28,22 +34,22 @@ export class HelpView { { showCollapseAll: true, treeDataProvider: this.treeViewProvider } ); vscode.window.registerTreeDataProvider(HelpView.viewType, this.treeViewProvider); - // create the open external help link command const openHelpLink = vscode.commands.registerCommand(HelpUtil.helpCommandId, (helpUrl: string) => { - console.log(`Open ${helpUrl} using '${HelpUtil.helpCommandId}'`); // NOI18 + console.log(`Open ${helpUrl} using '${HelpUtil.helpCommandId}'`); // NOI18N vscode.env.openExternal(vscode.Uri.parse(helpUrl)); }); // register the open external help link command context.subscriptions.push(openHelpLink); - // register the help view context.subscriptions.push(view); } } +/** + * Help tree provider. + */ class HelpTreeProvider implements vscode.TreeDataProvider { - private treeItems: Map = new Map(); getTreeItem(id: string): vscode.TreeItem | Thenable { @@ -80,38 +86,41 @@ class HelpTreeProvider implements vscode.TreeDataProvider { } } +/** + * Help Tree Item + */ class HelpTreeItem extends vscode.TreeItem { private static readonly properties = { [HelpProperty.Install.toLowerCase()]: { - iconPath: new vscode.ThemeIcon("getting-started-beginner"), // NOI18 - id: "install", // NOI18 + iconPath: new vscode.ThemeIcon("getting-started-beginner"), // NOI18N + id: "install", // NOI18N name: "Pyrsia Quick Installation", url: HelpUtil.quickStartUrl }, [HelpProperty.Overview.toLowerCase()]: { - iconPath: new vscode.ThemeIcon("open-editors-view-icon"), // NOI18 - id: "overview", // NOI18 + iconPath: new vscode.ThemeIcon("open-editors-view-icon"), // NOI18N + id: "overview", // NOI18N name: "Read Pyrsia Documentation", - url: "https://pyrsia.io/docs/" // NOI18 + url: "https://pyrsia.io/docs/" // NOI18N }, [HelpProperty.Tutorials.toLowerCase()]: { - iconPath: new vscode.ThemeIcon("play-circle"), // NOI18 - id: "tutorials", // NOI18 + iconPath: new vscode.ThemeIcon("play-circle"), // NOI18N + id: "tutorials", // NOI18N name: "Watch Pyrsia Tutorials", - url: "https://www.youtube.com/@pyrsiaoss/playlists" // NOI18 + url: "https://www.youtube.com/@pyrsiaoss/playlists" // NOI18N }, [HelpProperty.Github.toLowerCase()]: { - iconPath: new vscode.ThemeIcon("github"), // NOI18 - id: "github", // NOI18 + iconPath: new vscode.ThemeIcon("github"), // NOI18N + id: "github", // NOI18N name: "Get Involved", - url: "https://github.com/pyrsia" // NOI18 + url: "https://github.com/pyrsia" // NOI18N }, [HelpProperty.Issue.toLowerCase()]: { - iconPath: new vscode.ThemeIcon("remote-explorer-report-issues"), // NOI18 - id: "issue", // NOI18 + iconPath: new vscode.ThemeIcon("remote-explorer-report-issues"), // NOI18N + id: "issue", // NOI18N name: "Report Issue", - url: "https://github.com/pyrsia/pyrsia/issues" // NOI18 + url: "https://github.com/pyrsia/pyrsia/issues" // NOI18N } }; diff --git a/src/webviews/IntegrationsView.ts b/src/webviews/IntegrationsView.ts index 49bf177..cfa3935 100644 --- a/src/webviews/IntegrationsView.ts +++ b/src/webviews/IntegrationsView.ts @@ -1,45 +1,51 @@ +/* eslint-disable @typescript-eslint/member-ordering */ // https://github.com/xojs/eslint-config-xo-typescript/issues/43 /* eslint-disable @typescript-eslint/naming-convention */ import * as vscode from 'vscode'; -import { Integration, IntegrationTreeItem, Event } from '../integrations/api/Integration'; // NOI18 +import { Integration, IntegrationTreeItem, Event } from '../api/Integration'; // NOI18N +/** + * Integrations view. + */ export class IntegrationsView { - private static readonly refreshIntegrationModelCommandId: string = "pyrsia.integrations.model.update"; // NOI18 - private static readonly refreshIntegrationViewCommandId: string = "pyrsia.integrations.view.update"; // NOI18 - private static readonly refreshIntegrationCommandId: string = "pyrsia.integrations.update"; // NOI18 - private static readonly viewType: string = "pyrsia.node-integrations"; // NOI18 + private static readonly refreshIntegrationModelCommandId: string = "pyrsia.integrations.model.update"; // NOI18N + private static readonly refreshIntegrationViewCommandId: string = "pyrsia.integrations.view.update"; // NOI18N + private static readonly refreshIntegrationCommandId: string = "pyrsia.integrations.update"; // NOI18N + private static readonly viewType: string = "pyrsia.node-integrations"; // NOI18N - private readonly treeViewProvider: NodeIntegrationsTreeProvider; + private readonly treeViewProvider: IntegrationsTreeProvider; private readonly _view?: vscode.TreeView; constructor(context: vscode.ExtensionContext) { - - this.treeViewProvider = new NodeIntegrationsTreeProvider(); - + this.treeViewProvider = new IntegrationsTreeProvider(); this._view = vscode.window.createTreeView( IntegrationsView.viewType, { showCollapseAll: true, treeDataProvider: this.treeViewProvider } ); - this.treeViewProvider.update(); + // register update model command vscode.commands.registerCommand(IntegrationsView.refreshIntegrationModelCommandId, () => { this.treeViewProvider.update(); }); + // register update view command vscode.commands.registerCommand(IntegrationsView.refreshIntegrationViewCommandId, () => { - this.treeViewProvider.refreshTreeView(); + this.treeViewProvider.updateTreeView(); }); + // register command that update both - mode and view vscode.commands.registerCommand(IntegrationsView.refreshIntegrationCommandId, () => { this.treeViewProvider.update(); - this.treeViewProvider.refreshTreeView(); + this.treeViewProvider.updateTreeView(); }); + // triggered the update when view is shown this._view.onDidChangeVisibility(() => { this.treeViewProvider.update(); }); + // triggered the update on the selection change this._view.onDidChangeSelection(() => { this.treeViewProvider.update(); }); @@ -47,22 +53,31 @@ export class IntegrationsView { context.subscriptions.push(this._view); } - static requestIntegrationsModelUpdate(): void { - vscode.commands.executeCommand(this.refreshIntegrationModelCommandId); - } - + /** + * Requests update view. + */ static requestIntegrationsViewUpdate(): void { vscode.commands.executeCommand(this.refreshIntegrationViewCommandId); } + /** + * Requests update view and model. + */ static requestIntegrationsUpdate(): void { vscode.commands.executeCommand(this.refreshIntegrationCommandId); } - addIntegration(integration: Integration): void { + /** + * Add an integration instance for the Integrations view (e.g. DockerIntegration, etc) + * @param {Integration} integration + */ + addIntegration(integration: Integration) { this.treeViewProvider.addIntegration(integration); } + /** + * Triggers the tree provider update. + */ async update(): Promise { if (this.treeViewProvider) { await this.treeViewProvider.update(); @@ -70,17 +85,15 @@ export class IntegrationsView { } } -class NodeIntegrationsTreeProvider implements vscode.TreeDataProvider { +/** + * Integration view tree provider. + */ +class IntegrationsTreeProvider implements vscode.TreeDataProvider { // on change tree data private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - // eslint-disable-next-line @typescript-eslint/member-ordering readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - - // on visibility change - // private _onDidChangeVisibility: vscode.EventEmitter = new vscode.EventEmitter(); - // readonly onDidChangeVisibility: vscode.Event = this._onDidChangeVisibility.event; - + // integrations (e.g. DockerIntegration) private readonly integrations: Set = new Set(); addIntegration(integration: Integration): void { @@ -127,7 +140,8 @@ class NodeIntegrationsTreeProvider implements vscode.TreeDataProvider { return item; } - refreshTreeView(): void { + // fires the tree view update (UI) + updateTreeView(): void { this._onDidChangeTreeData.fire(undefined); } } diff --git a/src/webviews/NodeConfigView.ts b/src/webviews/NodeConfigView.ts index 435c8fd..bd822e9 100644 --- a/src/webviews/NodeConfigView.ts +++ b/src/webviews/NodeConfigView.ts @@ -1,57 +1,67 @@ // https://github.com/xojs/eslint-config-xo-typescript/issues/43 /* eslint-disable @typescript-eslint/naming-convention */ import * as vscode from "vscode"; -import { Util } from "../utilities/util"; +import { Util } from "../utilities/Util"; import * as client from "../utilities/pyrsiaClient"; import { HelpUtil } from "./HelpView"; -import { Event, Integration } from "../integrations/api/Integration"; +import { Event, Integration } from "../api/Integration"; import { IntegrationsView } from "./IntegrationsView"; -// TODO Without branches enum NodeConfigProperty { - Status = "status", // NOI18 - Peers = "peers", // NOI18 - Error1 = "error1", // NOI18 - Error2 = "error2", // NOI18 + Status = "status", // NOI18N + Peers = "peers", // NOI18N + WarningConnection = "warningconnection", // NOI18N + WarningUpdateNode = "warningupdatenode", // NOI18N } +/** + * Node Config view. + */ export class NodeConfigView { - public static readonly configNodeCommandId = "pyrsia.configure-node"; + // Ids (view, commands) + public static readonly configNodeCommandId = "pyrsia.node-config.update-url"; // NOI18N + private static readonly viewType: string = "pyrsia.node-config"; // NOI18N + private static readonly updateViewCommandId = "pyrsia.node-config.update-view"; // NOI18N - private static readonly viewType: string = "pyrsia.node-config"; // NOI18 private readonly treeViewProvider: NodeConfigTreeProvider; private readonly view; private readonly integrations: Set = new Set(); constructor(context: vscode.ExtensionContext) { + // create the view provider this.treeViewProvider = new NodeConfigTreeProvider(); + // create the tree view this.view = vscode.window.createTreeView( NodeConfigView.viewType, { showCollapseAll: true, treeDataProvider: this.treeViewProvider } ); + // register the view provider vscode.window.registerTreeDataProvider(NodeConfigView.viewType, this.treeViewProvider); - + // subscribe the node config view context.subscriptions.push(this.view); - - vscode.commands.registerCommand("pyrsia.node-config.tree.refresh", () => { - this.treeViewProvider.update(); + // register the update view node (responsible for the view update on certain events) + vscode.commands.registerCommand(NodeConfigView.updateViewCommandId, () => { + this.update(); + this.notifyNodeConfigUpdated(); }); - + // update the view (UI/model) on certain view events this.view.onDidChangeVisibility(() => { + this.update(); this.treeViewProvider.update(); }); - // docker open and update configuration editor command for the docker integration + // Add a command to update the Pyrsia node configuration (actually just URL) const configureNodeCommand = vscode.commands.registerCommand( NodeConfigView.configNodeCommandId, async () => { + // the update node url input box const options: vscode.InputBoxOptions = { prompt: "Update the Pyrsia node address (e.g. localhost:7888)", validateInput(value) { let errorMessage: string | undefined; console.info(`Node configuration input: ${value}`); - if (!value.toLocaleLowerCase().startsWith("http")) { - value = `http://${value}`; + if (!value.toLocaleLowerCase().startsWith(Util.getNodeConfig.prototype)) { + value = `${Util.getNodeConfig().protocol}://${value}`; } try { new URL(value); @@ -65,13 +75,12 @@ export class NodeConfigView { value: Util.getNodeConfig().host }; - // show the input box so user can provide a new node address + // show the url input box so the user can provide a new node address const newNodeAddress: string | undefined = await vscode.window.showInputBox(options); Util.getNodeConfig().url = newNodeAddress; - - // update the UI + // update the view and the dependencies this.update(); - // notify the integrations update the update + // notify the integrations about the change this.notifyNodeConfigUpdated(); IntegrationsView.requestIntegrationsUpdate(); } @@ -90,6 +99,7 @@ export class NodeConfigView { }, 60000); } + // update the view public update(): void { this.treeViewProvider.update(); client.isNodeHealthy().then((healthy) => { @@ -97,31 +107,33 @@ export class NodeConfigView { }); } - addIntegration(integration: Integration): void { + // adds integration (mostly so there is a way to notify them about the changes) + public addIntegration(integration: Integration): void { this.integrations.add(integration); } + // notify the integrations (e.g. Docker) about the changes private notifyNodeConfigUpdated() { for (const integration of this.integrations) { integration.update(Event.NodeConfigurationUpdate); + integration.update(Event.IntegrationModelUpdate); } } } -// TODO This is flat tree (no branches) +// Tree data provider for the node config class NodeConfigTreeProvider implements vscode.TreeDataProvider { - + // update the tree on changes private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - // eslint-disable-next-line @typescript-eslint/member-ordering readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - + // tree items private readonly treeItems: Map; constructor() { this.treeItems = new Map(); - for (const nodeProperty in NodeConfigProperty) { // TODO Why nodeProperty is 'string' type? Investigate + for (const nodeProperty in NodeConfigProperty) { const treeItem = this.treeItems.get(nodeProperty.toLowerCase()); if (!treeItem) { // TODO Why I have to do this conversion in TS? Shouldn't 'nodeProperty' be the enum type? @@ -132,8 +144,9 @@ class NodeConfigTreeProvider implements vscode.TreeDataProvider { } } + // update the tree data update() { - for (const nodeProperty in NodeConfigProperty) { // TODO Why nodeProperty is 'string' type? Investigate + for (const nodeProperty in NodeConfigProperty) { const treeItem = this.treeItems.get(nodeProperty.toLocaleLowerCase()); if (treeItem) { treeItem.update(); @@ -157,11 +170,11 @@ class NodeConfigTreeProvider implements vscode.TreeDataProvider { getChildren(parentId?: string | undefined): vscode.ProviderResult { let children: string[] = []; - if (!parentId) { // Create all tree Items for the tree + if (!parentId) { children = [... this.treeItems].map(([, value]) => { return value.isRoot() ? value.id : ""; }).filter(value => value !== ""); - } else { // not tree root then get the particular id for the parentId + } else { const childId = NodeTreeItem.getChildrenId(parentId); const treeItem: NodeTreeItem = this.treeItems.get(childId) as NodeTreeItem; children = [treeItem.id]; @@ -171,18 +184,22 @@ class NodeConfigTreeProvider implements vscode.TreeDataProvider { } } +/** + * Node config tree item + */ class NodeTreeItem extends vscode.TreeItem { - - // reusable icons - private static readonly emptyIcon = new vscode.ThemeIcon("non-icon"); - private static readonly rightArrowIcon = new vscode.ThemeIcon("arrow-right"); - private static readonly cloudIcon = new vscode.ThemeIcon("cloud"); - private static readonly brokenConnectionIcon = new vscode.ThemeIcon("alert"); - private static readonly peersCountIcon = new vscode.ThemeIcon("extensions-install-count"); + // tree item icons + private static readonly emptyIcon = new vscode.ThemeIcon("non-icon"); // NOI18N + private static readonly rightArrowIcon = new vscode.ThemeIcon("arrow-right"); // NOI18N + private static readonly cloudIcon = new vscode.ThemeIcon("cloud"); // NOI18N + private static readonly brokenConnectionIcon = new vscode.ThemeIcon("alert"); // NOI18N + private static readonly peersCountIcon = new vscode.ThemeIcon("extensions-install-count"); // NOI18N + + // Tree item properties and the logic to update it. private static readonly properties = { [NodeConfigProperty.Status.toLowerCase()]: { iconPath: NodeTreeItem.cloudIcon, - id: "status", // NOI18 + id: "status", // NOI18N listener: { onUpdate: async (treeItem: NodeTreeItem) => { const healthy: boolean = await client.isNodeHealthy(); @@ -198,7 +215,7 @@ class NodeTreeItem extends vscode.TreeItem { }, [NodeConfigProperty.Peers.toLowerCase()]: { iconPath: NodeTreeItem.peersCountIcon, - id: "peers", // NOI18 + id: "peers", // NOI18N listener: { onUpdate: async (treeItem: NodeTreeItem) => { const health = await client.isNodeHealthy(); @@ -207,7 +224,7 @@ class NodeTreeItem extends vscode.TreeItem { const { name } = NodeTreeItem.properties[NodeConfigProperty.Peers.toLowerCase()]; treeItem.label = `${name}: ${peers.toString()}`; treeItem.iconPath = NodeTreeItem.peersCountIcon; - } else { // don't show the item content + } else { // don't show the item content is connection is broken treeItem.label = ""; treeItem.iconPath = NodeTreeItem.emptyIcon; } @@ -216,15 +233,14 @@ class NodeTreeItem extends vscode.TreeItem { name: "Node peers", root: true }, - [NodeConfigProperty.Error1.toLowerCase()]: { + [NodeConfigProperty.WarningConnection.toLowerCase()]: { iconPath: NodeTreeItem.emptyIcon, - id: "error1", // NOI18 + id: "warningconnection", // NOI18N listener: { onUpdate: async (treeItem: NodeTreeItem) => { const healthy: boolean = await client.isNodeHealthy(); treeItem.label = healthy ? "" : "👋 Read how to install and configure Pyrsia"; - const iconPath = healthy ? NodeTreeItem.emptyIcon : NodeTreeItem.rightArrowIcon; - treeItem.iconPath = iconPath; + treeItem.iconPath = healthy ? NodeTreeItem.emptyIcon : NodeTreeItem.rightArrowIcon; treeItem.command = healthy ? undefined : { arguments: [HelpUtil.quickStartUrl], command: HelpUtil.helpCommandId, @@ -235,9 +251,9 @@ class NodeTreeItem extends vscode.TreeItem { name: "", root: true }, - [NodeConfigProperty.Error2.toLowerCase()]: { + [NodeConfigProperty.WarningUpdateNode.toLowerCase()]: { iconPath: NodeTreeItem.emptyIcon, - id: "error2", // NOI18 + id: "warningupdatenode", // NOI18N listener: { onUpdate: async (treeItem: NodeTreeItem) => { const healthy: boolean = await client.isNodeHealthy(); @@ -265,7 +281,7 @@ class NodeTreeItem extends vscode.TreeItem { this.tooltip = this.label; } - static create(nodeProperty: NodeConfigProperty): NodeTreeItem { + public static create(nodeProperty: NodeConfigProperty): NodeTreeItem { const property = this.properties[nodeProperty]; const collapsibleState = vscode.TreeItemCollapsibleState.None; return new NodeTreeItem( @@ -278,19 +294,22 @@ class NodeTreeItem extends vscode.TreeItem { ); } - static getChildrenId(parentId: string) { + public static getChildrenId(parentId: string) { return `${parentId}value`; } - update() { + public update() { this.listener.onUpdate(this); } - isRoot(): boolean { + public isRoot(): boolean { return this.root; } } +/** + * Node Config Tree Item update interface + */ interface NodeConfigListener { onUpdate(treeItem: NodeTreeItem): void; } diff --git a/src/webviews/NodeStatusView.ts b/src/webviews/NodeStatusView.ts deleted file mode 100644 index 57896dc..0000000 --- a/src/webviews/NodeStatusView.ts +++ /dev/null @@ -1,153 +0,0 @@ -import * as vscode from 'vscode'; -import { NodeProvider } from "../nodeProvider"; -import { Util } from "../utilities/util"; - -export class NodeStatusViewProvider implements vscode.WebviewViewProvider { - public static readonly viewType = "pyrsia.node"; - - private nodeProvider; - private _view?: vscode.WebviewView; - private extensionUri: vscode.Uri; - private readonly onDidConnectListeners: Set = new Set(); - - constructor(private readonly context: vscode.ExtensionContext) { - this.nodeProvider = new NodeProvider(); - - vscode.window.registerWebviewViewProvider( - NodeStatusViewProvider.viewType, - this - ); - - this.extensionUri = context.extensionUri; - } - - public getWebView(): vscode.WebviewView { - return this._view as vscode.WebviewView; - } - - onDidConnect(listener: NodeStatusViewListener): void { - this.onDidConnectListeners.add(listener); - } - - public resolveWebviewView(view: vscode.WebviewView) { - this._view = view; - - view.webview.options = { - enableScripts: true - }; - - view.webview.html = this.getWebviewContent(view.webview, this.extensionUri); - this.setWebviewMessageListener(view); - - view.onDidChangeVisibility((): void => { - this.updateView(); - }); - - this.updateView(); - } - - public async updateView() { - const connected: boolean = await this.nodeProvider.isNodeHealthy(); - if (connected) { - this.connected(); - } else { - this.disconnected(); - } - } - - private getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { - - const toolkitUri = Util.getUri(webview, extensionUri, [ - "node_modules", - "@vscode", - "webview-ui-toolkit", - "dist", - "toolkit.js" - ]); - - const mainUri = Util.getUri(webview, extensionUri, ["src", "webview-ui", "main.js"]); - const stylesUri = Util.getUri(webview, extensionUri, ["src", "webview-ui", "styles.css"]); - const pyrsiaHostname = this.nodeProvider.getHostname(); - - return /*html*/ ` - - - - - - - - - - Node - - - -
-
-
🟢 Connected to ${pyrsiaHostname}
-
- -
-
🔴 Failed connecting to ${pyrsiaHostname}
-
-
👉 Please make sure Pyrsia is installed, - - running and configured.. 👈 -
-
- -
- -
- - - - `; - } - - private setWebviewMessageListener(view: vscode.WebviewView) { - view.webview.onDidReceiveMessage((message) => { - const { command } = message; - switch (command) { - case "node-update-view": { - this.updateView(); - break; - } - default: { - throw new Error(`Command ${command} not found`); - } - } - }); - } - - private connected() { - const view = this._view; - if (view) { - let nodeStatus: unknown; - this.nodeProvider.getStatus().then((data) => { - nodeStatus = data; - }).finally(() => { - view.webview.postMessage({ nodeStatus, type: 'node-connected' }); - for (const listener of this.onDidConnectListeners) { - listener.onDidConnect(); - } - }); - } - } - - private disconnected() { - if (this._view) { - this._view.webview.postMessage({ type: 'node-disconnected' }); - } - for (const listener of this.onDidConnectListeners) { - listener.onDidConnect(); - } - } -} - -export interface NodeStatusViewListener { - onDidConnect(): void; -} From 5bebea8762a1136c1f674295b92068bd9293f90e Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Mon, 23 Jan 2023 14:20:28 -0800 Subject: [PATCH 07/16] - Changelog added - Bug fixes - Refactoring --- CHANGELOG.md | 17 ++++++++++++++--- package.json | 4 ++-- src/utilities/pyrsiaClient.ts | 6 +++--- src/utilities/util.ts | 12 ++++++++---- src/webviews/IntegrationsView.ts | 10 +++++++--- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f9ee7f..1b8a6f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ -# Change Log +# Pyrsia VS Code Extension Changelog + ## [Unreleased] -# JFrog MS Teams App Changelog +Not available in the VS Code store yet. ## [0.0.1] - Apr 25th, 2022 -* Initial API, views and the basic Pyrsia node support. +* Initial release. Features: + * Views: + * Pyrsia node connection status. + * Integrations (Docker support). + * Help. + * New commands: + * Update Pyrsia node configuration. + * Update the docker config. + * Add docker image to Pyrsia (request build). + * Open the transparency log (docker images). + * Request to replace the local docker images with Pyrsia images. diff --git a/package.json b/package.json index c406e8a..becccf5 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "pyrsia-extension", "displayName": "Pyrsia", - "description": "Pyrsia Extension for VS Code", - "version": "0.0.1", + "description": "Pyrsia VS Code Extension", + "version": "0.1.0", "repository": { "type": "git", "url": "https://github.com/pyrsia/pyrsia-vscode-extension" diff --git a/src/utilities/pyrsiaClient.ts b/src/utilities/pyrsiaClient.ts index 48068a0..c48fd58 100644 --- a/src/utilities/pyrsiaClient.ts +++ b/src/utilities/pyrsiaClient.ts @@ -66,7 +66,7 @@ export const getStatus = async (): Promise => { /** * Returns Pyrsia Transparency Log. - * @params {string} docker image name + * @param {string} imageName - docker image name * @returns {Promise<[]>} docker transparency log */ export const getDockerTransparencyLog = async (imageName: string): Promise<[]> => { @@ -90,8 +90,8 @@ export const getDockerTransparencyLog = async (imageName: string): Promise<[]> = /** * Request docker build (adds a new docker image to Pyrsia) - * @params {string} docker image name (tags) - * @param imageName + * @param {string} imageName docker image name (tags) + * @returns {Promise[]} - the build info (mostly the build ID) */ export const requestDockerBuild = async (imageName: string): Promise<[]> => { console.log(`Request build for docker image: ${imageName}`); diff --git a/src/utilities/util.ts b/src/utilities/util.ts index 3b3c461..1f84212 100644 --- a/src/utilities/util.ts +++ b/src/utilities/util.ts @@ -14,11 +14,12 @@ export class Util { /** * It's called once to pass the init values. - * @param context + * @param {vscode.ExtensionContext} context - extension context + * @returns {void} */ - public static init(context: vscode.ExtensionContext): void { + public static init(context: vscode.ExtensionContext) { if (this.config) { - throw new Error("Utils class is already initialized") + throw new Error("Utils class is already initialized"); } // set the resource path Util.resourcePath = context.asAbsolutePath(path.join('resources')); // NOI18N @@ -131,7 +132,10 @@ class NodeConfigImpl implements NodeConfig { // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(workspaceState: any) { this.workspaceState = workspaceState; - const nodeUrl = workspaceState.get(NodeConfigImpl.nodeUrlKey); + let nodeUrl = workspaceState.get(NodeConfigImpl.nodeUrlKey); + if (!nodeUrl) { + nodeUrl = this.defaultUrl; + } this.url = nodeUrl; } diff --git a/src/webviews/IntegrationsView.ts b/src/webviews/IntegrationsView.ts index cfa3935..f3cfe3e 100644 --- a/src/webviews/IntegrationsView.ts +++ b/src/webviews/IntegrationsView.ts @@ -55,21 +55,24 @@ export class IntegrationsView { /** * Requests update view. + * @returns {void} */ - static requestIntegrationsViewUpdate(): void { + static requestIntegrationsViewUpdate() { vscode.commands.executeCommand(this.refreshIntegrationViewCommandId); } /** * Requests update view and model. + * @returns {void} */ - static requestIntegrationsUpdate(): void { + static requestIntegrationsUpdate() { vscode.commands.executeCommand(this.refreshIntegrationCommandId); } /** * Add an integration instance for the Integrations view (e.g. DockerIntegration, etc) - * @param {Integration} integration + * @param {Integration} integration (e.g DockerIntegration) + * @returns {void} */ addIntegration(integration: Integration) { this.treeViewProvider.addIntegration(integration); @@ -77,6 +80,7 @@ export class IntegrationsView { /** * Triggers the tree provider update. + * @returns {Promise} void */ async update(): Promise { if (this.treeViewProvider) { From 696aceab19bd2bdde21773481044ddde88afd3a6 Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Mon, 23 Jan 2023 18:27:21 -0800 Subject: [PATCH 08/16] - A few new tests added - Refactoring and clean up --- .vscode/launch.json | 14 ++++++++++- package.json | 5 ++-- src/integrations/DockerIntegration.ts | 10 ++++---- src/test/suite/extension.test.ts | 36 ++++++++++++++++++++------- src/utilities/pyrsiaClient.ts | 6 ++--- src/utilities/util.ts | 16 ++++++------ src/webviews/HelpView.ts | 2 +- src/webviews/IntegrationsView.ts | 6 ++--- src/webviews/NodeConfigView.ts | 4 +-- 9 files changed, 64 insertions(+), 35 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 5e5afd6..fd0ed72 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,6 +2,19 @@ // https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Launch Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + ], + "outFiles": [ + "${workspaceFolder}/out/test/**/*.js" + ] + }, { "args": [ "--extensionDevelopmentPath=${workspaceFolder}" @@ -17,6 +30,5 @@ "VSCODE_DEBUG_MODE": "true" } } - ] } \ No newline at end of file diff --git a/package.json b/package.json index becccf5..db06871 100644 --- a/package.json +++ b/package.json @@ -14,20 +14,21 @@ "Other" ], "activationEvents": [ + "*" ], "main": "./out/extension.js", "contributes": { "viewsContainers": { "activitybar": [ { - "id": "package-explorer", + "id": "pyrsia", "title": "Pyrsia", "icon": "resources/images/pyrsia_white.svg" } ] }, "views": { - "package-explorer": [ + "pyrsia": [ { "id": "pyrsia.node-config", "name": "Node Status" diff --git a/src/integrations/DockerIntegration.ts b/src/integrations/DockerIntegration.ts index 5d2d8dd..3f23a7e 100644 --- a/src/integrations/DockerIntegration.ts +++ b/src/integrations/DockerIntegration.ts @@ -132,14 +132,14 @@ export class DockerIntegration implements Integration { // pull the deleted image and show the progress (icons in the Integrations view) // eslint-disable-next-line @typescript-eslint/no-explicit-any dockerClient.pull(imageName, (err: string, stream: any) => { - console.log(err); + console.debug(err); // this one is executed when the pulling is done const onFinished = (error_: unknown, output: unknown) => { if (error_) { console.error(error_); } - console.log(output); + console.debug(output); // delete the tree item representing the image, it will be recreated on the next update this.treeItems.delete(DockerIntegration.getTreeItemImageId(imageName)); // request the view (UI) update @@ -148,7 +148,7 @@ export class DockerIntegration implements Integration { // this method is periodically called as the image is being pulled const onProgress = (event: unknown) => { - console.log(event); + console.debug(event); const treeItem = this.treeItems.get(DockerIntegration.getTreeItemImageId(imageName)); if (treeItem) { treeItem.label = `Pulling '${imageName}'`; @@ -264,7 +264,7 @@ export class DockerIntegration implements Integration { new DockerConfigTreeItem(label, id, confFilePath, vscode.TreeItemCollapsibleState.None) ); } else { - console.log(`No configuration for 'Docker' - ${path.join(confPath, fileName)}`); + console.debug(`No configuration for 'Docker' - ${path.join(confPath, fileName)}`); } }); } @@ -542,7 +542,7 @@ class DockerTreeItem extends IntegrationTreeItem { } update(): void { - console.log("Nothing to update in Docker integration tree item"); + console.debug("Nothing to update in Docker integration tree item"); } } diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index b971424..65c1b8e 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -1,13 +1,31 @@ -import * as assert from 'assert'; -import * as vscode from 'vscode'; -// import * as myExtension from '../../extension'; +import * as assert from "assert"; +import * as vscode from "vscode"; +import { NodeConfig } from "../../api/NodeConfig"; +import { Util } from "../../utilities/Util"; -// TODO - Tests -suite('Pyrsia Extension Test Suite', () => { - vscode.window.showInformationMessage('Start all tests.'); +// TODO more tests! +suite("Pyrsia Extension Test Suite", () => { + vscode.window.showInformationMessage("Start all tests."); - test('Test', () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)); - assert.strictEqual(-1, [1, 2, 3].indexOf(0)); + test("Test if the extension is activated", () => { + const extension = vscode.extensions.getExtension("undefined_publisher.pyrsia-extension"); + assert.equal(extension?.isActive, true); }); + + test("Test NodeConfig", () => { + const nodeConfig: NodeConfig = Util.getNodeConfig(); + // check if the config is created + assert(nodeConfig); + // check if has a value + assert(nodeConfig.url); + // set a new value + nodeConfig.url = nodeConfig.defaultUrl; + // check is the node url was correctly assigned + assert.equal(nodeConfig.url, nodeConfig.defaultUrl); + // check host + assert.equal(nodeConfig.host, nodeConfig.defaultUrl.href); + // check host with protocol + assert.equal(nodeConfig.hostWithProtocol, `${nodeConfig.protocol}://${nodeConfig.defaultUrl.href}`); + }); + }); diff --git a/src/utilities/pyrsiaClient.ts b/src/utilities/pyrsiaClient.ts index c48fd58..a5441d4 100644 --- a/src/utilities/pyrsiaClient.ts +++ b/src/utilities/pyrsiaClient.ts @@ -70,7 +70,7 @@ export const getStatus = async (): Promise => { * @returns {Promise<[]>} docker transparency log */ export const getDockerTransparencyLog = async (imageName: string): Promise<[]> => { - console.log(`Get docker image transparency log info for ${imageName}`); + console.debug(`Get docker image transparency log info for ${imageName}`); const nodeUrl = `${Util.getNodeConfig().hostWithProtocol}/inspect/docker`; let data; @@ -94,7 +94,7 @@ export const getDockerTransparencyLog = async (imageName: string): Promise<[]> = * @returns {Promise[]} - the build info (mostly the build ID) */ export const requestDockerBuild = async (imageName: string): Promise<[]> => { - console.log(`Request build for docker image: ${imageName}`); + console.debug(`Request build for docker image: ${imageName}`); const nodeUrl = `${Util.getNodeConfig().hostWithProtocol}/build/docker`; let data; try { @@ -116,7 +116,7 @@ export const requestDockerBuild = async (imageName: string): Promise<[]> => { * @returns {Pyrsia} number of peers */ export const getPeers = async (): Promise => { - console.log('Get node peers'); + console.debug('Get node peers'); const data = await getStatus(); return data ? data.peers_count : "0"; diff --git a/src/utilities/util.ts b/src/utilities/util.ts index 1f84212..2610f80 100644 --- a/src/utilities/util.ts +++ b/src/utilities/util.ts @@ -17,7 +17,7 @@ export class Util { * @param {vscode.ExtensionContext} context - extension context * @returns {void} */ - public static init(context: vscode.ExtensionContext) { + public static init(context: vscode.ExtensionContext): Util { if (this.config) { throw new Error("Utils class is already initialized"); } @@ -25,6 +25,8 @@ export class Util { Util.resourcePath = context.asAbsolutePath(path.join('resources')); // NOI18N // load the configuration from the context (context is used to store the node configuration - e.g URL) this.config = new NodeConfigImpl(context.workspaceState); + + return this; } /** @@ -127,16 +129,12 @@ class NodeConfigImpl implements NodeConfig { private static readonly nodeUrlKey: string = "PYRSIA_NODE_URL_KEY"; // NOI18N private nodeUrl: URL; - private workspaceState; + private workspaceState: vscode.Memento; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(workspaceState: any) { + constructor(workspaceState: vscode.Memento) { this.workspaceState = workspaceState; - let nodeUrl = workspaceState.get(NodeConfigImpl.nodeUrlKey); - if (!nodeUrl) { - nodeUrl = this.defaultUrl; - } - this.url = nodeUrl; + const nodeUrl: string | undefined = workspaceState.get(NodeConfigImpl.nodeUrlKey); + this.url = !nodeUrl ? this.defaultUrl : new URL(nodeUrl); } get defaultUrl(): URL { diff --git a/src/webviews/HelpView.ts b/src/webviews/HelpView.ts index 95a5c8b..f7624ef 100644 --- a/src/webviews/HelpView.ts +++ b/src/webviews/HelpView.ts @@ -36,7 +36,7 @@ export class HelpView { vscode.window.registerTreeDataProvider(HelpView.viewType, this.treeViewProvider); // create the open external help link command const openHelpLink = vscode.commands.registerCommand(HelpUtil.helpCommandId, (helpUrl: string) => { - console.log(`Open ${helpUrl} using '${HelpUtil.helpCommandId}'`); // NOI18N + console.debug(`Open ${helpUrl} using '${HelpUtil.helpCommandId}'`); // NOI18N vscode.env.openExternal(vscode.Uri.parse(helpUrl)); }); // register the open external help link command diff --git a/src/webviews/IntegrationsView.ts b/src/webviews/IntegrationsView.ts index f3cfe3e..896c5c2 100644 --- a/src/webviews/IntegrationsView.ts +++ b/src/webviews/IntegrationsView.ts @@ -119,7 +119,7 @@ class IntegrationsTreeProvider implements vscode.TreeDataProvider { } getChildren(parentId?: string | undefined): vscode.ProviderResult { - console.log(parentId); + console.debug(parentId); let children: string[] = []; for (const integration of this.integrations) { children = children.concat(integration.getTreeItemChildren(parentId)); @@ -139,8 +139,8 @@ class IntegrationsTreeProvider implements vscode.TreeDataProvider { element: string, token: vscode.CancellationToken ): vscode.ProviderResult { - console.log(element); - console.log(token); + console.debug(element); + console.debug(token); return item; } diff --git a/src/webviews/NodeConfigView.ts b/src/webviews/NodeConfigView.ts index bd822e9..ccef350 100644 --- a/src/webviews/NodeConfigView.ts +++ b/src/webviews/NodeConfigView.ts @@ -24,7 +24,7 @@ export class NodeConfigView { private static readonly updateViewCommandId = "pyrsia.node-config.update-view"; // NOI18N private readonly treeViewProvider: NodeConfigTreeProvider; - private readonly view; + private readonly view: vscode.TreeView; private readonly integrations: Set = new Set(); constructor(context: vscode.ExtensionContext) { @@ -59,7 +59,7 @@ export class NodeConfigView { prompt: "Update the Pyrsia node address (e.g. localhost:7888)", validateInput(value) { let errorMessage: string | undefined; - console.info(`Node configuration input: ${value}`); + console.debug(`Node configuration input: ${value}`); if (!value.toLocaleLowerCase().startsWith(Util.getNodeConfig.prototype)) { value = `${Util.getNodeConfig().protocol}://${value}`; } From 48fa68ced0ed1552d2eb4efaa48207335977e5cd Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Mon, 23 Jan 2023 18:31:59 -0800 Subject: [PATCH 09/16] Changelog update --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b8a6f5..3d81d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,17 @@ # Pyrsia VS Code Extension Changelog ## [Unreleased] + Not available in the VS Code store yet. -## [0.0.1] - Apr 25th, 2022 +## [0.1.0] - Apr 25th, 2022 + * Initial release. Features: - * Views: + * Views: * Pyrsia node connection status. * Integrations (Docker support). * Help. - * New commands: + * New commands: * Update Pyrsia node configuration. * Update the docker config. * Add docker image to Pyrsia (request build). From 1a70e8a9c21345ea5fd0e4e06ab4e91063c44616 Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Mon, 23 Jan 2023 18:35:33 -0800 Subject: [PATCH 10/16] The date updated in the changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d81d13..58ea8cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Not available in the VS Code store yet. -## [0.1.0] - Apr 25th, 2022 +## [0.1.0] - Jan 23th, 2023 * Initial release. Features: * Views: From ca1786a02876452603d773387adca2517728a69c Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Mon, 23 Jan 2023 18:41:51 -0800 Subject: [PATCH 11/16] Refactoring: - 'webviews' folder renamed to 'views' --- src/extension.ts | 6 +++--- src/integrations/DockerIntegration.ts | 2 +- src/{webviews => views}/HelpView.ts | 0 src/{webviews => views}/IntegrationsView.ts | 0 src/{webviews => views}/NodeConfigView.ts | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename src/{webviews => views}/HelpView.ts (100%) rename src/{webviews => views}/IntegrationsView.ts (100%) rename src/{webviews => views}/NodeConfigView.ts (100%) diff --git a/src/extension.ts b/src/extension.ts index 42e02e2..ab85913 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,9 +1,9 @@ import * as vscode from "vscode"; -import { NodeConfigView } from "./webviews/NodeConfigView"; +import { NodeConfigView } from "./views/NodeConfigView"; // import { NodeStatusViewProvider } from "./webviews/NodeStatusView"; -import { IntegrationsView as IntegrationsView } from "./webviews/IntegrationsView"; +import { IntegrationsView as IntegrationsView } from "./views/IntegrationsView"; import { Util } from "./utilities/Util"; -import { HelpView } from "./webviews/HelpView"; +import { HelpView } from "./views/HelpView"; import { Integration } from "./api/Integration"; import { DockerIntegration } from "./integrations/DockerIntegration"; diff --git a/src/integrations/DockerIntegration.ts b/src/integrations/DockerIntegration.ts index 3f23a7e..08a420b 100644 --- a/src/integrations/DockerIntegration.ts +++ b/src/integrations/DockerIntegration.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import { Util } from "../utilities/Util"; import * as vscode from "vscode"; import * as client from "../utilities/pyrsiaClient"; -import { IntegrationsView } from "../webviews/IntegrationsView"; +import { IntegrationsView } from "../views/IntegrationsView"; /** * Implements Docker support for Pyrsia. diff --git a/src/webviews/HelpView.ts b/src/views/HelpView.ts similarity index 100% rename from src/webviews/HelpView.ts rename to src/views/HelpView.ts diff --git a/src/webviews/IntegrationsView.ts b/src/views/IntegrationsView.ts similarity index 100% rename from src/webviews/IntegrationsView.ts rename to src/views/IntegrationsView.ts diff --git a/src/webviews/NodeConfigView.ts b/src/views/NodeConfigView.ts similarity index 100% rename from src/webviews/NodeConfigView.ts rename to src/views/NodeConfigView.ts From ff734c7990593c0fe6c47af2eea727924a21cece Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Tue, 24 Jan 2023 13:25:56 -0800 Subject: [PATCH 12/16] - Pre commit script added - Github issue/feature templates added - Github Codeowners - Github PR template --- .github/CODEOWNERS | 1 + .github/ISSUE_TEMPLATE/bug_report.yaml | 35 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature.yaml | 22 +++++++++++++ .github/pull_request_template.md | 43 ++++++++++++++++++++++++++ package-lock.json | 8 ++--- pre-commit.sh | 11 +++++++ 6 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature.yaml create mode 100644 .github/pull_request_template.md create mode 100755 pre-commit.sh diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..815b613 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @pyrsia/collaborators diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..77fc73a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,35 @@ +name: "❌ Bug report 🐛" +description: Report errors bugs and unexpected behavior +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Please make sure to [search for existing issues](https://github.com/pyrsia/pyrsia-vscode-extension/issues) before creating a new report. + + If possible, upload an image of the case as well, it'd be useful for the maintainers. + + - type: textarea + attributes: + label: Steps taken/Steps to reproduce + placeholder: Tell us the steps required to experience the bug. + validations: + required: true + + - type: textarea + attributes: + label: Expected Behavior + description: If you want to include screenshots, paste them into the markdown editor below. + placeholder: What were you expecting? + validations: + required: false + + - type: textarea + attributes: + label: Experienced Instance of the bug + placeholder: | + VS Code Version: + Operating system: + Pyrsia IDE Extension SHA/Version: + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml new file mode 100644 index 0000000..270e7b1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -0,0 +1,22 @@ +name: "💡 Feature suggestion ✅" +description: Suggest new features and propose new ideas +labels: ["triage", "enhancement"] +body: + - type: markdown + attributes: + value: | + Please make sure to [search for existing suggestions](https://github.com/pyrsia/pyrsia-vscode-extension/issues) before creating a new feature-request. + + - type: textarea + attributes: + label: Suggestion details + placeholder: Tell us about the new suggestion + validations: + required: true + + - type: textarea + attributes: + label: Additional details + placeholder: Any additional details including images, links, media + validations: + required: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..5fb5bd3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,43 @@ + + +## Description + + + +Fixes pyrsia/pyrsia-vscode-extension# + +This PR does... by accomplishing... and it can be reviewed by... you can also test the changes by running... + +## Screenshots (optional) + +## PR Checklist + + + +- [ ] I've read the [contributing guidelines](https://github.com/pyrsia/.github/blob/main/contributing.md). +- [ ] I've read ["What is a Good PR?"](https://github.com/pyrsia/pyrsia/blob/main/docs/community/get_involved/good_pr.md) +- [ ] I've included a good title and brief description along with how to review them. +- [ ] I've linked any associated an [issue](https://github.com/pyrsia/pyrsia-vscode-extension/issues). + +### Code Contributions + + + +- [ ] I've run [pre-commit.sh](https://github.com/pyrsia/pyrsia-vscode-extension/blob/main/pre-commit.sh) successfully. diff --git a/package-lock.json b/package-lock.json index ac70a52..f5b01f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "pyrsia-integration", - "version": "0.0.1", + "name": "pyrsia-extension", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "pyrsia-integration", - "version": "0.0.1", + "name": "pyrsia-extension", + "version": "0.1.0", "dependencies": { "@vscode/webview-ui-toolkit": "^1.2.0", "axios": "^1.2.1", diff --git a/pre-commit.sh b/pre-commit.sh new file mode 100755 index 0000000..1504210 --- /dev/null +++ b/pre-commit.sh @@ -0,0 +1,11 @@ +echo "Pre commit check." +echo "👋 Removing 'node_modules' and 'out' folder." +rm -rf ./node_modules ./out +echo "👋 Compiling the project" +npm install +npm run compile +echo "👋 Running the linter." +npm run lint +echo "👋 Running the tests." +npm run test +echo "Pre commit completed." From 6d570063b5aab6616260cd0216f0689cbdb95fa9 Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Tue, 24 Jan 2023 14:52:46 -0800 Subject: [PATCH 13/16] Refactoring related to 'forceConsistentCasingInFileNames' --- src/utilities/{util.ts => Util.ts} | 0 src/views/NodeConfigView.ts | 2 +- tsconfig.json | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) rename src/utilities/{util.ts => Util.ts} (100%) diff --git a/src/utilities/util.ts b/src/utilities/Util.ts similarity index 100% rename from src/utilities/util.ts rename to src/utilities/Util.ts diff --git a/src/views/NodeConfigView.ts b/src/views/NodeConfigView.ts index ccef350..9f3425b 100644 --- a/src/views/NodeConfigView.ts +++ b/src/views/NodeConfigView.ts @@ -1,11 +1,11 @@ // https://github.com/xojs/eslint-config-xo-typescript/issues/43 /* eslint-disable @typescript-eslint/naming-convention */ import * as vscode from "vscode"; -import { Util } from "../utilities/Util"; import * as client from "../utilities/pyrsiaClient"; import { HelpUtil } from "./HelpView"; import { Event, Integration } from "../api/Integration"; import { IntegrationsView } from "./IntegrationsView"; +import { Util } from "../utilities/Util"; enum NodeConfigProperty { Status = "status", // NOI18N diff --git a/tsconfig.json b/tsconfig.json index 9ea4441..52eb29d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,5 +14,6 @@ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ "noUnusedParameters": true, /* Report errors on unused parameters. */ + "forceConsistentCasingInFileNames": true, } } From 0194815a71c10801ac65e4b387d9fe6ce39e2fe0 Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Tue, 24 Jan 2023 15:56:18 -0800 Subject: [PATCH 14/16] New github action added, PR check (compile, test, lint changes) --- .github/workflows/build_lint_test.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/build_lint_test.yml diff --git a/.github/workflows/build_lint_test.yml b/.github/workflows/build_lint_test.yml new file mode 100644 index 0000000..02874e8 --- /dev/null +++ b/.github/workflows/build_lint_test.yml @@ -0,0 +1,25 @@ +name: Pyrsia VS Code Extension Merge Check (build, lint, test) + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + push: + branches: + - main +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 19 + - run: sudo apt-get install xvfb + - run: npm install + - run: npm run compile + - run: npm run lint + - run: xvfb-run --auto-servernum npm run test From 5bb390e4de04728e7dcdd554e16842a9cebcbbfd Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Wed, 25 Jan 2023 14:37:22 -0800 Subject: [PATCH 15/16] README.md update - including additonal info about how to run and debug the extension and tests. --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e1f9d9b..4067a9d 100644 --- a/README.md +++ b/README.md @@ -18,15 +18,17 @@ Pyrsia support for Microsoft VS Code (extension).**This is an early prototype an npm install npm run watch ``` +- Open VS Code, in the Activity Bar select "Run and Debug" and make sure the "Lunch Extension" configuration is selected. +- Press F5 to run the Pyrsia extension (debug mode), a new VS Code instance will appear and should have the Pyrsia extension installed (should be shown in the Activity Bar). -- Press F5 to run the extension, a new VS Code instance will appear and should have the extension installed. - -## How to test the project (TBD) +## How to test the project (and debug the tests) ```sh npm run test ``` +To debug the tests, in the Activity Bar select "Run and Debug" and make sure the "Lunch Tests" configuration the active one, press F5 to start the tests. + ## Before merging or creating PR - Run the tests and linter. From 739d48e805921f78ba28c82876131eac323110b1 Mon Sep 17 00:00:00 2001 From: Karol Harezlak Date: Fri, 27 Jan 2023 09:23:05 -0800 Subject: [PATCH 16/16] - Trailing spaces fixed - New linter rulei added to detect trailing spaces (mostly *.ts files, more work is needed to cover the files outside of /src) --- .eslintrc.cjs | 7 ++++--- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .gitignore | 2 +- .vscode/launch.json | 2 +- .vscode/settings.json | 2 +- .vscode/tasks.json | 2 +- src/api/Integration.ts | 2 +- src/api/NodeConfig.ts | 4 ++-- src/integrations/DockerIntegration.ts | 16 ++++++++-------- src/utilities/Util.ts | 16 ++++++++-------- src/views/HelpView.ts | 2 +- src/views/IntegrationsView.ts | 2 +- src/views/NodeConfigView.ts | 2 +- 13 files changed, 31 insertions(+), 30 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b0cc4cb..ec79a83 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -78,10 +78,11 @@ module.exports = { "object-curly-spacing": ["warn", "always"], "operator-linebreak": "warn", "semi-style": "warn", - "sort-keys": "warn", + "sort-keys": "warn", "require-jsdoc": "warn", "no-useless-constructor": "warn", "prefer-destructuring": "warn", - "no-var": "error" + "no-var": "error", + "no-trailing-spaces": "error" }, - }; \ No newline at end of file + }; diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 77fc73a..194bbeb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -28,7 +28,7 @@ body: attributes: label: Experienced Instance of the bug placeholder: | - VS Code Version: + VS Code Version: Operating system: Pyrsia IDE Extension SHA/Version: validations: diff --git a/.gitignore b/.gitignore index 8a6a127..ff37e77 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ node_modules .idea out .vscode-test -*.vsix \ No newline at end of file +*.vsix diff --git a/.vscode/launch.json b/.vscode/launch.json index fd0ed72..7c4e2bd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,4 +31,4 @@ } } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 9943c44..d3afbcf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,4 +14,4 @@ "warningupdatenode", "webviews" ] -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 13b7744..3968b63 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -10,4 +10,4 @@ "detail": "tsc -watch -p ./" } ] -} \ No newline at end of file +} diff --git a/src/api/Integration.ts b/src/api/Integration.ts index a9f07f0..2d0fe5a 100644 --- a/src/api/Integration.ts +++ b/src/api/Integration.ts @@ -24,7 +24,7 @@ export interface Integration { /** * Returns an array of children ids (treeItemId). - * @param {string} parentId + * @param {string} parentId * @returns {string[]} array of children IDs (treeItemId) */ getTreeItemChildren(parentId?: string): string[]; diff --git a/src/api/NodeConfig.ts b/src/api/NodeConfig.ts index 2b72edb..11e0a6c 100644 --- a/src/api/NodeConfig.ts +++ b/src/api/NodeConfig.ts @@ -12,10 +12,10 @@ export interface NodeConfig { * @returns {string} protocol */ get protocol(): string; - + /** * Getter, returns the host name as string (e.g 'localhost:7888'). - * @returns {string} host + * @returns {string} host */ get host(): string; diff --git a/src/integrations/DockerIntegration.ts b/src/integrations/DockerIntegration.ts index 08a420b..0ecb71b 100644 --- a/src/integrations/DockerIntegration.ts +++ b/src/integrations/DockerIntegration.ts @@ -15,7 +15,7 @@ export class DockerIntegration implements Integration { static readonly confirmOption = "Yes"; static readonly cancelOption = "No"; static readonly closeOption = "Close"; - + // command Ids static readonly updateDockerConfCommandId = "pyrsia.docker.update-config"; // NOI18N static readonly reloadDockerImagesCommandId = "pyrsia.docker.replace-images"; // NOI18N @@ -38,7 +38,7 @@ export class DockerIntegration implements Integration { private static dockerConfigPathsMap: Map = new Map(); // Used in the docker configuration logic (the property we have to update) private static readonly registryMirrorsName = "registry-mirrors"; - + // 'static' tree items props private static readonly mainTreeItemName = "Docker"; private static readonly configTreeItemName = "Configuration"; @@ -53,11 +53,11 @@ export class DockerIntegration implements Integration { // TODO add support for windows! DockerIntegration.dockerConfigPathsMap.set(path.join(os.homedir(), ".docker"), "daemon.json"); } - + constructor(context: vscode.ExtensionContext) { // get the icon info for the main tree item (Docker) this.mainTreeItemIconPath = { - dark: path.join(Util.getResourceImagePath(), "docker_small_dark.svg"), + dark: path.join(Util.getResourceImagePath(), "docker_small_dark.svg"), light: path.join(Util.getResourceImagePath(), "docker_small_dark.svg") //TODO create the "light" icon }; @@ -111,7 +111,7 @@ export class DockerIntegration implements Integration { // if the image has containers go to hell (skip it and warn the user) if (containers.length > 0) { vscode.window.showErrorMessage( - `Reloading the '${imageName}' docker image + `Reloading the '${imageName}' docker image failed because it has container(s) attached, please remove the container(s) and try again.`, DockerIntegration.closeOption ); @@ -123,7 +123,7 @@ export class DockerIntegration implements Integration { if (error) { // something went wrong, warn the user then go to the next image vscode.window.showErrorMessage( - `Reloading the '${imageName}' docker image + `Reloading the '${imageName}' docker image failed, Error: ${error}`, DockerIntegration.closeOption ); @@ -238,7 +238,7 @@ export class DockerIntegration implements Integration { // update the Docker tree item in case Docker or node is down const dockerTreeItem = this.treeItems.get(DockerIntegration.integrationId); if (dockerTreeItem && (!isDockerUp || !isPyrsiaNodeUp)) { - // create warning tree item and hide the rest of the tree items + // create warning tree item and hide the rest of the tree items dockerTreeItem.label = `${DockerIntegration.mainTreeItemName} (Pyrsia Node or Docker is unavailable)`; dockerTreeItem.tooltip = "Please make sure that Docker service and Pyrsia node is up and configured"; dockerTreeItem.iconPath = DockerIntegration.warningIconPath; @@ -408,7 +408,7 @@ export class DockerIntegration implements Integration { } } ); - + // request docker image build const requestDockerImageBuild = vscode.commands.registerCommand( DockerIntegration.requestBuildId, diff --git a/src/utilities/Util.ts b/src/utilities/Util.ts index 2610f80..86796ca 100644 --- a/src/utilities/Util.ts +++ b/src/utilities/Util.ts @@ -66,7 +66,7 @@ export class Util { return this.dockerClient; } - + /** * Checks if in the debug mode. * @returns {boolean} Boolean true if in the debug mode @@ -86,7 +86,7 @@ export class Util { } console.debug(message); } - + /** * Sleeps for the given amount of time. * @param {number} milliseconds sf @@ -99,7 +99,7 @@ export class Util { /** * Searches for a file in the given dir recursively. * @async - * @param {string} dir path + * @param {string} dir path * @param {string} fileName searched filename * @returns {string} file path | unknown */ @@ -112,13 +112,13 @@ export class Util { break; } } - + return matchedFile; } } /** - * Private NodeConfig implementation. + * Private NodeConfig implementation. */ class NodeConfigImpl implements NodeConfig { // the node supported protocol @@ -127,7 +127,7 @@ class NodeConfigImpl implements NodeConfig { private static readonly defaultNodeUrl = new URL("localhost:7888"); // NOI18N // the configuration ket, it uses to store configuration in context.workspaceState private static readonly nodeUrlKey: string = "PYRSIA_NODE_URL_KEY"; // NOI18N - + private nodeUrl: URL; private workspaceState: vscode.Memento; @@ -136,7 +136,7 @@ class NodeConfigImpl implements NodeConfig { const nodeUrl: string | undefined = workspaceState.get(NodeConfigImpl.nodeUrlKey); this.url = !nodeUrl ? this.defaultUrl : new URL(nodeUrl); } - + get defaultUrl(): URL { return NodeConfigImpl.defaultNodeUrl; } @@ -144,7 +144,7 @@ class NodeConfigImpl implements NodeConfig { get hostWithProtocol(): string { let host = this.nodeUrl.href; if (!host.toLocaleLowerCase().startsWith(NodeConfigImpl.protocol)) { - host = `${NodeConfigImpl.protocol}://${host}`; + host = `${NodeConfigImpl.protocol}://${host}`; } return host; diff --git a/src/views/HelpView.ts b/src/views/HelpView.ts index f7624ef..5277db3 100644 --- a/src/views/HelpView.ts +++ b/src/views/HelpView.ts @@ -90,7 +90,7 @@ class HelpTreeProvider implements vscode.TreeDataProvider { * Help Tree Item */ class HelpTreeItem extends vscode.TreeItem { - + private static readonly properties = { [HelpProperty.Install.toLowerCase()]: { iconPath: new vscode.ThemeIcon("getting-started-beginner"), // NOI18N diff --git a/src/views/IntegrationsView.ts b/src/views/IntegrationsView.ts index 896c5c2..621fd6a 100644 --- a/src/views/IntegrationsView.ts +++ b/src/views/IntegrationsView.ts @@ -12,7 +12,7 @@ export class IntegrationsView { private static readonly refreshIntegrationViewCommandId: string = "pyrsia.integrations.view.update"; // NOI18N private static readonly refreshIntegrationCommandId: string = "pyrsia.integrations.update"; // NOI18N private static readonly viewType: string = "pyrsia.node-integrations"; // NOI18N - + private readonly treeViewProvider: IntegrationsTreeProvider; private readonly _view?: vscode.TreeView; diff --git a/src/views/NodeConfigView.ts b/src/views/NodeConfigView.ts index 9f3425b..8092727 100644 --- a/src/views/NodeConfigView.ts +++ b/src/views/NodeConfigView.ts @@ -194,7 +194,7 @@ class NodeTreeItem extends vscode.TreeItem { private static readonly cloudIcon = new vscode.ThemeIcon("cloud"); // NOI18N private static readonly brokenConnectionIcon = new vscode.ThemeIcon("alert"); // NOI18N private static readonly peersCountIcon = new vscode.ThemeIcon("extensions-install-count"); // NOI18N - + // Tree item properties and the logic to update it. private static readonly properties = { [NodeConfigProperty.Status.toLowerCase()]: {