diff --git a/RELEASE_GUIDE.md b/RELEASE_GUIDE.md index 8821fedc38..d46911b0ce 100644 --- a/RELEASE_GUIDE.md +++ b/RELEASE_GUIDE.md @@ -15,6 +15,8 @@ All releases will be made by creating a PR which bumps the version field in the 1. If you are making a patch release (or a prerelease for one) make sure you are on the `release/v.` branch. 1. Run `npm run create-release-pr`. 1. Pick the PRs that you want to include in this release using the keys listed. + - If you are making a patch release this might include fixing up some cherry-picking of commits. These actions should be done in a separate terminal. + - If a package version is having a major version bump then `npm` will complain about `peerDependency` conflicts. These will have to be fixed up separately. 1. Once the PR is created, approved, and then merged the `Release Open Lens` workflow will create a tag and release for you. 1. If you are making a major or minor release, create a `release/v.` branch and push it to `origin` so that future patch releases can be made from it. 1. If you released a major or minor version, create a new patch milestone and move all bug issues to that milestone and all enhancement issues to the next minor milestone. diff --git a/package-lock.json b/package-lock.json index 2b313d03d2..946b5f8168 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,20 +73,20 @@ } }, "node_modules/@babel/core": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.3.tgz", - "integrity": "sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.0.tgz", + "integrity": "sha512-PuxUbxcW6ZYe656yL3EAhpy7qXKq0DmYsrJLpbB8XrsCP9Nm+XCg9XFMb5vIDliPD7+U/+M+QJlH17XOcB7eXA==", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.21.3", + "@babel/generator": "^7.21.0", "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-module-transforms": "^7.21.2", + "@babel/helper-module-transforms": "^7.21.0", "@babel/helpers": "^7.21.0", - "@babel/parser": "^7.21.3", + "@babel/parser": "^7.21.0", "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.3", - "@babel/types": "^7.21.3", + "@babel/traverse": "^7.21.0", + "@babel/types": "^7.21.0", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -115,9 +115,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.21.3.tgz", - "integrity": "sha512-kfhmPimwo6k4P8zxNs8+T7yR44q1LdpsZdE1NkCsVlfiuTPRfnGgjaF8Qgug9q9Pou17u6wneYF0lDCZJATMFg==", + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz", + "integrity": "sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==", "peer": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", @@ -151,11 +151,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.3.tgz", - "integrity": "sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==", + "version": "7.21.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.1.tgz", + "integrity": "sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA==", "dependencies": { - "@babel/types": "^7.21.3", + "@babel/types": "^7.21.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -595,9 +595,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz", - "integrity": "sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==", + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.2.tgz", + "integrity": "sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1269,9 +1269,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", - "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz", + "integrity": "sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.20.2" @@ -1523,9 +1523,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz", - "integrity": "sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz", + "integrity": "sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.20.2" @@ -1754,12 +1754,11 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.3.tgz", - "integrity": "sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.0.tgz", + "integrity": "sha512-xo///XTPp3mDzTtrqXoBlK9eiAYW3wv9JXglcn/u1bi60RW11dEUxIgA8cbnDhutS1zacjMRmAwxE0gMklLnZg==", "peer": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-create-class-features-plugin": "^7.21.0", "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-typescript": "^7.20.0" @@ -1997,18 +1996,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.3.tgz", - "integrity": "sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==", + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.2.tgz", + "integrity": "sha512-ts5FFU/dSUPS13tv8XiEObDu9K+iagEKME9kAbaP7r0Y9KtZJZ+NGndDvWoRAYNpeWafbpFeki3q9QoMD6gxyw==", "dependencies": { "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.21.3", + "@babel/generator": "^7.21.1", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.21.0", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.3", - "@babel/types": "^7.21.3", + "@babel/parser": "^7.21.2", + "@babel/types": "^7.21.2", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -2017,9 +2016,9 @@ } }, "node_modules/@babel/types": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz", - "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==", + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.2.tgz", + "integrity": "sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==", "dependencies": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", @@ -2100,92 +2099,25 @@ } }, "node_modules/@electron/get": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.14.1.tgz", - "integrity": "sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz", + "integrity": "sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==", "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", - "got": "^9.6.0", + "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "engines": { - "node": ">=8.6" + "node": ">=12" }, "optionalDependencies": { - "global-agent": "^3.0.0", - "global-tunnel-ng": "^2.7.1" + "global-agent": "^3.0.0" } }, - "node_modules/@electron/get/node_modules/@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/@electron/get/node_modules/@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "dependencies": { - "defer-to-connect": "^1.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@electron/get/node_modules/cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/get/node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@electron/get/node_modules/decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@electron/get/node_modules/defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" - }, "node_modules/@electron/get/node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -2199,51 +2131,6 @@ "node": ">=6 <7 || >=8" } }, - "node_modules/@electron/get/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@electron/get/node_modules/got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "dependencies": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/@electron/get/node_modules/got/node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@electron/get/node_modules/json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==" - }, "node_modules/@electron/get/node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -2252,55 +2139,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/@electron/get/node_modules/keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "dependencies": { - "json-buffer": "3.0.0" - } - }, - "node_modules/@electron/get/node_modules/normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/get/node_modules/p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/@electron/get/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/@electron/get/node_modules/responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", - "dependencies": { - "lowercase-keys": "^1.0.0" - } - }, - "node_modules/@electron/get/node_modules/responselike/node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@electron/get/node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -3114,9 +2952,9 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.4.tgz", - "integrity": "sha512-SQOeVbMwb1di+mVWWJLpsUTToKfqVNioXys011beCAhyOIFtS+GQoW4EQSneuxzmQKddExDwQ+X0hLl4lJJaSQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.3.tgz", + "integrity": "sha512-upVRtrNZuYNsw+EoxkiBFRPROnU8UTy/u/dZ9U0W14BlemPYODwhhxYXSR2Y9xOnvr1XtptJRWx7gL8Te1qaog==", "dev": true }, "node_modules/@floating-ui/dom": { @@ -4686,6 +4524,14 @@ "resolved": "packages/bump-version-for-cron", "link": true }, + "node_modules/@k8slens/cluster-settings": { + "resolved": "packages/cluster-settings", + "link": true + }, + "node_modules/@k8slens/computed-channel": { + "resolved": "packages/technical-features/messaging/computed-channel", + "link": true + }, "node_modules/@k8slens/core": { "resolved": "packages/core", "link": true @@ -4722,6 +4568,22 @@ "resolved": "packages/technical-features/application/legacy-extensions", "link": true }, + "node_modules/@k8slens/messaging": { + "resolved": "packages/technical-features/messaging/agnostic", + "link": true + }, + "node_modules/@k8slens/messaging-fake-bridge": { + "resolved": "packages/technical-features/messaging/message-bridge-fake", + "link": true + }, + "node_modules/@k8slens/messaging-for-main": { + "resolved": "packages/technical-features/messaging/electron/main", + "link": true + }, + "node_modules/@k8slens/messaging-for-renderer": { + "resolved": "packages/technical-features/messaging/electron/renderer", + "link": true + }, "node_modules/@k8slens/node-fetch": { "resolved": "packages/node-fetch", "link": true @@ -4738,6 +4600,10 @@ "resolved": "packages/semver", "link": true }, + "node_modules/@k8slens/startable-stoppable": { + "resolved": "packages/utility-features/startable-stoppable", + "link": true + }, "node_modules/@k8slens/test-utils": { "resolved": "packages/utility-features/test-utils", "link": true @@ -6882,7 +6748,6 @@ "version": "12.1.5", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.5.tgz", "integrity": "sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg==", - "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", "@testing-library/dom": "^8.0.0", @@ -6900,7 +6765,6 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz", "integrity": "sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -6918,14 +6782,12 @@ "node_modules/@testing-library/react/node_modules/@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", - "dev": true + "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==" }, "node_modules/@testing-library/react/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, "engines": { "node": ">=10" }, @@ -6937,7 +6799,6 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, "dependencies": { "deep-equal": "^2.0.5" } @@ -6946,7 +6807,6 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -7506,16 +7366,6 @@ "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", "dev": true }, - "node_modules/@types/mini-css-extract-plugin": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@types/mini-css-extract-plugin/-/mini-css-extract-plugin-2.5.1.tgz", - "integrity": "sha512-evjjtJttaUexgg3au9ZJFy76tV9mySwX3a4Jl82BuormBYluWLRt0xk2urWrhOdPgDWzulRFyotwYOJTmkSgKw==", - "deprecated": "This is a stub types definition. mini-css-extract-plugin provides its own type definitions, so you do not need this installed.", - "dev": true, - "dependencies": { - "mini-css-extract-plugin": "*" - } - }, "node_modules/@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", @@ -7663,7 +7513,6 @@ "version": "17.0.19", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.19.tgz", "integrity": "sha512-PiYG40pnQRdPHnlf7tZnp0aQ6q9tspYr72vD61saO6zFCybLfMqwUCN0va1/P+86DXn18ZWeW30Bk7xlC5eEAQ==", - "dev": true, "dependencies": { "@types/react": "^17" } @@ -7862,16 +7711,6 @@ "integrity": "sha512-6pwWTx8oUtWvsiZUCrhrK/53MzKVLnuNSSaZILPy3uMes9QnTrLMar9BDlJArbMOjDcjb3QXFk6Rz8qmmuySZw==", "dev": true }, - "node_modules/@types/tempy": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@types/tempy/-/tempy-0.3.0.tgz", - "integrity": "sha512-graSgBSy4TEsenfAasXh28mxfdrLkgrFlXU8QFShFVNHEGHCg5IfO5LBlZjq2SAUnCm/VdxvuQKlp2buPR/YwA==", - "deprecated": "This is a stub types definition. tempy provides its own type definitions, so you do not need this installed.", - "dev": true, - "dependencies": { - "tempy": "*" - } - }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.5", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", @@ -7981,6 +7820,16 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.55.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.55.0.tgz", @@ -11058,6 +10907,7 @@ "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, "engines": [ "node >= 0.8" ], @@ -11071,12 +10921,14 @@ "node_modules/concat-stream/node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true }, "node_modules/concat-stream/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -11091,6 +10943,7 @@ "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" } @@ -11182,7 +11035,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", - "devOptional": true, + "dev": true, "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" @@ -12661,11 +12514,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "node_modules/duplexer3": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", - "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==" - }, "node_modules/duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -12740,20 +12588,20 @@ } }, "node_modules/electron": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/electron/-/electron-19.1.9.tgz", - "integrity": "sha512-XT5LkTzIHB+ZtD3dTmNnKjVBWrDWReCKt9G1uAFLz6uJMEVcIUiYO+fph5pLXETiBw/QZBx8egduMEfIccLx+g==", + "version": "22.3.3", + "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.3.tgz", + "integrity": "sha512-+ZJDVfyhw7J2A46/kGKscktIhzOisTeJKrUBJLXa7PTB+U+cwyoxCBIaIOnDsdicBCX4nAc1mo6YMQjQQdAmgw==", "hasInstallScript": true, "dependencies": { - "@electron/get": "^1.14.1", + "@electron/get": "^2.0.0", "@types/node": "^16.11.26", - "extract-zip": "^1.0.3" + "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" }, "engines": { - "node": ">= 8.6" + "node": ">= 12.20.55" } }, "node_modules/electron-builder": { @@ -12944,9 +12792,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.329", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.329.tgz", - "integrity": "sha512-dcwPzNUG4+reo5z+wHnrl2eZMu4kz+nLQEeepxLEDTLDC7Mi7AVTM4NXWct1TZyu3G4oQgygaAfbByaBtPqw2Q==" + "version": "1.4.328", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.328.tgz", + "integrity": "sha512-DE9tTy2PNmy1v55AZAO542ui+MLC2cvINMK4P2LXGsJdput/ThVG9t+QGecPuAZZSgC8XoI+Jh9M1OG9IoNSCw==" }, "node_modules/electron-updater": { "version": "4.6.5", @@ -13058,7 +12906,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "devOptional": true, + "dev": true, "engines": { "node": ">= 0.8" } @@ -15017,42 +14865,46 @@ } }, "node_modules/extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dependencies": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", + "debug": "^4.1.1", + "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "bin": { "extract-zip": "cli.js" - } - }, - "node_modules/extract-zip/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/extract-zip/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==", - "dependencies": { - "minimist": "^1.2.6" }, - "bin": { - "mkdirp": "bin/cmd.js" + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" } }, - "node_modules/extract-zip/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extract-zip/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/extsprintf": { "version": "1.3.0", @@ -16110,22 +15962,6 @@ "node": ">=10.0" } }, - "node_modules/global-tunnel-ng": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz", - "integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==", - "optional": true, - "peer": true, - "dependencies": { - "encodeurl": "^1.0.2", - "lodash": "^4.17.10", - "npm-conf": "^1.1.3", - "tunnel": "^0.0.6" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -21295,16 +21131,6 @@ "language-subtag-registry": "~0.3.2" } }, - "node_modules/launch-editor": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", - "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", - "dev": true, - "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.7.3" - } - }, "node_modules/lazy-cache": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", @@ -24098,30 +23924,6 @@ "npm-normalize-package-bin": "^1.0.1" } }, - "node_modules/npm-conf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", - "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", - "optional": true, - "peer": true, - "dependencies": { - "config-chain": "^1.1.11", - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-conf/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "optional": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/npm-install-checks": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-5.0.0.tgz", @@ -28539,14 +28341,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", - "engines": { - "node": ">=4" - } - }, "node_modules/prettier": { "version": "2.8.4", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", @@ -28741,7 +28535,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "devOptional": true + "dev": true }, "node_modules/protocols": { "version": "2.0.1", @@ -32445,14 +32239,6 @@ "node": ">=0.10.0" } }, - "node_modules/to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -32765,16 +32551,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -32850,8 +32626,7 @@ "node_modules/typed-emitter": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-1.4.0.tgz", - "integrity": "sha512-weBmoo3HhpKGgLBOYwe8EB31CzDFuaK7CCL+axXhUYhn4jo6DSkHnbefboCF5i4DQ2aMFe0C/FdTWcPdObgHyg==", - "dev": true + "integrity": "sha512-weBmoo3HhpKGgLBOYwe8EB31CzDFuaK7CCL+axXhUYhn4jo6DSkHnbefboCF5i4DQ2aMFe0C/FdTWcPdObgHyg==" }, "node_modules/typed-regex": { "version": "0.0.8", @@ -32861,7 +32636,8 @@ "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true }, "node_modules/typedoc": { "version": "0.23.25", @@ -33185,17 +32961,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", - "dependencies": { - "prepend-http": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/use-isomorphic-layout-effect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", @@ -33633,9 +33398,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.12.0.tgz", - "integrity": "sha512-XRN9YRnvOj3TQQ5w/0pR1y1xDcVnbWtNkTri46kuEbaWUPTHsWUvOyAAI7PZHLY+hsFki2kRltJjKMw7e+IiqA==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz", + "integrity": "sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==", "dev": true, "dependencies": { "@types/bonjour": "^3.5.9", @@ -33657,7 +33422,6 @@ "html-entities": "^2.3.2", "http-proxy-middleware": "^2.0.3", "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", "open": "^8.0.9", "p-retry": "^4.5.0", "rimraf": "^3.0.2", @@ -33667,7 +33431,7 @@ "sockjs": "^0.3.24", "spdy": "^4.0.2", "webpack-dev-middleware": "^5.3.1", - "ws": "^8.13.0" + "ws": "^8.4.2" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" @@ -34382,6 +34146,25 @@ "integrity": "sha512-ZOzvDRWp8dCVBmgnkIqYCArgdFOO9YzocZp8Ra25N/RStKiWvMOXHMz+GjSeVNe5TstaTmTWPucGJkDw0XXJWA==", "dev": true }, + "packages/cluster-settings": { + "name": "@k8slens/cluster-settings", + "version": "6.5.0-alpha.1", + "license": "MIT", + "devDependencies": { + "@ogre-tools/injectable": "^15.1.2", + "@swc/cli": "^0.1.61", + "@swc/core": "^1.3.37", + "@types/node": "^16.18.11", + "@types/semver": "^7.3.13", + "rimraf": "^4.1.2" + } + }, + "packages/cluster-settings/node_modules/@types/node": { + "version": "16.18.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.18.tgz", + "integrity": "sha512-fwGw1uvQAzabxL1pyoknPlJIF2t7+K90uTqynleKRx24n3lYcxWa3+KByLhgkF8GEAK2c7hC8Ki0RkNM5H15jQ==", + "dev": true + }, "packages/core": { "name": "@k8slens/core", "version": "6.5.0-alpha.3", @@ -34390,6 +34173,7 @@ "@astronautlabs/jsonpath": "^1.1.0", "@hapi/call": "^9.0.1", "@hapi/subtext": "^7.1.0", + "@k8slens/cluster-settings": "^6.5.0-alpha.1", "@k8slens/node-fetch": "^6.5.0-alpha.1", "@kubernetes/client-node": "^0.18.1", "@material-ui/styles": "^4.11.5", @@ -34401,7 +34185,6 @@ "@sentry/electron": "^3.0.8", "@sentry/integrations": "^6.19.3", "@side/jest-runtime": "^1.1.0", - "abort-controller": "^3.0.0", "auto-bind": "^4.0.0", "await-lock": "^2.2.2", "byline": "^5.0.0", @@ -34460,6 +34243,7 @@ }, "devDependencies": { "@async-fn/jest": "1.6.4", + "@k8slens/messaging-fake-bridge": "^1.0.0-alpha.1", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", @@ -34490,7 +34274,6 @@ "@types/marked": "^4.0.8", "@types/md5-file": "^4.0.2", "@types/memorystream": "^0.3.0", - "@types/mini-css-extract-plugin": "^2.4.0", "@types/mock-fs": "^4.13.1", "@types/node": "^16.18.11", "@types/proper-lockfile": "^4.1.2", @@ -34507,7 +34290,6 @@ "@types/semver": "^7.3.13", "@types/tar": "^6.1.4", "@types/tcp-port-used": "^1.0.1", - "@types/tempy": "^0.3.0", "@types/triple-beam": "^1.3.2", "@types/url-parse": "^1.4.8", "@types/uuid": "^8.3.4", @@ -34529,7 +34311,7 @@ "css-loader": "^6.7.3", "deepdash": "^5.3.9", "dompurify": "^2.4.4", - "electron": "^19.1.9", + "electron": "^22.3.3", "electron-builder": "^23.6.0", "esbuild": "^0.17.8", "esbuild-loader": "^2.21.0", @@ -34598,7 +34380,11 @@ "@k8slens/application": "^6.5.0-alpha.0", "@k8slens/application-for-electron-main": "^6.5.0-alpha.0", "@k8slens/legacy-extensions": "^1.0.0-alpha.0", + "@k8slens/messaging": "^1.0.0-alpha.1", + "@k8slens/messaging-for-main": "^1.0.0-alpha.1", + "@k8slens/messaging-for-renderer": "^1.0.0-alpha.1", "@k8slens/run-many": "^1.0.0-alpha.1", + "@k8slens/startable-stoppable": "^1.0.0-alpha.1", "@k8slens/test-utils": "^1.0.0-alpha.1", "@k8slens/utilities": "^1.0.0-alpha.1", "@types/byline": "^4.2.33", @@ -36664,7 +36450,11 @@ "@k8slens/generate-tray-icons": "^6.5.0-alpha.1", "@k8slens/legacy-extension-example": "^1.0.0-alpha.1", "@k8slens/legacy-extensions": "^1.0.0-alpha.1", + "@k8slens/messaging": "^1.0.0-alpha.1", + "@k8slens/messaging-for-main": "^1.0.0-alpha.1", + "@k8slens/messaging-for-renderer": "^1.0.0-alpha.1", "@k8slens/run-many": "^1.0.0-alpha.1", + "@k8slens/startable-stoppable": "^1.0.0-alpha.1", "@k8slens/test-utils": "^1.0.0-alpha.1", "@k8slens/utilities": "^1.0.0-alpha.1", "@ogre-tools/fp": "^15.1.2", @@ -36702,14 +36492,13 @@ "@types/webpack-dev-server": "^4.7.2", "@types/webpack-env": "^1.18.0", "@types/webpack-node-externals": "2.5.3", - "abort-controller": "^3.0.0", "autoprefixer": "^10.4.13", "circular-dependency-plugin": "^5.2.2", "concurrently": "^7.6.0", "copy-webpack-plugin": "^11.0.0", "cross-env": "^7.0.3", "css-loader": "^6.7.2", - "electron": "^19.1.9", + "electron": "^22.3.3", "electron-builder": "^23.6.0", "electron-notarize": "^0.3.0", "esbuild-loader": "^2.20.0", @@ -37203,7 +36992,7 @@ "@k8slens/feature-core": "^6.5.0-alpha.0", "@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", - "electron": "^19.1.9" + "electron": "^22.3.3" } }, "packages/technical-features/application/legacy-extensions": { @@ -37228,30 +37017,180 @@ "@ogre-tools/injectable": "^15.1.2" } }, + "packages/technical-features/messaging/agnostic": { + "name": "@k8slens/messaging", + "version": "1.0.0-alpha.1", + "license": "MIT", + "devDependencies": { + "@k8slens/eslint-config": "^6.5.0-alpha.1" + }, + "peerDependencies": { + "@k8slens/application": "^6.5.0-alpha.0", + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@k8slens/startable-stoppable": "^1.0.0-alpha.1", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", + "@ogre-tools/injectable-extension-for-mobx": "^15.1.2", + "lodash": "^4.17.21", + "mobx": "^6.7.0" + } + }, + "packages/technical-features/messaging/computed-channel": { + "name": "@k8slens/computed-channel", + "version": "1.0.0-alpha.1", + "license": "MIT", + "devDependencies": { + "@k8slens/eslint-config": "^6.5.0-alpha.1", + "@k8slens/messaging-fake-bridge": "^1.0.0-alpha.1", + "type-fest": "^2.14.0" + }, + "peerDependencies": { + "@k8slens/application": "^6.5.0-alpha.0", + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@k8slens/messaging": "^1.0.0-alpha.1", + "@ogre-tools/fp": "^15.1.2", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", + "@ogre-tools/injectable-extension-for-mobx": "^15.1.2", + "lodash": "^4.17.21", + "mobx": "^6.8.0" + } + }, + "packages/technical-features/messaging/electron/main": { + "name": "@k8slens/messaging-for-main", + "version": "1.0.0-alpha.1", + "license": "MIT", + "devDependencies": { + "@k8slens/eslint-config": "^6.5.0-alpha.1" + }, + "peerDependencies": { + "@k8slens/application": "^6.5.0-alpha.0", + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@k8slens/messaging": "^1.0.0-alpha.1", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", + "electron": "^22.3.3", + "lodash": "^4.17.21" + } + }, + "packages/technical-features/messaging/electron/renderer": { + "name": "@k8slens/messaging-for-renderer", + "version": "1.0.0-alpha.1", + "license": "MIT", + "devDependencies": { + "@k8slens/eslint-config": "^6.5.0-alpha.1" + }, + "peerDependencies": { + "@k8slens/application": "^6.5.0-alpha.0", + "@k8slens/messaging": "^1.0.0-alpha.1", + "@k8slens/run-many": "^1.0.0-alpha.1", + "@k8slens/startable-stoppable": "^1.0.0-alpha.1", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", + "electron": "^22.3.3", + "lodash": "^4.17.21" + } + }, + "packages/technical-features/messaging/message-bridge-fake": { + "name": "@k8slens/messaging-fake-bridge", + "version": "1.0.0-alpha.1", + "license": "MIT", + "devDependencies": { + "@async-fn/jest": "^1.6.4", + "@k8slens/eslint-config": "^6.5.0-alpha.1", + "@k8slens/feature-core": "6.5.0-alpha.1", + "@ogre-tools/injectable-extension-for-mobx": "^15.1.2", + "mobx": "^6.7.0" + }, + "peerDependencies": { + "@k8slens/messaging": "^1.0.0-alpha.1", + "@ogre-tools/fp": "^15.1.2", + "@ogre-tools/injectable": "^15.1.2", + "lodash": "^4.17.21" + } + }, "packages/utility-features/run-many": { "name": "@k8slens/run-many", "version": "1.0.0-alpha.1", "license": "MIT", + "devDependencies": { + "@types/uuid": "^9.0.1" + }, "peerDependencies": { "@k8slens/test-utils": "^1.0.0-alpha.1", "@k8slens/utilities": "^1.0.0-alpha.1", - "@ogre-tools/fp": "^15.1.1", - "@ogre-tools/injectable": "^15.1.1" + "@ogre-tools/fp": "^15.1.2", + "@ogre-tools/injectable": "^15.1.2", + "type-fest": "^2.19.0", + "typed-emitter": "^1.4.0", + "uuid": "^8.3.2" + } + }, + "packages/utility-features/run-many/node_modules/@types/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", + "dev": true + }, + "packages/utility-features/startable-stoppable": { + "name": "@k8slens/startable-stoppable", + "version": "1.0.0-alpha.1", + "license": "MIT", + "devDependencies": { + "@k8slens/eslint-config": "^6.5.0-alpha.1" } }, "packages/utility-features/test-utils": { "name": "@k8slens/test-utils", "version": "1.0.0-alpha.1", - "license": "MIT" + "license": "MIT", + "devDependencies": { + "@types/lodash": "^4.14.191" + }, + "peerDependencies": { + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-react": "^15.1.2", + "@testing-library/react": "^12.1.5", + "lodash": "^4.17.21", + "react": "^17.0.2" + } }, "packages/utility-features/utilities": { "name": "@k8slens/utilities", "version": "1.0.0-alpha.1", "license": "MIT", + "devDependencies": { + "@types/crypto-js": "^4.1.1", + "@types/lodash": "^4.14.191", + "@types/react": "^17.0.2", + "@types/react-router": "^5.1.20", + "@types/readable-stream": "^2.3.15", + "@types/semver": "^7.3.13", + "@types/tar": "^6.1.4", + "type-fest": "^2.14.0" + }, "peerDependencies": { + "@astronautlabs/jsonpath": "^1.1.0", + "crypto-js": "^4.1.1", + "lodash": "^4.17.21", "mobx": "^6.8.0", - "type-fest": "^2.19.0" + "moment": "^2.29.4", + "p-limit": "^3.1.0", + "path-to-regexp": "^6.2.1", + "react": "^17.0.2", + "react-router": "^5.3.4", + "readable-stream": "^3.6.2", + "semver": "^7.3.8", + "tar": "^6.1.13", + "type-fest": "^2.19.0", + "typed-regex": "^0.0.8" } + }, + "packages/utility-features/utilities/node_modules/@types/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==", + "dev": true } } } diff --git a/package.json b/package.json index 982724f37f..0a9f693adb 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "clean:node_modules": "lerna clean -y && rimraf node_modules", "dev": "lerna run dev --stream --skip-nx-cache", "lint": "lerna run lint --stream", + "lint:fix": "lerna run lint:fix --stream", "mkdocs:serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest", "mkdocs:verify": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict", "test:unit": "lerna run --stream test:unit", @@ -28,6 +29,11 @@ "precreate-release-pr": "cd packages/release-tool && npm run build", "create-release-pr": "node packages/release-tool/dist/index.js" }, + "overrides": { + "underscore": "^1.12.1", + "react": "^17", + "@types/react": "^17" + }, "devDependencies": { "adr": "^1.4.3", "cross-env": "^7.0.3", diff --git a/packages/cluster-settings/.swcrc b/packages/cluster-settings/.swcrc new file mode 100644 index 0000000000..8e7a530f16 --- /dev/null +++ b/packages/cluster-settings/.swcrc @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "parser": { + "syntax": "typescript" + }, + "target": "es2022" + } +} diff --git a/packages/cluster-settings/README.md b/packages/cluster-settings/README.md new file mode 100644 index 0000000000..c3d3b890f4 --- /dev/null +++ b/packages/cluster-settings/README.md @@ -0,0 +1,3 @@ +# Description + +The package exports tokens needed for external configuration of Cluster Settings page. diff --git a/packages/cluster-settings/package.json b/packages/cluster-settings/package.json new file mode 100644 index 0000000000..199347dcf4 --- /dev/null +++ b/packages/cluster-settings/package.json @@ -0,0 +1,31 @@ +{ + "name": "@k8slens/cluster-settings", + "version": "6.5.0-alpha.1", + "description": "Injection token exporter for cluster settings configuration", + "license": "MIT", + "private": false, + "mode": "production", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "clean": "rimraf dist/", + "generate-types": "tsc --d --declarationDir ./dist --declarationMap --emitDeclarationOnly", + "build": "npm run generate-types && swc ./src/index.ts -d ./dist", + "prepare:test": "npm run build" + }, + "devDependencies": { + "@ogre-tools/injectable": "^15.1.2", + "@swc/cli": "^0.1.61", + "@swc/core": "^1.3.37", + "@types/node": "^16.18.11", + "@types/semver": "^7.3.13", + "rimraf": "^4.1.2" + } +} diff --git a/packages/cluster-settings/src/index.ts b/packages/cluster-settings/src/index.ts new file mode 100644 index 0000000000..181c69d46a --- /dev/null +++ b/packages/cluster-settings/src/index.ts @@ -0,0 +1,30 @@ +import { getInjectionToken } from "@ogre-tools/injectable"; + +type ClusterPreferences = { + clusterName?: string; + icon?: string | null; +} + +export interface ClusterIconMenuItem { + id: string; + title: string; + disabled?: (preferences: ClusterPreferences) => boolean; + onClick: (preferences: ClusterPreferences) => void; +} + +export interface ClusterIconSettingComponentProps { + preferences: ClusterPreferences; +} + +export interface ClusterIconSettingsComponent { + id: string; + Component: React.ComponentType; +} + +export const clusterIconSettingsMenuInjectionToken = getInjectionToken({ + id: "cluster-icon-settings-menu-injection-token", +}); + +export const clusterIconSettingsComponentInjectionToken = getInjectionToken({ + id: "cluster-icon-settings-component-injection-token", +}); \ No newline at end of file diff --git a/packages/cluster-settings/tsconfig.json b/packages/cluster-settings/tsconfig.json new file mode 100644 index 0000000000..534a5fd447 --- /dev/null +++ b/packages/cluster-settings/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist/", + "paths": { + "*": [ + "node_modules/*", + "types/*" + ] + }, + }, + "include": [ + "src/**/*", + ], + "exclude": [ + "node_modules", + ] +} diff --git a/packages/core/package.json b/packages/core/package.json index bf304a4c13..ad6e7e45ae 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -116,13 +116,11 @@ } } }, - "resolutions": { - "@astronautlabs/jsonpath/underscore": "^1.12.1" - }, "dependencies": { "@astronautlabs/jsonpath": "^1.1.0", "@hapi/call": "^9.0.1", "@hapi/subtext": "^7.1.0", + "@k8slens/cluster-settings": "^6.5.0-alpha.1", "@k8slens/node-fetch": "^6.5.0-alpha.1", "@kubernetes/client-node": "^0.18.1", "@material-ui/styles": "^4.11.5", @@ -134,7 +132,6 @@ "@sentry/electron": "^3.0.8", "@sentry/integrations": "^6.19.3", "@side/jest-runtime": "^1.1.0", - "abort-controller": "^3.0.0", "auto-bind": "^4.0.0", "await-lock": "^2.2.2", "byline": "^5.0.0", @@ -193,6 +190,7 @@ }, "devDependencies": { "@async-fn/jest": "1.6.4", + "@k8slens/messaging-fake-bridge": "^1.0.0-alpha.1", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", @@ -223,7 +221,6 @@ "@types/marked": "^4.0.8", "@types/md5-file": "^4.0.2", "@types/memorystream": "^0.3.0", - "@types/mini-css-extract-plugin": "^2.4.0", "@types/mock-fs": "^4.13.1", "@types/node": "^16.18.11", "@types/proper-lockfile": "^4.1.2", @@ -240,7 +237,6 @@ "@types/semver": "^7.3.13", "@types/tar": "^6.1.4", "@types/tcp-port-used": "^1.0.1", - "@types/tempy": "^0.3.0", "@types/triple-beam": "^1.3.2", "@types/url-parse": "^1.4.8", "@types/uuid": "^8.3.4", @@ -262,7 +258,7 @@ "css-loader": "^6.7.3", "deepdash": "^5.3.9", "dompurify": "^2.4.4", - "electron": "^19.1.9", + "electron": "^22.3.3", "electron-builder": "^23.6.0", "esbuild": "^0.17.8", "esbuild-loader": "^2.21.0", @@ -328,7 +324,11 @@ "@k8slens/application": "^6.5.0-alpha.0", "@k8slens/application-for-electron-main": "^6.5.0-alpha.0", "@k8slens/legacy-extensions": "^1.0.0-alpha.0", + "@k8slens/messaging": "^1.0.0-alpha.1", + "@k8slens/messaging-for-main": "^1.0.0-alpha.1", + "@k8slens/messaging-for-renderer": "^1.0.0-alpha.1", "@k8slens/run-many": "^1.0.0-alpha.1", + "@k8slens/startable-stoppable": "^1.0.0-alpha.1", "@k8slens/test-utils": "^1.0.0-alpha.1", "@k8slens/utilities": "^1.0.0-alpha.1", "@types/byline": "^4.2.33", diff --git a/packages/core/src/common/app-paths/app-path-names.ts b/packages/core/src/common/app-paths/app-path-names.ts index 8e3d2c440e..b7b829a5e3 100644 --- a/packages/core/src/common/app-paths/app-path-names.ts +++ b/packages/core/src/common/app-paths/app-path-names.ts @@ -11,7 +11,7 @@ export const pathNames: PathName[] = [ "home", "appData", "userData", - "cache", + "sessionData", "temp", "exe", "module", diff --git a/packages/core/src/common/app-paths/app-paths-channel.ts b/packages/core/src/common/app-paths/app-paths-channel.ts index 4502569d3b..e738b4896a 100644 --- a/packages/core/src/common/app-paths/app-paths-channel.ts +++ b/packages/core/src/common/app-paths/app-paths-channel.ts @@ -3,11 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import type { AppPaths } from "./app-path-injection-token"; -import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; +import { getRequestChannel } from "@k8slens/messaging"; -export type AppPathsChannel = RequestChannel; - -export const appPathsChannel: AppPathsChannel = { - id: "app-paths", -}; +export const appPathsChannel = getRequestChannel("app-paths"); diff --git a/packages/core/src/common/app-paths/app-paths.test.ts b/packages/core/src/common/app-paths/app-paths.test.ts index f847346dad..851ece4390 100644 --- a/packages/core/src/common/app-paths/app-paths.test.ts +++ b/packages/core/src/common/app-paths/app-paths.test.ts @@ -21,7 +21,6 @@ describe("app-paths", () => { const defaultAppPathsStub: AppPaths = { currentApp: "/some-current-app", appData: "/some-app-data", - cache: "/some-cache", crashDumps: "/some-crash-dumps", desktop: "/some-desktop", documents: "/some-documents", @@ -36,6 +35,7 @@ describe("app-paths", () => { temp: "/some-temp", videos: "/some-videos", userData: "/some-irrelevant-user-data", + sessionData: "/some-irrelevant-user-data", // By default this points to userData }; builder.beforeApplicationStart(({ mainDi }) => { @@ -73,7 +73,6 @@ describe("app-paths", () => { expect(actual).toEqual({ currentApp: "/some-current-app", appData: "/some-app-data", - cache: "/some-cache", crashDumps: "/some-crash-dumps", desktop: "/some-desktop", documents: "/some-documents", @@ -88,6 +87,7 @@ describe("app-paths", () => { temp: "/some-temp", videos: "/some-videos", userData: "/some-app-data/some-product-name", + sessionData: "/some-app-data/some-product-name", }); }); @@ -97,7 +97,6 @@ describe("app-paths", () => { expect(actual).toEqual({ currentApp: "/some-current-app", appData: "/some-app-data", - cache: "/some-cache", crashDumps: "/some-crash-dumps", desktop: "/some-desktop", documents: "/some-documents", @@ -112,6 +111,7 @@ describe("app-paths", () => { temp: "/some-temp", videos: "/some-videos", userData: "/some-app-data/some-product-name", + sessionData: "/some-app-data/some-product-name", }); }); }); diff --git a/packages/core/src/common/base-store/base-store.ts b/packages/core/src/common/base-store/base-store.ts index be7abf1293..a1dd26f0f7 100644 --- a/packages/core/src/common/base-store/base-store.ts +++ b/packages/core/src/common/base-store/base-store.ts @@ -15,7 +15,7 @@ import type { GetConfigurationFileModel } from "../get-configuration-file-model/ import type { Logger } from "../logger"; import type { PersistStateToConfig } from "./save-to-file"; import type { GetBasenameOfPath } from "../path/get-basename.injectable"; -import type { EnlistMessageChannelListener } from "../utils/channel/enlist-message-channel-listener-injection-token"; +import type { EnlistMessageChannelListener } from "@k8slens/messaging"; import { toJS } from "../utils"; export interface BaseStoreParams extends Omit, "migrations"> { @@ -108,6 +108,7 @@ export abstract class BaseStore { this.params.syncOptions, ), this.dependencies.enlistMessageChannelListener({ + id: this.displayName, channel: { id: `${this.dependencies.ipcChannelPrefixes.local}:${config.path}`, }, diff --git a/packages/core/src/common/certificate/lens-proxy-certificate-channel.ts b/packages/core/src/common/certificate/lens-proxy-certificate-channel.ts index 7d9652ce5c..9dd339ee65 100644 --- a/packages/core/src/common/certificate/lens-proxy-certificate-channel.ts +++ b/packages/core/src/common/certificate/lens-proxy-certificate-channel.ts @@ -4,6 +4,6 @@ */ import type { SelfSignedCert } from "selfsigned"; -import { getRequestChannel } from "../utils/channel/get-request-channel"; +import { getRequestChannel } from "@k8slens/messaging"; export const lensProxyCertificateChannel = getRequestChannel("request-lens-proxy-certificate"); diff --git a/packages/core/src/common/cluster-store/cluster-store.injectable.ts b/packages/core/src/common/cluster-store/cluster-store.injectable.ts index ddd811c760..79eb02e36c 100644 --- a/packages/core/src/common/cluster-store/cluster-store.injectable.ts +++ b/packages/core/src/common/cluster-store/cluster-store.injectable.ts @@ -16,7 +16,7 @@ import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync"; import { persistStateToConfigInjectionToken } from "../base-store/save-to-file"; import getBasenameOfPathInjectable from "../path/get-basename.injectable"; -import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token"; +import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; const clusterStoreInjectable = getInjectable({ id: "cluster-store", diff --git a/packages/core/src/common/cluster/authorization-review.injectable.ts b/packages/core/src/common/cluster/authorization-review.injectable.ts deleted file mode 100644 index 3352423377..0000000000 --- a/packages/core/src/common/cluster/authorization-review.injectable.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; -import { AuthorizationV1Api } from "@kubernetes/client-node"; -import { getInjectable } from "@ogre-tools/injectable"; -import loggerInjectable from "../logger.injectable"; - -/** - * Requests the permissions for actions on the kube cluster - * @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed - * @returns `true` if the actions described are allowed - */ -export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise; - -/** - * @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster - */ -export type CreateAuthorizationReview = (proxyConfig: KubeConfig) => CanI; - -const createAuthorizationReviewInjectable = getInjectable({ - id: "authorization-review", - instantiate: (di): CreateAuthorizationReview => { - const logger = di.inject(loggerInjectable); - - return (proxyConfig) => { - const api = proxyConfig.makeApiClient(AuthorizationV1Api); - - return async (resourceAttributes: V1ResourceAttributes): Promise => { - try { - const { body } = await api.createSelfSubjectAccessReview({ - apiVersion: "authorization.k8s.io/v1", - kind: "SelfSubjectAccessReview", - spec: { resourceAttributes }, - }); - - return body.status?.allowed ?? false; - } catch (error) { - logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes }); - - return false; - } - }; - }; - }, -}); - -export default createAuthorizationReviewInjectable; diff --git a/packages/core/src/common/cluster/create-authorization-api.injectable.ts b/packages/core/src/common/cluster/create-authorization-api.injectable.ts new file mode 100644 index 0000000000..c0658a4a34 --- /dev/null +++ b/packages/core/src/common/cluster/create-authorization-api.injectable.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { AuthorizationV1Api } from "@kubernetes/client-node"; +import type { KubeConfig } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; + +export type CreateAuthorizationApi = (config: KubeConfig) => AuthorizationV1Api; + +const createAuthorizationApiInjectable = getInjectable({ + id: "create-authorization-api", + instantiate: (): CreateAuthorizationApi => (config) => config.makeApiClient(AuthorizationV1Api), +}); + +export default createAuthorizationApiInjectable; diff --git a/packages/core/src/common/cluster/create-can-i.injectable.ts b/packages/core/src/common/cluster/create-can-i.injectable.ts new file mode 100644 index 0000000000..59d30dbe77 --- /dev/null +++ b/packages/core/src/common/cluster/create-can-i.injectable.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AuthorizationV1Api, V1ResourceAttributes } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; +import loggerInjectable from "../logger.injectable"; + +/** + * Requests the permissions for actions on the kube cluster + * @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed + * @returns `true` if the actions described are allowed + */ +export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise; + +export type CreateCanI = (api: AuthorizationV1Api) => CanI; + +const createCanIInjectable = getInjectable({ + id: "create-can-i", + instantiate: (di): CreateCanI => { + const logger = di.inject(loggerInjectable); + + return (api) => async (resourceAttributes: V1ResourceAttributes): Promise => { + try { + const { body } = await api.createSelfSubjectAccessReview({ + apiVersion: "authorization.k8s.io/v1", + kind: "SelfSubjectAccessReview", + spec: { resourceAttributes }, + }); + + return body.status?.allowed ?? false; + } catch (error) { + logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes }); + + return false; + } + }; + }, +}); + +export default createCanIInjectable; diff --git a/packages/core/src/common/cluster/create-core-api.injectable.ts b/packages/core/src/common/cluster/create-core-api.injectable.ts new file mode 100644 index 0000000000..2389b12478 --- /dev/null +++ b/packages/core/src/common/cluster/create-core-api.injectable.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { KubeConfig } from "@kubernetes/client-node"; +import { CoreV1Api } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; + +export type CreateCoreApi = (config: KubeConfig) => CoreV1Api; + +const createCoreApiInjectable = getInjectable({ + id: "create-core-api", + instantiate: (): CreateCoreApi => config => config.makeApiClient(CoreV1Api), +}); + +export default createCoreApiInjectable; diff --git a/packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts b/packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts new file mode 100644 index 0000000000..b3e8875dc1 --- /dev/null +++ b/packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AuthorizationV1Api } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; +import loggerInjectable from "../logger.injectable"; +import type { KubeApiResource } from "../rbac"; + +export type CanListResource = (resource: KubeApiResource) => boolean; + +/** + * Requests the permissions for actions on the kube cluster + * @param namespace The namespace of the resources + */ +export type RequestNamespaceListPermissions = (namespace: string) => Promise; + +export type CreateRequestNamespaceListPermissions = (api: AuthorizationV1Api) => RequestNamespaceListPermissions; + +const createRequestNamespaceListPermissionsInjectable = getInjectable({ + id: "create-request-namespace-list-permissions", + instantiate: (di): CreateRequestNamespaceListPermissions => { + const logger = di.inject(loggerInjectable); + + return (api) => async (namespace) => { + try { + const { body: { status }} = await api.createSelfSubjectRulesReview({ + apiVersion: "authorization.k8s.io/v1", + kind: "SelfSubjectRulesReview", + spec: { namespace }, + }); + + if (!status || status.incomplete) { + logger.warn(`[AUTHORIZATION-NAMESPACE-REVIEW]: allowing all resources in namespace="${namespace}" due to incomplete SelfSubjectRulesReview: ${status?.evaluationError}`); + + return () => true; + } + + const { resourceRules } = status; + + return (resource) => ( + resourceRules + .filter(({ apiGroups = ["*"] }) => apiGroups.includes("*") || apiGroups.includes(resource.group)) + .filter(({ resources = ["*"] }) => resources.includes("*") || resources.includes(resource.apiName)) + .some(({ verbs }) => verbs.includes("*") || verbs.includes("list")) + ); + } catch (error) { + logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review`, { namespace, error }); + + return () => true; + } + }; + }, +}); + +export default createRequestNamespaceListPermissionsInjectable; diff --git a/packages/core/src/common/cluster/current-cluster-channel.ts b/packages/core/src/common/cluster/current-cluster-channel.ts index 957baa6f9c..c16af7832b 100644 --- a/packages/core/src/common/cluster/current-cluster-channel.ts +++ b/packages/core/src/common/cluster/current-cluster-channel.ts @@ -4,7 +4,7 @@ */ import type { ClusterId } from "../cluster-types"; -import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; +import type { MessageChannel } from "@k8slens/messaging"; export const currentClusterMessageChannel: MessageChannel = { id: "current-visible-cluster", diff --git a/packages/core/src/common/cluster/list-namespaces.injectable.ts b/packages/core/src/common/cluster/list-namespaces.injectable.ts index 363a10abb1..04acb0671b 100644 --- a/packages/core/src/common/cluster/list-namespaces.injectable.ts +++ b/packages/core/src/common/cluster/list-namespaces.injectable.ts @@ -2,27 +2,21 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { KubeConfig } from "@kubernetes/client-node"; -import { CoreV1Api } from "@kubernetes/client-node"; +import type { CoreV1Api } from "@kubernetes/client-node"; import { getInjectable } from "@ogre-tools/injectable"; import { isDefined } from "@k8slens/utilities"; export type ListNamespaces = () => Promise; - -export type CreateListNamespaces = (config: KubeConfig) => ListNamespaces; +export type CreateListNamespaces = (api: CoreV1Api) => ListNamespaces; const createListNamespacesInjectable = getInjectable({ id: "create-list-namespaces", - instantiate: (): CreateListNamespaces => (config) => { - const coreApi = config.makeApiClient(CoreV1Api); + instantiate: (): CreateListNamespaces => (api) => async () => { + const { body: { items }} = await api.listNamespace(); - return async () => { - const { body: { items }} = await coreApi.listNamespace(); - - return items - .map(ns => ns.metadata?.name) - .filter(isDefined); - }; + return items + .map(ns => ns.metadata?.name) + .filter(isDefined); }, }); diff --git a/packages/core/src/common/cluster/request-namespace-list-permissions.injectable.ts b/packages/core/src/common/cluster/request-namespace-list-permissions.injectable.ts deleted file mode 100644 index 4b1aadeee6..0000000000 --- a/packages/core/src/common/cluster/request-namespace-list-permissions.injectable.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { KubeConfig } from "@kubernetes/client-node"; -import { AuthorizationV1Api } from "@kubernetes/client-node"; -import { getInjectable } from "@ogre-tools/injectable"; -import loggerInjectable from "../logger.injectable"; -import type { KubeApiResource } from "../rbac"; - -export type CanListResource = (resource: KubeApiResource) => boolean; - -/** - * Requests the permissions for actions on the kube cluster - * @param namespace The namespace of the resources - */ -export type RequestNamespaceListPermissions = (namespace: string) => Promise; - -/** - * @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster - */ -export type RequestNamespaceListPermissionsFor = (proxyConfig: KubeConfig) => RequestNamespaceListPermissions; - -const requestNamespaceListPermissionsForInjectable = getInjectable({ - id: "request-namespace-list-permissions-for", - instantiate: (di): RequestNamespaceListPermissionsFor => { - const logger = di.inject(loggerInjectable); - - return (proxyConfig) => { - const api = proxyConfig.makeApiClient(AuthorizationV1Api); - - return async (namespace) => { - try { - const { body: { status }} = await api.createSelfSubjectRulesReview({ - apiVersion: "authorization.k8s.io/v1", - kind: "SelfSubjectRulesReview", - spec: { namespace }, - }); - - if (!status || status.incomplete) { - logger.warn(`[AUTHORIZATION-NAMESPACE-REVIEW]: allowing all resources in namespace="${namespace}" due to incomplete SelfSubjectRulesReview: ${status?.evaluationError}`); - - return () => true; - } - - const { resourceRules } = status; - - return (resource) => { - const rules = resourceRules.filter(({ - apiGroups = ["*"], - resources = ["*"], - }) => { - const isAboutRelevantApiGroup = apiGroups.includes("*") || apiGroups.includes(resource.group); - const isAboutResource = resources.includes("*") || resources.includes(resource.apiName); - - return isAboutRelevantApiGroup && isAboutResource; - }); - - return rules.some(({ verbs }) => verbs.includes("*") || verbs.includes("list")); - }; - } catch (error) { - logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review`, { namespace, error }); - - return () => true; - } - }; - }; - }, -}); - -export default requestNamespaceListPermissionsForInjectable; diff --git a/packages/core/src/common/cluster/request-namespace-list-permissions.test.ts b/packages/core/src/common/cluster/request-namespace-list-permissions.test.ts index c62f69ca8e..194b5ce7e5 100644 --- a/packages/core/src/common/cluster/request-namespace-list-permissions.test.ts +++ b/packages/core/src/common/cluster/request-namespace-list-permissions.test.ts @@ -3,334 +3,225 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { V1SubjectRulesReviewStatus } from "@kubernetes/client-node"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { AuthorizationV1Api, V1SubjectRulesReviewStatus } from "@kubernetes/client-node"; import type { DiContainer } from "@ogre-tools/injectable"; +import type { IncomingMessage } from "http"; +import { anyObject } from "jest-mock-extended"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; -import type { RequestNamespaceListPermissionsFor } from "./request-namespace-list-permissions.injectable"; -import requestNamespaceListPermissionsForInjectable from "./request-namespace-list-permissions.injectable"; +import { cast } from "../../test-utils/cast"; +import type { KubeApiResource } from "../rbac"; +import type { RequestNamespaceListPermissions } from "./create-request-namespace-list-permissions.injectable"; +import createRequestNamespaceListPermissionsInjectable from "./create-request-namespace-list-permissions.injectable"; -const createStubProxyConfig = (statusResponse: Promise<{ body: { status: V1SubjectRulesReviewStatus }}>) => ({ - makeApiClient: () => ({ - createSelfSubjectRulesReview: (): Promise<{ body: { status: V1SubjectRulesReviewStatus }}> => statusResponse, - }), -}); +interface TestCase { + description: string; + status: V1SubjectRulesReviewStatus; + expected: boolean; +} describe("requestNamespaceListPermissions", () => { let di: DiContainer; - let requestNamespaceListPermissions: RequestNamespaceListPermissionsFor; + let createSelfSubjectRulesReviewMock: AsyncFnMock; + let requestNamespaceListPermissions: RequestNamespaceListPermissions; beforeEach(() => { di = getDiForUnitTesting(); - requestNamespaceListPermissions = di.inject(requestNamespaceListPermissionsForInjectable); + + const createRequestNamespaceListPermissions = di.inject(createRequestNamespaceListPermissionsInjectable); + + createSelfSubjectRulesReviewMock = asyncFn(); + + requestNamespaceListPermissions = createRequestNamespaceListPermissions(cast({ + createSelfSubjectRulesReview: createSelfSubjectRulesReviewMock, + })); }); - describe("when api returns incomplete data", () => { - it("returns truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: true, - resourceRules: [], - nonResourceRules: [], - }, - }, - })), - ) as any); + describe("when a request for list permissions in a namespace has been started", () => { + let request: ReturnType; - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); + beforeEach(() => { + request = requestNamespaceListPermissions("irrelevant-namespace"); }); - }); - describe("when api rejects", () => { - it("returns truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve, reject) => reject("unknown error")), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); + it("should request the creation of a SelfSubjectRulesReview", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "irrelevant-namespace", + }, + })); }); - }); - describe("when first resourceRule has all permissions for everything", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["*"], - }, - { - apiGroups: ["*"], - verbs: ["get"], - }, - ], - nonResourceRules: [], + ([ + { + description: "incomplete data", + status: { + incomplete: true, + resourceRules: [], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "first resourceRule has all permissions for everything", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["*"], + verbs: ["*"], }, - }, - })), - ) as any); + { + apiGroups: ["*"], + verbs: ["get"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "first resourceRule has list permissions for everything", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["*"], + verbs: ["list"], + }, + { + apiGroups: ["*"], + verbs: ["get"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "first resourceRule has list permissions for asked resource", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["some-api-group"], + resources: ["some-kind"], + verbs: ["list"], + }, + { + apiGroups: ["*"], + verbs: ["get"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "last resourceRule has all permissions for everything", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["*"], + verbs: ["get"], + }, + { + apiGroups: ["*"], + verbs: ["*"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "last resourceRule has list permissions for asked resource", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["*"], + verbs: ["get"], + }, + { + apiGroups: ["some-api-group"], + resources: ["some-kind"], + verbs: ["list"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "resourceRules has matching resource without list verb", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["some-api-group"], + resources: ["some-kind"], + verbs: ["get"], + }, + ], + nonResourceRules: [], + }, + expected: false, + }, + { + description: "resourceRules has no matching resource with list verb", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: [""], + resources: ["services"], + verbs: ["list"], + }, + ], + nonResourceRules: [], + }, + expected: false, + }, + ] as TestCase[]).forEach(({ description, status, expected }) => { + describe(`when api returns ${description}`, () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve({ + body: { + status, + spec: {}, + }, + response: null as unknown as IncomingMessage, + }); + }); - const permissionCheck = await requestPermissions("irrelevant-namespace"); + it(`allows the request to complete, and 'canListResource' will return ${expected}`, async () => { + const canListResource = await request; - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); + expect(canListResource(someKubeResource)).toBe(expected); + }); + }); }); - }); - describe("when first resourceRule has list permissions for everything", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["list"], - }, - { - apiGroups: ["*"], - verbs: ["get"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); + describe("when api rejects", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.reject(new Error("unknown error")); + }); - const permissionCheck = await requestPermissions("irrelevant-namespace"); + it("allows the request to complete, and 'canListResource' will return true", async () => { + const canListResource = await request; - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when first resourceRule has list permissions for asked resource", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: [""], - resources: ["pods"], - verbs: ["list"], - }, - { - apiGroups: ["*"], - verbs: ["get"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when last resourceRule has all permissions for everything", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["get"], - }, - { - apiGroups: ["*"], - verbs: ["*"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when last resourceRule has list permissions for everything", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["get"], - }, - { - apiGroups: ["*"], - verbs: ["list"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when last resourceRule has list permissions for asked resource", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["get"], - }, - { - apiGroups: [""], - resources: ["pods"], - verbs: ["list"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when resourceRules has matching resource without list verb", () => { - it("return falsy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: [""], - resources: ["pods"], - verbs: ["get"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeFalsy(); - }); - }); - - describe("when resourceRules has no matching resource with list verb", () => { - it("return falsy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: [""], - resources: ["services"], - verbs: ["list"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeFalsy(); + expect(canListResource(someKubeResource)).toBe(true); + }); }); }); }); + +const someKubeResource: KubeApiResource = { + apiName: "some-kind", + group: "some-api-group", + kind: "SomeKind", + namespaced: true, +}; diff --git a/packages/core/src/common/cluster/visibility-channel.ts b/packages/core/src/common/cluster/visibility-channel.ts index 8a1a297ff2..554217a409 100644 --- a/packages/core/src/common/cluster/visibility-channel.ts +++ b/packages/core/src/common/cluster/visibility-channel.ts @@ -4,7 +4,7 @@ */ import type { ClusterId } from "../cluster-types"; -import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; +import type { MessageChannel } from "@k8slens/messaging"; export const clusterVisibilityChannel: MessageChannel = { id: "cluster-visibility", diff --git a/packages/core/src/common/error-reporting/initialize-sentry-reporting.injectable.ts b/packages/core/src/common/error-reporting/initialize-sentry-reporting.injectable.ts index 778f959739..677c18a586 100644 --- a/packages/core/src/common/error-reporting/initialize-sentry-reporting.injectable.ts +++ b/packages/core/src/common/error-reporting/initialize-sentry-reporting.injectable.ts @@ -13,7 +13,7 @@ import userStoreInjectable from "../user-store/user-store.injectable"; export type InitializeSentryReportingWith = (initSentry: (opts: BrowserOptions | ElectronMainOptions) => void) => void; -const mapProcessName = (type: "browser" | "renderer" | "worker") => type === "browser" ? "main" : type; +const mapProcessName = (type: "browser" | "renderer" | "worker" | "utility") => type === "browser" ? "main" : type; const initializeSentryReportingWithInjectable = getInjectable({ id: "initialize-sentry-reporting-with", diff --git a/packages/core/src/common/fetch/timeout-controller.ts b/packages/core/src/common/fetch/timeout-controller.ts index 702becdc9d..c4688218db 100644 --- a/packages/core/src/common/fetch/timeout-controller.ts +++ b/packages/core/src/common/fetch/timeout-controller.ts @@ -3,13 +3,15 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +import { formatDuration } from "@k8slens/utilities"; + /** * Creates an AbortController with an associated timeout * @param timeout The number of milliseconds before this controller will auto abort */ export function withTimeout(timeout: number): AbortController { const controller = new AbortController(); - const id = setTimeout(() => controller.abort(), timeout); + const id = setTimeout(() => controller.abort(`Operation timed out: timeout ${formatDuration(timeout)}`), timeout); controller.signal.addEventListener("abort", () => clearTimeout(id)); diff --git a/packages/core/src/common/front-end-routing/app-navigation-channel.ts b/packages/core/src/common/front-end-routing/app-navigation-channel.ts index b3d8f02c66..8670fc4953 100644 --- a/packages/core/src/common/front-end-routing/app-navigation-channel.ts +++ b/packages/core/src/common/front-end-routing/app-navigation-channel.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { IpcRendererNavigationEvents } from "../ipc/navigation-events"; -import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; +import type { MessageChannel } from "@k8slens/messaging"; export type AppNavigationChannel = MessageChannel; diff --git a/packages/core/src/common/front-end-routing/cluster-frame-navigation-channel.ts b/packages/core/src/common/front-end-routing/cluster-frame-navigation-channel.ts index 2439f523e2..5f96526da4 100644 --- a/packages/core/src/common/front-end-routing/cluster-frame-navigation-channel.ts +++ b/packages/core/src/common/front-end-routing/cluster-frame-navigation-channel.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { IpcRendererNavigationEvents } from "../ipc/navigation-events"; -import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; +import type { MessageChannel } from "@k8slens/messaging"; export type ClusterFrameNavigationChannel = MessageChannel; diff --git a/packages/core/src/common/fs/copy.global-override-for-injectable.ts b/packages/core/src/common/fs/copy.global-override-for-injectable.ts deleted file mode 100644 index 3799d3b760..0000000000 --- a/packages/core/src/common/fs/copy.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import copyInjectable from "./copy.injectable"; - -export default getGlobalOverride(copyInjectable, () => async () => { - throw new Error("tried to copy filepaths without override"); -}); diff --git a/packages/core/src/common/fs/lstat.global-override-for-injectable.ts b/packages/core/src/common/fs/lstat.global-override-for-injectable.ts deleted file mode 100644 index 155fac7451..0000000000 --- a/packages/core/src/common/fs/lstat.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import lstatInjectable from "./lstat.injectable"; - -export default getGlobalOverride(lstatInjectable, () => async () => { - throw new Error("tried to lstat a filepath without override"); -}); diff --git a/packages/core/src/common/fs/read-directory.global-override-for-injectable.ts b/packages/core/src/common/fs/read-directory.global-override-for-injectable.ts deleted file mode 100644 index 72d9b523f4..0000000000 --- a/packages/core/src/common/fs/read-directory.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import readDirectoryInjectable from "./read-directory.injectable"; - -export default getGlobalOverride(readDirectoryInjectable, () => async () => { - throw new Error("tried to read a directory's content without override"); -}); diff --git a/packages/core/src/common/fs/remove.global-override-for-injectable.ts b/packages/core/src/common/fs/remove.global-override-for-injectable.ts deleted file mode 100644 index 58fb0f9dce..0000000000 --- a/packages/core/src/common/fs/remove.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import removePathInjectable from "./remove.injectable"; - -export default getGlobalOverride(removePathInjectable, () => async () => { - throw new Error("tried to remove path without override"); -}); diff --git a/packages/core/src/common/fs/write-file.global-override-for-injectable.ts b/packages/core/src/common/fs/write-file.global-override-for-injectable.ts deleted file mode 100644 index e87f648305..0000000000 --- a/packages/core/src/common/fs/write-file.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import writeFileInjectable from "./write-file.injectable"; - -export default getGlobalOverride(writeFileInjectable, () => async () => { - throw new Error("tried to write file without override"); -}); diff --git a/packages/core/src/common/helm/add-helm-repository-channel.ts b/packages/core/src/common/helm/add-helm-repository-channel.ts index 258367c7ec..1744192059 100644 --- a/packages/core/src/common/helm/add-helm-repository-channel.ts +++ b/packages/core/src/common/helm/add-helm-repository-channel.ts @@ -3,11 +3,10 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import type { HelmRepo } from "./helm-repo"; -import type { AsyncResult } from "@k8slens/utilities"; -import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; +import type { Result } from "@k8slens/utilities"; +import { getRequestChannel } from "@k8slens/messaging"; -export type AddHelmRepositoryChannel = RequestChannel>; - -export const addHelmRepositoryChannel: AddHelmRepositoryChannel = { - id: "add-helm-repository-channel", -}; +export const addHelmRepositoryChannel = getRequestChannel< + HelmRepo, + Result +>("add-helm-repository-channel"); diff --git a/packages/core/src/common/helm/get-active-helm-repositories-channel.ts b/packages/core/src/common/helm/get-active-helm-repositories-channel.ts index 2ea5a80030..ba4e0ae956 100644 --- a/packages/core/src/common/helm/get-active-helm-repositories-channel.ts +++ b/packages/core/src/common/helm/get-active-helm-repositories-channel.ts @@ -4,10 +4,9 @@ */ import type { HelmRepo } from "./helm-repo"; import type { AsyncResult } from "@k8slens/utilities"; -import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; +import { getRequestChannel } from "@k8slens/messaging"; -export type GetActiveHelmRepositoriesChannel = RequestChannel>; - -export const getActiveHelmRepositoriesChannel: GetActiveHelmRepositoriesChannel = { - id: "get-helm-active-list-repositories", -}; +export const getActiveHelmRepositoriesChannel = getRequestChannel< + void, + AsyncResult +>("get-helm-active-list-repositories"); diff --git a/packages/core/src/common/helm/remove-helm-repository-channel.ts b/packages/core/src/common/helm/remove-helm-repository-channel.ts index f1189cb580..09c7630f34 100644 --- a/packages/core/src/common/helm/remove-helm-repository-channel.ts +++ b/packages/core/src/common/helm/remove-helm-repository-channel.ts @@ -3,11 +3,10 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import type { AsyncResult } from "@k8slens/utilities"; -import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; +import { getRequestChannel } from "@k8slens/messaging"; import type { HelmRepo } from "./helm-repo"; -export type RemoveHelmRepositoryChannel = RequestChannel>; - -export const removeHelmRepositoryChannel: RemoveHelmRepositoryChannel = { - id: "remove-helm-repository-channel", -}; +export const removeHelmRepositoryChannel = getRequestChannel< + HelmRepo, + AsyncResult +>("remove-helm-repository-channel"); diff --git a/packages/core/src/common/hotbars/store.injectable.ts b/packages/core/src/common/hotbars/store.injectable.ts index cc15f93bf8..ea25840e11 100644 --- a/packages/core/src/common/hotbars/store.injectable.ts +++ b/packages/core/src/common/hotbars/store.injectable.ts @@ -14,7 +14,7 @@ import { hotbarStoreMigrationInjectionToken } from "./migrations-token"; import getBasenameOfPathInjectable from "../path/get-basename.injectable"; import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel-prefix"; import { persistStateToConfigInjectionToken } from "../base-store/save-to-file"; -import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token"; +import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync"; const hotbarStoreInjectable = getInjectable({ diff --git a/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts b/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts index 6ea6327038..48fdb3e544 100644 --- a/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts +++ b/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts @@ -4,6 +4,7 @@ */ import type { DiContainer } from "@ogre-tools/injectable"; +import { getInjectable } from "@ogre-tools/injectable"; import clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable"; import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable"; import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting"; @@ -21,6 +22,9 @@ import maybeKubeApiInjectable from "../maybe-kube-api.injectable"; // eslint-disable-next-line no-restricted-imports import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api"; import { Cluster } from "../../cluster/cluster"; +import { runInAction } from "mobx"; +import { customResourceDefinitionApiInjectionToken } from "../api-manager/crd-api-token"; +import assert from "assert"; class TestApi extends KubeApi { protected async checkPreferredVersion() { @@ -117,4 +121,90 @@ describe("ApiManager", () => { }); }); }); + + describe("given than a CRD has a default KubeApi registered for it", () => { + const apiBase = "/apis/aquasecurity.github.io/v1alpha1/vulnerabilityreports"; + + beforeEach(() => { + runInAction(() => { + di.register(getInjectable({ + id: `default-kube-api-for-custom-resource-definition-${apiBase}`, + instantiate: (di) => { + const objectConstructor = class extends KubeObject { + static readonly kind = "VulnerabilityReport"; + static readonly namespaced = true; + static readonly apiBase = apiBase; + }; + + return Object.assign( + new KubeApi({ + logger: di.inject(loggerInjectable), + maybeKubeApi: di.inject(maybeKubeApiInjectable), + }, { objectConstructor }), + { + myField: 1, + }, + ); + }, + injectionToken: customResourceDefinitionApiInjectionToken, + })); + }); + }); + + it("can be retrieved from apiManager", () => { + expect(apiManager.getApi(apiBase)).toMatchObject({ + myField: 1, + }); + }); + + it("can have a default KubeObjectStore instance retrieved for it", () => { + expect(apiManager.getStore(apiBase)).toBeInstanceOf(KubeObjectStore); + }); + + describe("given that an extension registers an api with the same apibase", () => { + beforeEach(() => { + void Object.assign(new ExternalKubeApi({ + objectConstructor: KubeObject, + apiBase, + kind: "VulnerabilityReport", + }), { + myField: 2, + }); + }); + + it("the extension's instance is retrievable instead from apiManager", () => { + expect(apiManager.getApi(apiBase)).toMatchObject({ + myField: 2, + }); + }); + + it("can have a default KubeObjectStore instance retrieved for it", () => { + expect(apiManager.getStore(apiBase)).toBeInstanceOf(KubeObjectStore); + }); + + describe("given that an extension registers a store for the same apibase", () => { + beforeEach(() => { + const api = apiManager.getApi(apiBase); + + assert(api); + + apiManager.registerStore(Object.assign( + new KubeObjectStore({ + context: di.inject(clusterFrameContextForNamespacedResourcesInjectable), + logger: di.inject(loggerInjectable), + }, api), + { + someField: 2, + }, + )); + }); + + it("can gets the custom KubeObjectStore instance instead", () => { + expect(apiManager.getStore(apiBase)).toMatchObject({ + someField: 2, + }); + }); + }); + }); + }); }); diff --git a/packages/core/src/common/k8s-api/__tests__/kube-api.test.ts b/packages/core/src/common/k8s-api/__tests__/kube-api.test.ts index fa5b024bd2..e7240895cf 100644 --- a/packages/core/src/common/k8s-api/__tests__/kube-api.test.ts +++ b/packages/core/src/common/k8s-api/__tests__/kube-api.test.ts @@ -22,7 +22,6 @@ import { flushPromises } from "@k8slens/test-utils"; import createKubeJsonApiInjectable from "../create-kube-json-api.injectable"; import type { IKubeWatchEvent } from "../kube-watch-event"; import type { KubeJsonApiDataFor, KubeStatusData } from "../kube-object"; -import AbortController from "abort-controller"; import setupAutoRegistrationInjectable from "../../../renderer/before-frame-starts/runnables/setup-auto-registration.injectable"; import { diff --git a/packages/core/src/common/k8s-api/api-manager/api-manager.ts b/packages/core/src/common/k8s-api/api-manager/api-manager.ts index f6c2758921..6b60ba8e7a 100644 --- a/packages/core/src/common/k8s-api/api-manager/api-manager.ts +++ b/packages/core/src/common/k8s-api/api-manager/api-manager.ts @@ -10,7 +10,8 @@ import { autorun, action, observable } from "mobx"; import type { KubeApi } from "../kube-api"; import type { KubeObject, ObjectReference } from "../kube-object"; import { parseKubeApi, createKubeApiURL } from "../kube-api-parse"; -import { iter } from "@k8slens/utilities"; +import { getOrInsertWith, iter } from "@k8slens/utilities"; +import type { CreateCustomResourceStore } from "./create-custom-resource-store.injectable"; export type RegisterableStore = Store extends KubeObjectStore ? Store @@ -26,13 +27,15 @@ export type FindApiCallback = (api: KubeApi) => boolean; interface Dependencies { readonly apis: IComputedValue; + readonly crdApis: IComputedValue; readonly stores: IComputedValue; + createCustomResourceStore: CreateCustomResourceStore; } export class ApiManager { private readonly externalApis = observable.array(); private readonly externalStores = observable.array(); - + private readonly defaultCrdStores = observable.map(); private readonly apis = observable.map(); constructor(private readonly dependencies: Dependencies) { @@ -56,6 +59,12 @@ export class ApiManager { } } + for (const crdApi of this.dependencies.crdApis.get()) { + if (!newState.has(crdApi.apiBase)) { + newState.set(crdApi.apiBase, crdApi); + } + } + this.apis.replace(newState); }); } @@ -110,6 +119,16 @@ export class ApiManager { this.externalStores.push(store); } + private apiIsDefaultCrdApi(api: KubeApi): boolean { + for (const crdApi of this.dependencies.crdApis.get()) { + if (crdApi.apiBase === api.apiBase) { + return true; + } + } + + return false; + } + getStore(api: string | undefined): KubeObjectStore | undefined; getStore(api: RegisterableApi): KubeObjectStoreFrom | undefined; /** @@ -130,9 +149,19 @@ export class ApiManager { return undefined; } - return iter.chain(this.dependencies.stores.get().values()) + const defaultResult = iter.chain(this.dependencies.stores.get().values()) .concat(this.externalStores.values()) .find(store => store.api.apiBase === api.apiBase); + + if (defaultResult) { + return defaultResult; + } + + if (this.apiIsDefaultCrdApi(api)) { + return getOrInsertWith(this.defaultCrdStores, api.apiBase, () => this.dependencies.createCustomResourceStore(api)); + } + + return undefined; } lookupApiLink(ref: ObjectReference, parentObject?: KubeObject): string { diff --git a/packages/core/src/common/k8s-api/api-manager/auto-registration-emitter.injectable.ts b/packages/core/src/common/k8s-api/api-manager/auto-registration-emitter.injectable.ts index d9a68a988c..714e9d8952 100644 --- a/packages/core/src/common/k8s-api/api-manager/auto-registration-emitter.injectable.ts +++ b/packages/core/src/common/k8s-api/api-manager/auto-registration-emitter.injectable.ts @@ -5,11 +5,9 @@ import { getInjectable } from "@ogre-tools/injectable"; import EventEmitter from "events"; import type TypedEventEmitter from "typed-emitter"; -import type { CustomResourceDefinition } from "../endpoints"; import type { KubeApi } from "../kube-api"; export interface LegacyAutoRegistration { - customResourceDefinition: (crd: CustomResourceDefinition) => void; kubeApi: (api: KubeApi) => void; } diff --git a/packages/core/src/common/k8s-api/api-manager/crd-api-token.ts b/packages/core/src/common/k8s-api/api-manager/crd-api-token.ts new file mode 100644 index 0000000000..d8f0c918c2 --- /dev/null +++ b/packages/core/src/common/k8s-api/api-manager/crd-api-token.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { KubeApi } from "../kube-api"; + +export const customResourceDefinitionApiInjectionToken = getInjectionToken({ + id: "custom-resource-definition-api-token", +}); diff --git a/packages/core/src/common/k8s-api/api-manager/create-custom-resource-store.injectable.ts b/packages/core/src/common/k8s-api/api-manager/create-custom-resource-store.injectable.ts new file mode 100644 index 0000000000..fbf46e2b42 --- /dev/null +++ b/packages/core/src/common/k8s-api/api-manager/create-custom-resource-store.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable"; +import loggerInjectable from "../../logger.injectable"; +import type { KubeApi } from "../kube-api"; +import type { KubeObject } from "../kube-object"; +import type { KubeObjectStoreDependencies } from "../kube-object.store"; +import { CustomResourceStore } from "./resource.store"; + +export type CreateCustomResourceStore = (api: KubeApi) => CustomResourceStore; + +const createCustomResourceStoreInjectable = getInjectable({ + id: "create-custom-resource-store", + instantiate: (di): CreateCustomResourceStore => { + const deps: KubeObjectStoreDependencies = { + context: di.inject(clusterFrameContextForNamespacedResourcesInjectable), + logger: di.inject(loggerInjectable), + }; + + return (api) => new CustomResourceStore(deps, api); + }, +}); + +export default createCustomResourceStoreInjectable; diff --git a/packages/core/src/common/k8s-api/api-manager/manager.injectable.ts b/packages/core/src/common/k8s-api/api-manager/manager.injectable.ts index f0b61c28b6..2ffd41a296 100644 --- a/packages/core/src/common/k8s-api/api-manager/manager.injectable.ts +++ b/packages/core/src/common/k8s-api/api-manager/manager.injectable.ts @@ -9,6 +9,8 @@ import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-f import { kubeObjectStoreInjectionToken } from "./kube-object-store-token"; import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token"; import { computed } from "mobx"; +import { customResourceDefinitionApiInjectionToken } from "./crd-api-token"; +import createCustomResourceStoreInjectable from "./create-custom-resource-store.injectable"; const apiManagerInjectable = getInjectable({ id: "api-manager", @@ -23,6 +25,10 @@ const apiManagerInjectable = getInjectable({ stores: storesAndApisCanBeCreated ? computedInjectMany(kubeObjectStoreInjectionToken) : computed(() => []), + crdApis: storesAndApisCanBeCreated + ? computedInjectMany(customResourceDefinitionApiInjectionToken) + : computed(() => []), + createCustomResourceStore: di.inject(createCustomResourceStoreInjectable), }); }, }); diff --git a/packages/core/src/common/k8s-api/kube-api.ts b/packages/core/src/common/k8s-api/kube-api.ts index 94fc24be7d..c5ed03abad 100644 --- a/packages/core/src/common/k8s-api/kube-api.ts +++ b/packages/core/src/common/k8s-api/kube-api.ts @@ -20,7 +20,6 @@ import type { Patch } from "rfc6902"; import assert from "assert"; import type { PartialDeep } from "type-fest"; import type { Logger } from "../logger"; -import type AbortController from "abort-controller"; import { matches } from "lodash/fp"; import { makeObservable, observable } from "mobx"; diff --git a/packages/core/src/common/k8s-api/kube-object.store.ts b/packages/core/src/common/k8s-api/kube-object.store.ts index 408794e44b..9effdc8f57 100644 --- a/packages/core/src/common/k8s-api/kube-object.store.ts +++ b/packages/core/src/common/k8s-api/kube-object.store.ts @@ -17,7 +17,6 @@ import type { Patch } from "rfc6902"; import type { Logger } from "../logger"; import assert from "assert"; import type { PartialDeep } from "type-fest"; -import AbortController from "abort-controller"; import type { ClusterContext } from "../../renderer/cluster-frame-context/cluster-frame-context"; import autoBind from "auto-bind"; @@ -89,7 +88,7 @@ export interface KubeObjectStoreDependencies { readonly logger: Logger; } -export abstract class KubeObjectStore< +export class KubeObjectStore< K extends KubeObject = KubeObject, A extends KubeApi = KubeApi>, D extends KubeJsonApiDataFor = KubeApiDataFrom, diff --git a/packages/core/src/common/kube-helpers/channels.ts b/packages/core/src/common/kube-helpers/channels.ts index b48f9f1f99..c5df4d7358 100644 --- a/packages/core/src/common/kube-helpers/channels.ts +++ b/packages/core/src/common/kube-helpers/channels.ts @@ -4,11 +4,9 @@ */ import { getInjectionToken } from "@ogre-tools/injectable"; -import type { Asyncify } from "type-fest"; -import type { RequestChannelHandler } from "../../main/utils/channel/channel-listeners/listener-tokens"; import type { ClusterId } from "../cluster-types"; -import type { AsyncResult } from "@k8slens/utilities"; -import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; +import type { AsyncResult, Result } from "@k8slens/utilities"; +import { getRequestChannel } from "@k8slens/messaging"; export interface KubectlApplyAllArgs { clusterId: ClusterId; @@ -16,11 +14,12 @@ export interface KubectlApplyAllArgs { extraArgs: string[]; } -export const kubectlApplyAllChannel: RequestChannel> = { - id: "kubectl-apply-all", -}; +export const kubectlApplyAllChannel = getRequestChannel< + KubectlApplyAllArgs, + Result +>("kubectl-apply-all"); -export type KubectlApplyAll = Asyncify>; +export type KubectlApplyAll = (req: KubectlApplyAllArgs) => AsyncResult; export const kubectlApplyAllInjectionToken = getInjectionToken({ id: "kubectl-apply-all", @@ -32,11 +31,12 @@ export interface KubectlDeleteAllArgs { extraArgs: string[]; } -export const kubectlDeleteAllChannel: RequestChannel> = { - id: "kubectl-delete-all", -}; +export const kubectlDeleteAllChannel = getRequestChannel< + KubectlDeleteAllArgs, + Result +>("kubectl-delete-all"); -export type KubectlDeleteAll = Asyncify>; +export type KubectlDeleteAll = (req: KubectlDeleteAllArgs) => AsyncResult; export const kubectlDeleteAllInjectionToken = getInjectionToken({ id: "kubectl-delete-all", diff --git a/packages/core/src/common/root-frame/root-frame-rendered-channel.ts b/packages/core/src/common/root-frame/root-frame-rendered-channel.ts index 060ae8735c..c4dc0cefa6 100644 --- a/packages/core/src/common/root-frame/root-frame-rendered-channel.ts +++ b/packages/core/src/common/root-frame/root-frame-rendered-channel.ts @@ -2,7 +2,7 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; +import type { MessageChannel } from "@k8slens/messaging"; export type RootFrameHasRenderedChannel = MessageChannel; diff --git a/packages/core/src/common/user-store/user-store.injectable.ts b/packages/core/src/common/user-store/user-store.injectable.ts index 3b45b03b1d..10806007a2 100644 --- a/packages/core/src/common/user-store/user-store.injectable.ts +++ b/packages/core/src/common/user-store/user-store.injectable.ts @@ -16,7 +16,7 @@ import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync"; import { persistStateToConfigInjectionToken } from "../base-store/save-to-file"; import getBasenameOfPathInjectable from "../path/get-basename.injectable"; -import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token"; +import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; import userStorePreferenceDescriptorsInjectable from "./preference-descriptors.injectable"; const userStoreInjectable = getInjectable({ diff --git a/packages/core/src/common/utils/channel/channel.test.ts b/packages/core/src/common/utils/channel/channel.test.ts deleted file mode 100644 index bc6043d349..0000000000 --- a/packages/core/src/common/utils/channel/channel.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { DiContainer } from "@ogre-tools/injectable"; -import { getInjectable } from "@ogre-tools/injectable"; -import type { SendMessageToChannel } from "./message-to-channel-injection-token"; -import { sendMessageToChannelInjectionToken } from "./message-to-channel-injection-token"; -import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; -import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; -import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable"; -import type { MessageChannel } from "./message-channel-listener-injection-token"; -import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token"; -import type { RequestFromChannel } from "./request-from-channel-injection-token"; -import { requestFromChannelInjectionToken } from "./request-from-channel-injection-token"; -import type { RequestChannel } from "./request-channel-listener-injection-token"; -import type { AsyncFnMock } from "@async-fn/jest"; -import asyncFn from "@async-fn/jest"; -import { getPromiseStatus } from "@k8slens/test-utils"; -import { runInAction } from "mobx"; -import type { RequestChannelHandler } from "../../../main/utils/channel/channel-listeners/listener-tokens"; -import { - getRequestChannelListenerInjectable, - requestChannelListenerInjectionToken, -} from "../../../main/utils/channel/channel-listeners/listener-tokens"; - -type TestMessageChannel = MessageChannel; -type TestRequestChannel = RequestChannel; - -describe("channel", () => { - describe("messaging from main to renderer, given listener for channel in a window and application has started", () => { - let messageListenerInWindowMock: jest.Mock; - let mainDi: DiContainer; - let messageToChannel: SendMessageToChannel; - let builder: ApplicationBuilder; - - beforeEach(async () => { - builder = getApplicationBuilder(); - - messageListenerInWindowMock = jest.fn(); - - const testChannelListenerInTestWindowInjectable = getInjectable({ - id: "test-channel-listener-in-test-window", - - instantiate: () => ({ - channel: testMessageChannel, - handler: messageListenerInWindowMock, - }), - - injectionToken: messageChannelListenerInjectionToken, - }); - - builder.beforeWindowStart(({ windowDi }) => { - runInAction(() => { - windowDi.register(testChannelListenerInTestWindowInjectable); - }); - }); - - mainDi = builder.mainDi; - - await builder.startHidden(); - - messageToChannel = mainDi.inject(sendMessageToChannelInjectionToken); - }); - - describe("given window is started", () => { - let someWindowFake: LensWindow; - - beforeEach(async () => { - someWindowFake = builder.applicationWindow.create("some-window"); - - await someWindowFake.start(); - }); - - it("when sending message, triggers listener in window", () => { - messageToChannel(testMessageChannel, "some-message"); - - expect(messageListenerInWindowMock).toHaveBeenCalledWith("some-message"); - }); - - it("given window is hidden, when sending message, does not trigger listener in window", () => { - someWindowFake.close(); - - messageToChannel(testMessageChannel, "some-message"); - - expect(messageListenerInWindowMock).not.toHaveBeenCalled(); - }); - }); - - it("given multiple started windows, when sending message, triggers listeners in all windows", async () => { - const someWindowFake = builder.applicationWindow.create("some-window"); - const someOtherWindowFake = builder.applicationWindow.create("some-other-window"); - - await someWindowFake.start(); - await someOtherWindowFake.start(); - - messageToChannel(testMessageChannel, "some-message"); - - expect(messageListenerInWindowMock.mock.calls).toEqual([ - ["some-message"], - ["some-message"], - ]); - }); - }); - - describe("messaging from renderer to main, given listener for channel in a main and application has started", () => { - let messageListenerInMainMock: jest.Mock; - let messageToChannel: SendMessageToChannel; - - beforeEach(async () => { - const applicationBuilder = getApplicationBuilder(); - - messageListenerInMainMock = jest.fn(); - - const testChannelListenerInMainInjectable = getInjectable({ - id: "test-channel-listener-in-main", - - instantiate: () => ({ - channel: testMessageChannel, - handler: messageListenerInMainMock, - }), - - injectionToken: messageChannelListenerInjectionToken, - }); - - applicationBuilder.beforeApplicationStart(({ mainDi }) => { - runInAction(() => { - mainDi.register(testChannelListenerInMainInjectable); - }); - }); - - await applicationBuilder.render(); - - const windowDi = applicationBuilder.applicationWindow.only.di; - - messageToChannel = windowDi.inject(sendMessageToChannelInjectionToken); - }); - - it("when sending message, triggers listener in main", () => { - messageToChannel(testMessageChannel, "some-message"); - - expect(messageListenerInMainMock).toHaveBeenCalledWith("some-message"); - }); - }); - - describe("requesting from main in renderer, given listener for channel in a main and application has started", () => { - let requestListenerInMainMock: AsyncFnMock>; - let requestFromChannel: RequestFromChannel; - - beforeEach(async () => { - const applicationBuilder = getApplicationBuilder(); - - requestListenerInMainMock = asyncFn(); - - const testChannelListenerInMainInjectable = getRequestChannelListenerInjectable({ - channel: testRequestChannel, - handler: () => requestListenerInMainMock, - }); - - applicationBuilder.beforeApplicationStart(({ mainDi }) => { - runInAction(() => { - mainDi.register(testChannelListenerInMainInjectable); - }); - }); - - await applicationBuilder.render(); - - const windowDi = applicationBuilder.applicationWindow.only.di; - - requestFromChannel = windowDi.inject( - requestFromChannelInjectionToken, - ); - }); - - describe("when requesting from channel", () => { - let actualPromise: Promise; - - beforeEach(() => { - actualPromise = requestFromChannel(testRequestChannel, "some-request"); - }); - - it("triggers listener in main", () => { - expect(requestListenerInMainMock).toHaveBeenCalledWith("some-request"); - }); - - it("does not resolve yet", async () => { - const promiseStatus = await getPromiseStatus(actualPromise); - - expect(promiseStatus.fulfilled).toBe(false); - }); - - it("when main resolves with response, resolves with response", async () => { - await requestListenerInMainMock.resolve("some-response"); - - const actual = await actualPromise; - - expect(actual).toBe("some-response"); - }); - }); - }); - - it("when registering multiple handlers for the same channel, throws", async () => { - const applicationBuilder = getApplicationBuilder(); - - const someChannelListenerInjectable = getInjectable({ - id: "some-channel-listener", - - instantiate: () => ({ - channel: testRequestChannel, - handler: () => () => "irrelevant", - }), - - injectionToken: requestChannelListenerInjectionToken, - }); - - const someOtherChannelListenerInjectable = getInjectable({ - id: "some-other-channel-listener", - - instantiate: () => ({ - channel: testRequestChannel, - handler: () => () => "irrelevant", - }), - - injectionToken: requestChannelListenerInjectionToken, - }); - - applicationBuilder.beforeApplicationStart(({ mainDi }) => { - runInAction(() => { - mainDi.register(someChannelListenerInjectable); - mainDi.register(someOtherChannelListenerInjectable); - }); - }); - - await expect(applicationBuilder.render()).rejects.toThrow('Tried to register a multiple channel handlers for "some-request-channel-id", only one handler is supported for a request channel.'); - }); -}); - -const testMessageChannel: TestMessageChannel = { - id: "some-message-channel-id", -}; - -const testRequestChannel: TestRequestChannel = { - id: "some-request-channel-id", -}; - diff --git a/packages/core/src/common/utils/channel/enlist-message-channel-listener-injection-token.ts b/packages/core/src/common/utils/channel/enlist-message-channel-listener-injection-token.ts deleted file mode 100644 index d6db37cea2..0000000000 --- a/packages/core/src/common/utils/channel/enlist-message-channel-listener-injection-token.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectionToken } from "@ogre-tools/injectable"; -import type { Disposer } from "@k8slens/utilities"; -import type { MessageChannel, MessageChannelListener } from "./message-channel-listener-injection-token"; - -export type EnlistMessageChannelListener = (listener: MessageChannelListener>) => Disposer; - -export const enlistMessageChannelListenerInjectionToken = getInjectionToken({ - id: "enlist-message-channel-listener", -}); diff --git a/packages/core/src/common/utils/channel/get-request-channel.ts b/packages/core/src/common/utils/channel/get-request-channel.ts deleted file mode 100644 index 4dc5b4914e..0000000000 --- a/packages/core/src/common/utils/channel/get-request-channel.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { RequestChannel } from "./request-channel-listener-injection-token"; - -export const getRequestChannel = (id: string): RequestChannel => ({ - id, -}); diff --git a/packages/core/src/common/utils/channel/listening-on-message-channels.injectable.ts b/packages/core/src/common/utils/channel/listening-on-message-channels.injectable.ts deleted file mode 100644 index 6ebc9ca7c1..0000000000 --- a/packages/core/src/common/utils/channel/listening-on-message-channels.injectable.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { getStartableStoppable } from "../get-startable-stoppable"; -import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token"; -import { enlistMessageChannelListenerInjectionToken } from "./enlist-message-channel-listener-injection-token"; -import { disposer } from "@k8slens/utilities"; - -const listeningOnMessageChannelsInjectable = getInjectable({ - id: "listening-on-message-channels", - - instantiate: (di) => { - const enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken); - const messageChannelListeners = di.injectMany(messageChannelListenerInjectionToken); - - return getStartableStoppable("listening-on-channels", () => ( - disposer(messageChannelListeners.map(enlistMessageChannelListener)) - )); - }, -}); - - -export default listeningOnMessageChannelsInjectable; diff --git a/packages/core/src/common/utils/channel/message-channel-listener-injection-token.ts b/packages/core/src/common/utils/channel/message-channel-listener-injection-token.ts deleted file mode 100644 index 5bfc45a82d..0000000000 --- a/packages/core/src/common/utils/channel/message-channel-listener-injection-token.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { DiContainerForInjection } from "@ogre-tools/injectable"; -import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; - -export interface MessageChannel { - id: string; - _messageSignature?: Message; // only used to mark `Message` as used -} - -export type MessageChannelHandler = Channel extends MessageChannel - ? (message: Message) => void - : never; - -export interface MessageChannelListener { - channel: Channel; - handler: MessageChannelHandler; -} - -export const messageChannelListenerInjectionToken = getInjectionToken>>( - { - id: "message-channel-listener", - }, -); - -export interface GetMessageChannelListenerInfo< - Channel extends MessageChannel, - Message, -> { - id: string; - channel: Channel; - handler: (di: DiContainerForInjection) => MessageChannelHandler; - causesSideEffects?: boolean; -} - -export function getMessageChannelListenerInjectable< - Channel extends MessageChannel, - Message, ->(info: GetMessageChannelListenerInfo) { - return getInjectable({ - id: `${info.channel.id}-listener-${info.id}`, - instantiate: (di) => ({ - channel: info.channel, - handler: info.handler(di), - }), - injectionToken: messageChannelListenerInjectionToken, - causesSideEffects: info.causesSideEffects, - }); -} diff --git a/packages/core/src/common/utils/channel/request-channel-listener-injection-token.ts b/packages/core/src/common/utils/channel/request-channel-listener-injection-token.ts deleted file mode 100644 index 2f0b84a3cc..0000000000 --- a/packages/core/src/common/utils/channel/request-channel-listener-injection-token.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -export interface RequestChannel { - id: string; - _requestSignature?: Request; // used only to mark `Request` as "used" - _responseSignature?: Response; // used only to mark `Response` as "used" -} diff --git a/packages/core/src/common/utils/registrator-helper.ts b/packages/core/src/common/utils/registrator-helper.ts new file mode 100644 index 0000000000..4a9cc5c2d2 --- /dev/null +++ b/packages/core/src/common/utils/registrator-helper.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { iter } from "@k8slens/utilities"; +import type { DiContainerForInjection, Injectable } from "@ogre-tools/injectable"; + +// Register new injectables and deregister removed injectables by id + +export const injectableDifferencingRegistratorWith = (di: DiContainerForInjection) => ( + (rawCurrent: Injectable[], rawPrevious: Injectable[] = []) => { + const current = new Map(rawCurrent.map(inj => [inj.id, inj])); + const previous = new Map(rawPrevious.map(inj => [inj.id, inj])); + const toAdd = iter.chain(current.entries()) + .filter(([id]) => !previous.has(id)) + .collect(entries => new Map(entries)); + const toRemove = iter.chain(previous.entries()) + .filter(([id]) => !current.has(id)) + .collect(entries => new Map(entries)); + + di.deregister(...toRemove.values()); + di.register(...toAdd.values()); + } +); diff --git a/packages/core/src/common/utils/resolve-system-proxy/resolve-system-proxy-channel.ts b/packages/core/src/common/utils/resolve-system-proxy/resolve-system-proxy-channel.ts index c823a8a8f9..210112c0b7 100644 --- a/packages/core/src/common/utils/resolve-system-proxy/resolve-system-proxy-channel.ts +++ b/packages/core/src/common/utils/resolve-system-proxy/resolve-system-proxy-channel.ts @@ -2,10 +2,8 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { RequestChannel } from "../channel/request-channel-listener-injection-token"; +import { getRequestChannel } from "@k8slens/messaging"; -export type ResolveSystemProxyChannel = RequestChannel; - -export const resolveSystemProxyChannel: ResolveSystemProxyChannel = { - id: "resolve-system-proxy-channel", -}; +export const resolveSystemProxyChannel = getRequestChannel( + "resolve-system-proxy-channel", +); diff --git a/packages/core/src/common/utils/sync-box/channel-listener.injectable.ts b/packages/core/src/common/utils/sync-box/channel-listener.injectable.ts index a97d95d726..482886d178 100644 --- a/packages/core/src/common/utils/sync-box/channel-listener.injectable.ts +++ b/packages/core/src/common/utils/sync-box/channel-listener.injectable.ts @@ -3,13 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { syncBoxChannel } from "./channels"; -import { getMessageChannelListenerInjectable } from "../channel/message-channel-listener-injection-token"; +import { getMessageChannelListenerInjectable } from "@k8slens/messaging"; import syncBoxStateInjectable from "./sync-box-state.injectable"; const syncBoxChannelListenerInjectable = getMessageChannelListenerInjectable({ id: "init", channel: syncBoxChannel, - handler: (di) => ({ id, value }) => di.inject(syncBoxStateInjectable, id).set(value), + getHandler: (di) => ({ id, value }) => di.inject(syncBoxStateInjectable, id).set(value), }); export default syncBoxChannelListenerInjectable; diff --git a/packages/core/src/common/utils/sync-box/channels.ts b/packages/core/src/common/utils/sync-box/channels.ts index 4df0462dc3..5178ff013c 100644 --- a/packages/core/src/common/utils/sync-box/channels.ts +++ b/packages/core/src/common/utils/sync-box/channels.ts @@ -2,20 +2,12 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { MessageChannel } from "../channel/message-channel-listener-injection-token"; -import type { RequestChannel } from "../channel/request-channel-listener-injection-token"; +import { getMessageChannel, getRequestChannel } from "@k8slens/messaging"; -export type SyncBoxChannel = MessageChannel<{ id: string; value: any }>; +export const syncBoxChannel = + getMessageChannel<{ id: string; value: any }>("sync-box-channel"); -export const syncBoxChannel: SyncBoxChannel = { - id: "sync-box-channel", -}; - -export type SyncBoxInitialValueChannel = RequestChannel< +export const syncBoxInitialValueChannel = getRequestChannel< void, { id: string; value: any }[] ->; - -export const syncBoxInitialValueChannel: SyncBoxInitialValueChannel = { - id: "sync-box-initial-value-channel", -}; +>("sync-box-initial-value-channel"); diff --git a/packages/core/src/common/utils/sync-box/create-sync-box.injectable.ts b/packages/core/src/common/utils/sync-box/create-sync-box.injectable.ts index 4a01fe71a0..2a142dc573 100644 --- a/packages/core/src/common/utils/sync-box/create-sync-box.injectable.ts +++ b/packages/core/src/common/utils/sync-box/create-sync-box.injectable.ts @@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { IObservableValue } from "mobx"; import { computed } from "mobx"; import { syncBoxChannel } from "./channels"; -import { sendMessageToChannelInjectionToken } from "../channel/message-to-channel-injection-token"; +import { sendMessageToChannelInjectionToken } from "@k8slens/messaging"; import syncBoxStateInjectable from "./sync-box-state.injectable"; import type { SyncBox } from "./sync-box-injection-token"; import { toJS } from "../toJS"; diff --git a/packages/core/src/common/utils/sync-box/handler.injectable.ts b/packages/core/src/common/utils/sync-box/handler.injectable.ts deleted file mode 100644 index f520585474..0000000000 --- a/packages/core/src/common/utils/sync-box/handler.injectable.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import type { MessageChannelHandler } from "../channel/message-channel-listener-injection-token"; -import type { SyncBoxChannel } from "./channels"; -import syncBoxStateInjectable from "./sync-box-state.injectable"; - -const syncBoxChannelHandlerInjectable = getInjectable({ - id: "sync-box-channel-handler", - instantiate: (di): MessageChannelHandler => { - const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id); - - return ({ id, value }) => getSyncBoxState(id)?.set(value); - }, -}); - -export default syncBoxChannelHandlerInjectable; diff --git a/packages/core/src/common/vars/build-semantic-version.injectable.ts b/packages/core/src/common/vars/build-semantic-version.injectable.ts index 2a49327480..402fb9d442 100644 --- a/packages/core/src/common/vars/build-semantic-version.injectable.ts +++ b/packages/core/src/common/vars/build-semantic-version.injectable.ts @@ -7,7 +7,7 @@ import { getInjectionToken } from "@ogre-tools/injectable"; import { SemVer } from "semver"; import type { InitializableState } from "../initializable-state/create"; import { createInitializableState } from "../initializable-state/create"; -import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; +import type { RequestChannel } from "@k8slens/messaging"; export const buildVersionInjectionToken = getInjectionToken>({ id: "build-version-token", diff --git a/packages/core/src/common/weblinks-store/weblink-store.injectable.ts b/packages/core/src/common/weblinks-store/weblink-store.injectable.ts index cf793a2e58..843c8716e5 100644 --- a/packages/core/src/common/weblinks-store/weblink-store.injectable.ts +++ b/packages/core/src/common/weblinks-store/weblink-store.injectable.ts @@ -11,7 +11,7 @@ import { persistStateToConfigInjectionToken } from "../base-store/save-to-file"; import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; import loggerInjectable from "../logger.injectable"; import getBasenameOfPathInjectable from "../path/get-basename.injectable"; -import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token"; +import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable"; import { weblinkStoreMigrationInjectionToken } from "./migration-token"; import { WeblinkStore } from "./weblink-store"; diff --git a/packages/core/src/extensions/extension-loader/extension/extension.injectable.ts b/packages/core/src/extensions/extension-loader/extension/extension.injectable.ts index 07f054d3fd..d54d997d09 100644 --- a/packages/core/src/extensions/extension-loader/extension/extension.injectable.ts +++ b/packages/core/src/extensions/extension-loader/extension/extension.injectable.ts @@ -2,29 +2,18 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { difference, find, map } from "lodash"; import { reaction, runInAction } from "mobx"; import { disposer } from "@k8slens/utilities"; import type { LensExtension } from "../../lens-extension"; import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token"; +import { injectableDifferencingRegistratorWith } from "../../../common/utils/registrator-helper"; export interface Extension { register: () => void; deregister: () => void; } -const idsToInjectables = (ids: string[], injectables: Injectable[]) => ids.map(id => { - const injectable = find(injectables, { id }); - - if (!injectable) { - throw new Error(`Injectable ${id} not found`); - } - - return injectable; -}); - const extensionInjectable = getInjectable({ id: "extension", @@ -35,36 +24,27 @@ const extensionInjectable = getInjectable({ instantiate: (childDi) => { const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken); const reactionDisposer = disposer(); + const injectableDifferencingRegistrator = injectableDifferencingRegistratorWith(childDi); return { register: () => { - extensionRegistrators.forEach((getInjectablesOfExtension) => { - const injectables = getInjectablesOfExtension(instance); + for (const extensionRegistrator of extensionRegistrators) { + const injectables = extensionRegistrator(instance); - reactionDisposer.push( - // injectables is either an array or a computed array, in which case - // we need to update the registered injectables with a reaction every time they change - reaction( - () => Array.isArray(injectables) ? injectables : injectables.get(), - (currentInjectables, previousInjectables = []) => { - // Register new injectables and deregister removed injectables by id - const currentIds = map(currentInjectables, "id"); - const previousIds = map(previousInjectables, "id"); - const idsToAdd = difference(currentIds, previousIds); - const idsToRemove = previousIds.filter(previousId => !currentIds.includes(previousId)); - - if (idsToRemove.length > 0) { - childDi.deregister(...idsToInjectables(idsToRemove, previousInjectables)); - } - - if (idsToAdd.length > 0) { - childDi.register(...idsToInjectables(idsToAdd, currentInjectables)); - } - }, { + if (Array.isArray(injectables)) { + runInAction(() => { + injectableDifferencingRegistrator(injectables); + }); + } else { + reactionDisposer.push(reaction( + () => injectables.get(), + injectableDifferencingRegistrator, + { fireImmediately: true, }, )); - }); + } + } }, deregister: () => { diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts index 847ae37f03..2295608520 100644 --- a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts @@ -12,7 +12,7 @@ import { baseStoreIpcChannelPrefixesInjectionToken } from "../../../common/base- import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../../../common/base-store/disable-sync"; import { persistStateToConfigInjectionToken } from "../../../common/base-store/save-to-file"; import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; -import { enlistMessageChannelListenerInjectionToken } from "../../../common/utils/channel/enlist-message-channel-listener-injection-token"; +import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; import ensureHashedDirectoryForExtensionInjectable from "./ensure-hashed-directory-for-extension.injectable"; import { registeredExtensionsInjectable } from "./registered-extensions.injectable"; diff --git a/packages/core/src/extensions/extension-store.ts b/packages/core/src/extensions/extension-store.ts index cf435fb474..fe9cff2a02 100644 --- a/packages/core/src/extensions/extension-store.ts +++ b/packages/core/src/extensions/extension-store.ts @@ -20,7 +20,7 @@ import { baseStoreIpcChannelPrefixesInjectionToken } from "../common/base-store/ import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../common/base-store/disable-sync"; import { persistStateToConfigInjectionToken } from "../common/base-store/save-to-file"; import getBasenameOfPathInjectable from "../common/path/get-basename.injectable"; -import { enlistMessageChannelListenerInjectionToken } from "../common/utils/channel/enlist-message-channel-listener-injection-token"; +import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; export interface ExtensionStoreParams extends BaseStoreParams { migrations?: Migrations; diff --git a/packages/core/src/extensions/extensions-store/extensions-store.injectable.ts b/packages/core/src/extensions/extensions-store/extensions-store.injectable.ts index 9f5ff83270..9e08a96aac 100644 --- a/packages/core/src/extensions/extensions-store/extensions-store.injectable.ts +++ b/packages/core/src/extensions/extensions-store/extensions-store.injectable.ts @@ -10,7 +10,7 @@ import { persistStateToConfigInjectionToken } from "../../common/base-store/save import getConfigurationFileModelInjectable from "../../common/get-configuration-file-model/get-configuration-file-model.injectable"; import loggerInjectable from "../../common/logger.injectable"; import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable"; -import { enlistMessageChannelListenerInjectionToken } from "../../common/utils/channel/enlist-message-channel-listener-injection-token"; +import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; import storeMigrationVersionInjectable from "../../common/vars/store-migration-version.injectable"; import { ExtensionsStore } from "./extensions-store"; diff --git a/packages/core/src/features/application-menu/main/application-menu-reactivity.injectable.ts b/packages/core/src/features/application-menu/main/application-menu-reactivity.injectable.ts index 80bfd108fd..e5d95bf7d8 100644 --- a/packages/core/src/features/application-menu/main/application-menu-reactivity.injectable.ts +++ b/packages/core/src/features/application-menu/main/application-menu-reactivity.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { autorun } from "mobx"; -import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable"; +import { getStartableStoppable } from "@k8slens/startable-stoppable"; import populateApplicationMenuInjectable from "./populate-application-menu.injectable"; import applicationMenuItemCompositeInjectable from "./application-menu-item-composite.injectable"; diff --git a/packages/core/src/features/application-update/child-features/application-update-using-application-menu/main/check-for-updates-menu-item.injectable.ts b/packages/core/src/features/application-update/child-features/application-update-using-application-menu/main/check-for-updates-menu-item.injectable.ts index fe997e62fb..97b9acf9f8 100644 --- a/packages/core/src/features/application-update/child-features/application-update-using-application-menu/main/check-for-updates-menu-item.injectable.ts +++ b/packages/core/src/features/application-update/child-features/application-update-using-application-menu/main/check-for-updates-menu-item.injectable.ts @@ -28,7 +28,7 @@ const checkForUpdatesMenuItemInjectable = getInjectable({ id: "check-for-updates", parentId: isMac ? "mac" : "help", orderNumber: isMac ? 20 : 50, - label: "Check for updates", + label: "Check for Updates...", isShown: updatingIsEnabled, onClick: () => { diff --git a/packages/core/src/features/application-update/child-features/application-update-using-tray/installing-update-using-tray.test.ts b/packages/core/src/features/application-update/child-features/application-update-using-tray/installing-update-using-tray.test.ts index d5caa63149..f18f288469 100644 --- a/packages/core/src/features/application-update/child-features/application-update-using-tray/installing-update-using-tray.test.ts +++ b/packages/core/src/features/application-update/child-features/application-update-using-tray/installing-update-using-tray.test.ts @@ -146,7 +146,7 @@ describe("installing update using tray", () => { it("name of tray item for checking updates indicates that checking is happening", () => { expect( builder.tray.get("check-for-updates")?.label, - ).toBe("Checking for updates..."); + ).toBe("Checking for Updates..."); }); it("user cannot install update yet", () => { @@ -177,7 +177,7 @@ describe("installing update using tray", () => { it("name of tray item for checking updates no longer indicates that checking is happening", () => { expect( builder.tray.get("check-for-updates")?.label, - ).toBe("Check for updates"); + ).toBe("Check for Updates..."); }); it("renders", () => { @@ -241,7 +241,7 @@ describe("installing update using tray", () => { it("name of tray item for checking updates no longer indicates that downloading is happening", () => { expect( builder.tray.get("check-for-updates")?.label, - ).toBe("Check for updates"); + ).toBe("Check for Updates..."); }); it("renders", () => { @@ -269,7 +269,7 @@ describe("installing update using tray", () => { it("name of tray item for checking updates no longer indicates that downloading is happening", () => { expect( builder.tray.get("check-for-updates")?.label, - ).toBe("Check for updates"); + ).toBe("Check for Updates..."); }); it("renders", () => { diff --git a/packages/core/src/features/application-update/child-features/application-update-using-tray/main/tray-items/check-for-updates-tray-item.injectable.ts b/packages/core/src/features/application-update/child-features/application-update-using-tray/main/tray-items/check-for-updates-tray-item.injectable.ts index e904abaa99..3f05ddb71e 100644 --- a/packages/core/src/features/application-update/child-features/application-update-using-tray/main/tray-items/check-for-updates-tray-item.injectable.ts +++ b/packages/core/src/features/application-update/child-features/application-update-using-tray/main/tray-items/check-for-updates-tray-item.injectable.ts @@ -47,10 +47,10 @@ const checkForUpdatesTrayItemInjectable = getInjectable({ } if (checkingForUpdatesState.value.get()) { - return "Checking for updates..."; + return "Checking for Updates..."; } - return "Check for updates"; + return "Check for Updates..."; }), enabled: computed(() => !checkingForUpdatesState.value.get() && !downloadingUpdateState.value.get()), diff --git a/packages/core/src/features/application-update/child-features/periodical-checking-of-updates/main/periodical-check-for-updates.injectable.ts b/packages/core/src/features/application-update/child-features/periodical-checking-of-updates/main/periodical-check-for-updates.injectable.ts index 85c687e478..953aa2b45a 100644 --- a/packages/core/src/features/application-update/child-features/periodical-checking-of-updates/main/periodical-check-for-updates.injectable.ts +++ b/packages/core/src/features/application-update/child-features/periodical-checking-of-updates/main/periodical-check-for-updates.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { getStartableStoppable } from "../../../../../common/utils/get-startable-stoppable"; +import { getStartableStoppable } from "@k8slens/startable-stoppable"; import processCheckingForUpdatesInjectable from "../../../main/process-checking-for-updates.injectable"; import withOrphanPromiseInjectable from "../../../../../common/utils/with-orphan-promise/with-orphan-promise.injectable"; diff --git a/packages/core/src/features/application-update/common/restart-and-install-update-channel.ts b/packages/core/src/features/application-update/common/restart-and-install-update-channel.ts index 470debf981..87b8f76cf3 100644 --- a/packages/core/src/features/application-update/common/restart-and-install-update-channel.ts +++ b/packages/core/src/features/application-update/common/restart-and-install-update-channel.ts @@ -2,7 +2,7 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { MessageChannel } from "../../../common/utils/channel/message-channel-listener-injection-token"; +import type { MessageChannel } from "@k8slens/messaging"; export type RestartAndInstallUpdateChannel = MessageChannel; diff --git a/packages/core/src/features/application-update/main/restart-and-install-update/restart-and-install-update-listener.injectable.ts b/packages/core/src/features/application-update/main/restart-and-install-update/restart-and-install-update-listener.injectable.ts index 940856ad3b..768ad50ffd 100644 --- a/packages/core/src/features/application-update/main/restart-and-install-update/restart-and-install-update-listener.injectable.ts +++ b/packages/core/src/features/application-update/main/restart-and-install-update/restart-and-install-update-listener.injectable.ts @@ -3,13 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { restartAndInstallUpdateChannel } from "../../common/restart-and-install-update-channel"; -import { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import { getMessageChannelListenerInjectable } from "@k8slens/messaging"; import quitAndInstallUpdateInjectable from "../quit-and-install-update.injectable"; const restartAndInstallUpdateListenerInjectable = getMessageChannelListenerInjectable({ id: "restart", channel: restartAndInstallUpdateChannel, - handler: (di) => di.inject(quitAndInstallUpdateInjectable), + getHandler: (di) => di.inject(quitAndInstallUpdateInjectable), }); export default restartAndInstallUpdateListenerInjectable; diff --git a/packages/core/src/features/application-update/main/watch-if-update-should-happen-on-quit/watch-if-update-should-happen-on-quit.injectable.ts b/packages/core/src/features/application-update/main/watch-if-update-should-happen-on-quit/watch-if-update-should-happen-on-quit.injectable.ts index bcad45a0ac..bd949194e6 100644 --- a/packages/core/src/features/application-update/main/watch-if-update-should-happen-on-quit/watch-if-update-should-happen-on-quit.injectable.ts +++ b/packages/core/src/features/application-update/main/watch-if-update-should-happen-on-quit/watch-if-update-should-happen-on-quit.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { autorun } from "mobx"; -import { getStartableStoppable } from "../../../../common/utils/get-startable-stoppable"; +import { getStartableStoppable } from "@k8slens/startable-stoppable"; import setUpdateOnQuitInjectable from "../../../../main/electron-app/features/set-update-on-quit.injectable"; import selectedUpdateChannelInjectable from "../../common/selected-update-channel/selected-update-channel.injectable"; import type { ReleaseChannel, UpdateChannel } from "../../common/update-channels"; diff --git a/packages/core/src/features/application-update/renderer/restart-and-install-update.injectable.ts b/packages/core/src/features/application-update/renderer/restart-and-install-update.injectable.ts index 2502a599a2..d60f8da909 100644 --- a/packages/core/src/features/application-update/renderer/restart-and-install-update.injectable.ts +++ b/packages/core/src/features/application-update/renderer/restart-and-install-update.injectable.ts @@ -4,13 +4,13 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { restartAndInstallUpdateChannel } from "../common/restart-and-install-update-channel"; -import messageToChannelInjectable from "../../../renderer/utils/channel/message-to-channel.injectable"; +import { sendMessageToChannelInjectionToken } from "@k8slens/messaging"; const restartAndInstallUpdateInjectable = getInjectable({ id: "restart-and-install-update", instantiate: (di) => { - const messageToChannel = di.inject(messageToChannelInjectable); + const messageToChannel = di.inject(sendMessageToChannelInjectionToken); return () => { messageToChannel(restartAndInstallUpdateChannel); diff --git a/packages/core/src/features/catalog/__snapshots__/entity-running.test.tsx.snap b/packages/core/src/features/catalog/__snapshots__/entity-running.test.tsx.snap index 2d5653372c..8586d4538c 100644 --- a/packages/core/src/features/catalog/__snapshots__/entity-running.test.tsx.snap +++ b/packages/core/src/features/catalog/__snapshots__/entity-running.test.tsx.snap @@ -490,10 +490,10 @@ exports[`entity running technical tests when navigated to catalog renders 1`] = style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;" >
("certificate-authorities"); diff --git a/packages/core/src/features/certificate-authorities/main/channel-handler.global-override-for-injectable.ts b/packages/core/src/features/certificate-authorities/main/channel-handler.global-override-for-injectable.ts index 62882fb49f..a7c52503c5 100644 --- a/packages/core/src/features/certificate-authorities/main/channel-handler.global-override-for-injectable.ts +++ b/packages/core/src/features/certificate-authorities/main/channel-handler.global-override-for-injectable.ts @@ -8,6 +8,7 @@ import { casChannel } from "../common/channel"; import certificateAuthoritiesChannelListenerInjectable from "./channel-handler.injectable"; export default getGlobalOverride(certificateAuthoritiesChannelListenerInjectable, () => ({ + id: "certificate-authorities-channel-listener", channel: casChannel, handler: () => [], })); diff --git a/packages/core/src/features/certificate-authorities/main/channel-handler.injectable.ts b/packages/core/src/features/certificate-authorities/main/channel-handler.injectable.ts index 206a68ba94..d776e9fcce 100644 --- a/packages/core/src/features/certificate-authorities/main/channel-handler.injectable.ts +++ b/packages/core/src/features/certificate-authorities/main/channel-handler.injectable.ts @@ -2,14 +2,15 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { getRequestChannelListenerInjectable } from "../../../main/utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import { casChannel } from "../common/channel"; import { globalAgent } from "https"; import { isString } from "@k8slens/utilities"; const certificateAuthoritiesChannelListenerInjectable = getRequestChannelListenerInjectable({ + id: "certificate-authorities-channel-listener", channel: casChannel, - handler: () => () => { + getHandler: () => () => { if (Array.isArray(globalAgent.options.ca)) { return globalAgent.options.ca.filter(isString); } diff --git a/packages/core/src/features/certificate-authorities/renderer/request-system-cas.injectable.ts b/packages/core/src/features/certificate-authorities/renderer/request-system-cas.injectable.ts index e3c840a95a..e9ac074989 100644 --- a/packages/core/src/features/certificate-authorities/renderer/request-system-cas.injectable.ts +++ b/packages/core/src/features/certificate-authorities/renderer/request-system-cas.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import { casChannel } from "../common/channel"; import { requestSystemCAsInjectionToken } from "../common/request-system-cas-token"; diff --git a/packages/core/src/features/cluster/activation/common/channels.ts b/packages/core/src/features/cluster/activation/common/channels.ts index 3631c4d9b8..55172e75fc 100644 --- a/packages/core/src/features/cluster/activation/common/channels.ts +++ b/packages/core/src/features/cluster/activation/common/channels.ts @@ -4,7 +4,7 @@ */ import type { ClusterId } from "../../../../common/cluster-types"; -import { getRequestChannel } from "../../../../common/utils/channel/get-request-channel"; +import { getRequestChannel } from "@k8slens/messaging"; export interface ActivateCluster { clusterId: ClusterId; diff --git a/packages/core/src/features/cluster/activation/common/request-token.ts b/packages/core/src/features/cluster/activation/common/request-token.ts index 5bee44208f..0abe5775b3 100644 --- a/packages/core/src/features/cluster/activation/common/request-token.ts +++ b/packages/core/src/features/cluster/activation/common/request-token.ts @@ -4,7 +4,7 @@ */ import { getInjectionToken } from "@ogre-tools/injectable"; -import type { ChannelRequester } from "../../../../common/utils/channel/request-from-channel-injection-token"; +import type { ChannelRequester } from "@k8slens/messaging"; import type { activateClusterChannel, deactivateClusterChannel } from "./channels"; export type RequestClusterActivation = ChannelRequester; diff --git a/packages/core/src/features/cluster/activation/main/activate-listener.injectable.ts b/packages/core/src/features/cluster/activation/main/activate-listener.injectable.ts index 4ce22e6311..08b068abef 100644 --- a/packages/core/src/features/cluster/activation/main/activate-listener.injectable.ts +++ b/packages/core/src/features/cluster/activation/main/activate-listener.injectable.ts @@ -2,13 +2,14 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import { activateClusterChannel } from "../common/channels"; import requestClusterActivationInjectable from "./request-activation.injectable"; const activateClusterRequestChannelListenerInjectable = getRequestChannelListenerInjectable({ + id: "activate-cluster-request-channel-listener", channel: activateClusterChannel, - handler: (di) => di.inject(requestClusterActivationInjectable), + getHandler: (di) => di.inject(requestClusterActivationInjectable), }); export default activateClusterRequestChannelListenerInjectable; diff --git a/packages/core/src/features/cluster/activation/main/deactivate-listener.injectable.ts b/packages/core/src/features/cluster/activation/main/deactivate-listener.injectable.ts index e24ce48ba6..bbfe79a41d 100644 --- a/packages/core/src/features/cluster/activation/main/deactivate-listener.injectable.ts +++ b/packages/core/src/features/cluster/activation/main/deactivate-listener.injectable.ts @@ -2,13 +2,14 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import { deactivateClusterChannel } from "../common/channels"; import requestClusterDeactivationInjectable from "./request-deactivation.injectable"; const clusterDeactivationRequestChannelListenerInjectable = getRequestChannelListenerInjectable({ + id: "cluster-deactivation-request-channel-listener", channel: deactivateClusterChannel, - handler: (di) => di.inject(requestClusterDeactivationInjectable), + getHandler: (di) => di.inject(requestClusterDeactivationInjectable), }); export default clusterDeactivationRequestChannelListenerInjectable; diff --git a/packages/core/src/features/cluster/activation/renderer/request-activation.injectable.ts b/packages/core/src/features/cluster/activation/renderer/request-activation.injectable.ts index 1677d67792..6609516918 100644 --- a/packages/core/src/features/cluster/activation/renderer/request-activation.injectable.ts +++ b/packages/core/src/features/cluster/activation/renderer/request-activation.injectable.ts @@ -3,14 +3,14 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import { activateClusterChannel } from "../common/channels"; import { requestClusterActivationInjectionToken } from "../common/request-token"; const requestClusterActivationInjectable = getInjectable({ id: "request-cluster-activation", instantiate: (di) => { - const requestFromChannel = di.inject(requestFromChannelInjectable); + const requestFromChannel = di.inject(requestFromChannelInjectionToken); return (req) => requestFromChannel(activateClusterChannel, req); }, diff --git a/packages/core/src/features/cluster/activation/renderer/request-deactivation.injectable.ts b/packages/core/src/features/cluster/activation/renderer/request-deactivation.injectable.ts index 362784e91b..e019b8b615 100644 --- a/packages/core/src/features/cluster/activation/renderer/request-deactivation.injectable.ts +++ b/packages/core/src/features/cluster/activation/renderer/request-deactivation.injectable.ts @@ -3,14 +3,14 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import { deactivateClusterChannel } from "../common/channels"; import { requestClusterDeactivationInjectionToken } from "../common/request-token"; const requestClusterDeactivationInjectable = getInjectable({ id: "request-cluster-deactivation", instantiate: (di) => { - const requestFromChannel = di.inject(requestFromChannelInjectable); + const requestFromChannel = di.inject(requestFromChannelInjectionToken); return (clusterId) => requestFromChannel(deactivateClusterChannel, clusterId); }, diff --git a/packages/core/src/features/cluster/delete-dialog/common/clear-as-deleting-channel.ts b/packages/core/src/features/cluster/delete-dialog/common/clear-as-deleting-channel.ts index bf33a23165..f6bdc1d072 100644 --- a/packages/core/src/features/cluster/delete-dialog/common/clear-as-deleting-channel.ts +++ b/packages/core/src/features/cluster/delete-dialog/common/clear-as-deleting-channel.ts @@ -3,10 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import type { ClusterId } from "../../../../common/cluster-types"; -import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; +import { getRequestChannel } from "@k8slens/messaging"; -export type ClearClusterAsDeletingChannel = RequestChannel; - -export const clearClusterAsDeletingChannel: ClearClusterAsDeletingChannel = { - id: "clear-cluster-as-deleting", -}; +export const clearClusterAsDeletingChannel = getRequestChannel( + "clear-cluster-as-deleting", +); diff --git a/packages/core/src/features/cluster/delete-dialog/common/delete-channel.ts b/packages/core/src/features/cluster/delete-dialog/common/delete-channel.ts index 0e9142fcd3..b3a007b1bd 100644 --- a/packages/core/src/features/cluster/delete-dialog/common/delete-channel.ts +++ b/packages/core/src/features/cluster/delete-dialog/common/delete-channel.ts @@ -3,10 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import type { ClusterId } from "../../../../common/cluster-types"; -import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; +import type { RequestChannel } from "@k8slens/messaging"; +import { getRequestChannel } from "@k8slens/messaging"; export type DeleteClusterChannel = RequestChannel; -export const deleteClusterChannel: DeleteClusterChannel = { - id: "delete-cluster", -}; +export const deleteClusterChannel = getRequestChannel( + "delete-cluster", +); diff --git a/packages/core/src/features/cluster/delete-dialog/common/set-as-deleting-channel.ts b/packages/core/src/features/cluster/delete-dialog/common/set-as-deleting-channel.ts index 57ef2e3a8d..7b93b292d5 100644 --- a/packages/core/src/features/cluster/delete-dialog/common/set-as-deleting-channel.ts +++ b/packages/core/src/features/cluster/delete-dialog/common/set-as-deleting-channel.ts @@ -3,10 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import type { ClusterId } from "../../../../common/cluster-types"; -import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; +import { getRequestChannel } from "@k8slens/messaging"; -export type SetClusterAsDeletingChannel = RequestChannel; - -export const setClusterAsDeletingChannel: SetClusterAsDeletingChannel = { - id: "set-cluster-as-deleting", -}; +export const setClusterAsDeletingChannel = getRequestChannel( + "set-cluster-as-deleting", +); diff --git a/packages/core/src/features/cluster/delete-dialog/main/clear-as-deleting-channel-listener.injectable.ts b/packages/core/src/features/cluster/delete-dialog/main/clear-as-deleting-channel-listener.injectable.ts index a8dd2a80b0..a83de06acb 100644 --- a/packages/core/src/features/cluster/delete-dialog/main/clear-as-deleting-channel-listener.injectable.ts +++ b/packages/core/src/features/cluster/delete-dialog/main/clear-as-deleting-channel-listener.injectable.ts @@ -3,12 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable"; -import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import { clearClusterAsDeletingChannel } from "../common/clear-as-deleting-channel"; const clearClusterAsDeletingChannelListenerInjectable = getRequestChannelListenerInjectable({ + id: "clear-cluster-as-deleting-channel-listener", channel: clearClusterAsDeletingChannel, - handler: (di) => { + getHandler: (di) => { const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable); return (clusterId) => { diff --git a/packages/core/src/features/cluster/delete-dialog/main/delete-channel-listener.injectable.ts b/packages/core/src/features/cluster/delete-dialog/main/delete-channel-listener.injectable.ts index 94caf169ba..30d976c59c 100644 --- a/packages/core/src/features/cluster/delete-dialog/main/delete-channel-listener.injectable.ts +++ b/packages/core/src/features/cluster/delete-dialog/main/delete-channel-listener.injectable.ts @@ -10,12 +10,13 @@ import removePathInjectable from "../../../../common/fs/remove.injectable"; import joinPathsInjectable from "../../../../common/path/join-paths.injectable"; import clusterConnectionInjectable from "../../../../main/cluster/cluster-connection.injectable"; import { noop } from "@k8slens/utilities"; -import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import { deleteClusterChannel } from "../common/delete-channel"; const deleteClusterChannelListenerInjectable = getRequestChannelListenerInjectable({ + id: "delete-cluster-channel-listener", channel: deleteClusterChannel, - handler: (di) => { + getHandler: (di) => { const emitAppEvent = di.inject(emitAppEventInjectable); const clusterStore = di.inject(clusterStoreInjectable); const clusterFrames = di.inject(clusterFramesInjectable); diff --git a/packages/core/src/features/cluster/delete-dialog/main/set-as-deleteing-channel-listener.injectable.ts b/packages/core/src/features/cluster/delete-dialog/main/set-as-deleteing-channel-listener.injectable.ts index f532b4a81f..74bd168723 100644 --- a/packages/core/src/features/cluster/delete-dialog/main/set-as-deleteing-channel-listener.injectable.ts +++ b/packages/core/src/features/cluster/delete-dialog/main/set-as-deleteing-channel-listener.injectable.ts @@ -3,12 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable"; -import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import { setClusterAsDeletingChannel } from "../common/set-as-deleting-channel"; const setClusterAsDeletingChannelHandlerInjectable = getRequestChannelListenerInjectable({ + id: "set-cluster-as-deleting-channel-handler", channel: setClusterAsDeletingChannel, - handler: (di) => { + getHandler: (di) => { const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable); return (clusterId) => { diff --git a/packages/core/src/features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable.ts b/packages/core/src/features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable.ts index e476998ca0..1e3125a52a 100644 --- a/packages/core/src/features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable.ts +++ b/packages/core/src/features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import type { ClusterId } from "../../../../common/cluster-types"; -import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import { clearClusterAsDeletingChannel } from "../common/clear-as-deleting-channel"; export type RequestClearClusterAsDeleting = (clusterId: ClusterId) => Promise; @@ -12,7 +12,7 @@ export type RequestClearClusterAsDeleting = (clusterId: ClusterId) => Promise { - const requestChannel = di.inject(requestFromChannelInjectable); + const requestChannel = di.inject(requestFromChannelInjectionToken); return (clusterId) => requestChannel(clearClusterAsDeletingChannel, clusterId); }, diff --git a/packages/core/src/features/cluster/delete-dialog/renderer/request-delete.injectable.ts b/packages/core/src/features/cluster/delete-dialog/renderer/request-delete.injectable.ts index c1286e3103..9e415e79d0 100644 --- a/packages/core/src/features/cluster/delete-dialog/renderer/request-delete.injectable.ts +++ b/packages/core/src/features/cluster/delete-dialog/renderer/request-delete.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import type { ClusterId } from "../../../../common/cluster-types"; -import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import { deleteClusterChannel } from "../common/delete-channel"; export type RequestDeleteCluster = (clusterId: ClusterId) => Promise; @@ -12,7 +12,7 @@ export type RequestDeleteCluster = (clusterId: ClusterId) => Promise; const requestDeleteClusterInjectable = getInjectable({ id: "request-delete-cluster", instantiate: (di): RequestDeleteCluster => { - const requestChannel = di.inject(requestFromChannelInjectable); + const requestChannel = di.inject(requestFromChannelInjectionToken); return (clusterId) => requestChannel(deleteClusterChannel, clusterId); }, diff --git a/packages/core/src/features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable.ts b/packages/core/src/features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable.ts index de3a6393b3..0e86632f89 100644 --- a/packages/core/src/features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable.ts +++ b/packages/core/src/features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import type { ClusterId } from "../../../../common/cluster-types"; -import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import { setClusterAsDeletingChannel } from "../common/set-as-deleting-channel"; export type RequestSetClusterAsDeleting = (clusterId: ClusterId) => Promise; @@ -12,7 +12,7 @@ export type RequestSetClusterAsDeleting = (clusterId: ClusterId) => Promise { - const requestChannel = di.inject(requestFromChannelInjectable); + const requestChannel = di.inject(requestFromChannelInjectionToken); return (clusterId) => requestChannel(setClusterAsDeletingChannel, clusterId); }, diff --git a/packages/core/src/features/cluster/refresh-accessibility-technical.test.ts b/packages/core/src/features/cluster/refresh-accessibility-technical.test.ts new file mode 100644 index 0000000000..73ff6151b9 --- /dev/null +++ b/packages/core/src/features/cluster/refresh-accessibility-technical.test.ts @@ -0,0 +1,637 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { AuthorizationV1Api, CoreV1Api, V1APIGroupList, V1APIVersions, V1NamespaceList, V1SelfSubjectAccessReview, V1SelfSubjectRulesReview } from "@kubernetes/client-node"; +import clusterStoreInjectable from "../../common/cluster-store/cluster-store.injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import createAuthorizationApiInjectable from "../../common/cluster/create-authorization-api.injectable"; +import writeJsonFileInjectable from "../../common/fs/write-json-file.injectable"; +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; +import type { PartialDeep } from "type-fest"; +import { anyObject } from "jest-mock-extended"; +import createCoreApiInjectable from "../../common/cluster/create-core-api.injectable"; +import type { K8sRequest } from "../../main/k8s-request.injectable"; +import k8sRequestInjectable from "../../main/k8s-request.injectable"; +import type { DetectClusterMetadata } from "../../main/cluster-detectors/detect-cluster-metadata.injectable"; +import detectClusterMetadataInjectable from "../../main/cluster-detectors/detect-cluster-metadata.injectable"; +import type { ClusterConnection } from "../../main/cluster/cluster-connection.injectable"; +import clusterConnectionInjectable from "../../main/cluster/cluster-connection.injectable"; +import type { KubeAuthProxy } from "../../main/kube-auth-proxy/create-kube-auth-proxy.injectable"; +import createKubeAuthProxyInjectable from "../../main/kube-auth-proxy/create-kube-auth-proxy.injectable"; +import type { Mocked } from "../../test-utils/mock-interface"; +import { flushPromises } from "@k8slens/test-utils"; + +describe("Refresh Cluster Accessibility Technical Tests", () => { + let builder: ApplicationBuilder; + let createSelfSubjectRulesReviewMock: AsyncFnMock; + let createSelfSubjectAccessReviewMock: AsyncFnMock; + let listNamespaceMock: AsyncFnMock; + let k8sRequestMock: AsyncFnMock; + let detectClusterMetadataMock: AsyncFnMock; + let kubeAuthProxyMock: Mocked; + + beforeEach(async () => { + builder = getApplicationBuilder(); + + const mainDi = builder.mainDi; + + mainDi.override(broadcastMessageInjectable, () => async () => {}); + + kubeAuthProxyMock = { + apiPrefix: "/some-api-prefix", + port: 0, + exit: jest.fn(), + run: asyncFn(), + }; + mainDi.override(createKubeAuthProxyInjectable, () => () => kubeAuthProxyMock); + + detectClusterMetadataMock = asyncFn(); + mainDi.override(detectClusterMetadataInjectable, () => detectClusterMetadataMock); + + k8sRequestMock = asyncFn(); + mainDi.override(k8sRequestInjectable, () => k8sRequestMock); + + createSelfSubjectRulesReviewMock = asyncFn(); + createSelfSubjectAccessReviewMock = asyncFn(); + mainDi.override(createAuthorizationApiInjectable, () => () => ({ + createSelfSubjectRulesReview: createSelfSubjectRulesReviewMock, + createSelfSubjectAccessReview: createSelfSubjectAccessReviewMock, + } as any)); + + listNamespaceMock = asyncFn(); + mainDi.override(createCoreApiInjectable, () => () => ({ + listNamespace: listNamespaceMock, + } as any)); + + await builder.render(); + }); + + describe("given a cluster with no configured preferences", () => { + let cluster: Cluster; + let clusterConnection: ClusterConnection; + let refreshPromise: Promise; + + beforeEach(async () => { + const mainDi = builder.mainDi; + const clusterStore = mainDi.inject(clusterStoreInjectable); + const writeJsonFile = mainDi.inject(writeJsonFileInjectable); + + await writeJsonFile("/some-kube-config-path", { + apiVersion: "v1", + kind: "Config", + clusters: [{ + name: "some-cluster-name", + cluster: { + server: "https://localhost:8989", + }, + }], + users: [{ + name: "some-user-name", + }], + contexts: [{ + name: "some-cluster-context", + context: { + user: "some-user-name", + cluster: "some-cluster-name", + }, + }], + }); + + clusterStore.addCluster({ + contextName: "some-cluster-context", + id: "some-cluster-id", + kubeConfigPath: "/some-kube-config-path", + }); + + cluster = clusterStore.getById("some-cluster-id") ?? (() => { throw new Error("missing cluster"); })(); + clusterConnection = mainDi.inject(clusterConnectionInjectable, cluster); + refreshPromise = clusterConnection.refreshAccessibilityAndMetadata(); + }); + + it("starts kubeAuthProxy", () => { + expect(kubeAuthProxyMock.run).toBeCalled(); + }); + + describe("when kubeAuthProxy has started running and its port is found", () => { + beforeEach(async () => { + kubeAuthProxyMock.port = 1235; + await kubeAuthProxyMock.run.resolve(); + await flushPromises(); + }); + + it("requests if cluster has admin permissions", async () => { + expect(createSelfSubjectAccessReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "kube-system", + resource: "*", + verb: "create", + }, + })); + }); + + describe.each([ true, false ])("when cluster admin request resolves to %p", (isAdmin) => { + beforeEach(async () => { + await createSelfSubjectAccessReviewMock.resolve({ + body: { + status: { + allowed: isAdmin, + }, + } as PartialDeep, + } as any); + }); + + it("requests if cluster has global watch permissions", () => { + expect(createSelfSubjectAccessReviewMock).toBeCalledWith(anyObject({ + spec: { + verb: "watch", + resource: "*", + }, + })); + }); + + describe.each([ true, false ])("when cluster global watch request resolves with %p", (globalWatch) => { + beforeEach(async () => { + await createSelfSubjectAccessReviewMock.resolve({ + body: { + status: { + allowed: globalWatch, + }, + } as PartialDeep, + } as any); + }); + + it("requests namespaces", () => { + expect(listNamespaceMock).toBeCalled(); + }); + + describe("when list namespaces resolves", () => { + beforeEach(async () => { + await listNamespaceMock.resolve(listNamespaceResponse); + }); + + it("requests core api versions", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/api", + ); + }); + + describe("when core api versions request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve({ + serverAddressByClientCIDRs: [], + versions: [ + "v1", + ], + } as V1APIVersions); + }); + + it("requests non-core api resource kinds", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/apis", + ); + }); + + describe("when non-core api resource kinds request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve(nonCoreApiResponse); + }); + + it("requests specific resource kinds in core", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/api/v1", + ); + }); + + describe("when core specific resource kinds request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve(coreApiKindsResponse); + }); + + it("requests specific resources kinds from the first non-core response", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/apis/node.k8s.io/v1", + ); + }); + + describe("when first specific resource kinds request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve(nodeK8sIoKindsResponse); + }); + + it("requests specific resources kinds from the second non-core response", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/apis/discovery.k8s.io/v1", + ); + }); + + describe("when second specific resource kinds request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve(discoveryK8sIoKindsResponse); + }); + + it("requests namespace list permissions for 'default' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "default", + }, + })); + }); + + describe("when the permissions are incomplete", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(defaultIncompletePermissions); + }); + + it("requests namespace list permissions for 'my-namespace' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "my-namespace", + }, + })); + }); + + describe("when the permissions request for 'my-namespace' resolves as empty", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests cluster metadata", () => { + expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" })); + }); + + describe("when cluster metadata request resolves", () => { + beforeEach(async () => { + await detectClusterMetadataMock.resolve({}); + }); + + it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => { + await refreshPromise; + }); + + it("should have the cluster displaying 'pods'", () => { + expect(cluster.resourcesToShow.has("pods")).toBe(true); + }); + + it("should have the cluster displaying 'namespaces'", () => { + expect(cluster.resourcesToShow.has("namespaces")).toBe(true); + }); + }); + }); + + describe.skip("when the permissions are incomplete", () => {}); + describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {}); + describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {}); + }); + + describe("when the permissions resolve to an empty list", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests namespace list permissions for 'my-namespace' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "my-namespace", + }, + })); + }); + + describe("when the permissions request for 'my-namespace' resolves as empty", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests cluster metadata", () => { + expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" })); + }); + + describe("when cluster metadata request resolves", () => { + beforeEach(async () => { + await detectClusterMetadataMock.resolve({}); + }); + + it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => { + await refreshPromise; + }); + + it("should have the cluster displaying 'pods'", () => { + expect(cluster.resourcesToShow.has("pods")).toBe(false); + }); + + it("should have the cluster not displaying 'namespaces'", () => { + expect(cluster.resourcesToShow.has("namespaces")).toBe(false); + }); + }); + }); + + describe.skip("when the permissions are incomplete", () => {}); + describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {}); + describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {}); + }); + + describe("when the permissions resolve to a single entry with 'list' verb", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(defaultSingleListPermissions); + }); + + it("requests namespace list permissions for 'my-namespace' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "my-namespace", + }, + })); + }); + + describe("when the permissions request for 'my-namespace' resolves as empty", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests cluster metadata", () => { + expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" })); + }); + + describe("when cluster metadata request resolves", () => { + beforeEach(async () => { + await detectClusterMetadataMock.resolve({}); + }); + + it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => { + await refreshPromise; + }); + + it("should have the cluster displaying 'pods'", () => { + expect(cluster.resourcesToShow.has("pods")).toBe(true); + }); + + it("should have the cluster not displaying 'namespaces'", () => { + expect(cluster.resourcesToShow.has("namespaces")).toBe(false); + }); + }); + }); + + describe.skip("when the permissions are incomplete", () => {}); + describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {}); + describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {}); + }); + + describe("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(defaultMultipleListPermissions); + }); + + it("requests namespace list permissions for 'my-namespace' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "my-namespace", + }, + })); + }); + + describe("when the permissions request for 'my-namespace' resolves as empty", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests cluster metadata", () => { + expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" })); + }); + + describe("when cluster metadata request resolves", () => { + beforeEach(async () => { + await detectClusterMetadataMock.resolve({}); + }); + + it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => { + await refreshPromise; + }); + + it("should have the cluster displaying 'pods'", () => { + expect(cluster.resourcesToShow.has("pods")).toBe(true); + }); + + it("should have the cluster not displaying 'namespaces'", () => { + expect(cluster.resourcesToShow.has("namespaces")).toBe(false); + }); + }); + }); + + describe.skip("when the permissions are incomplete", () => {}); + describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {}); + describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {}); + }); + }); + + describe.skip("when second specific resource kinds rejects", () => {}); + }); + }); + + describe.skip("when first specific resource kinds rejects", () => {}); + }); + }); + }); + }); + }); + }); + }); +}); + +const nonCoreApiResponse = { + groups: [ + { + name: "node.k8s.io", + versions: [ + { + groupVersion: "node.k8s.io/v1", + version: "v1", + }, + ], + preferredVersion: { + groupVersion: "node.k8s.io/v1", + version: "v1", + }, + }, + { + name: "discovery.k8s.io", + versions: [ + { + groupVersion: "discovery.k8s.io/v1", + version: "v1", + }, + ], + preferredVersion: { + groupVersion: "discovery.k8s.io/v1", + version: "v1", + }, + }, + ], +} as V1APIGroupList; + +const listNamespaceResponse = { + body: { + items: [ + { + metadata: { + name: "default", + }, + }, + { + metadata: { + name: "my-namespace", + }, + }, + ], + } as PartialDeep, +} as Awaited>; + +const coreApiKindsResponse = { + kind: "APIResourceList", + groupVersion: "v1", + resources: [ + { + name: "namespaces", + singularName: "", + namespaced: false, + kind: "Namespace", + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"], + shortNames: ["ns"], + storageVersionHash: "Q3oi5N2YM8M=", + }, + { + name: "pods", + singularName: "", + namespaced: true, + kind: "Pod", + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + shortNames: ["po"], + categories: ["all"], + storageVersionHash: "xPOwRZ+Yhw8=", + }, + { + name: "pods/attach", + singularName: "", + namespaced: true, + kind: "PodAttachOptions", + verbs: ["create", "get"], + }, + ], +}; + +const nodeK8sIoKindsResponse = { + kind: "APIResourceList", + apiVersion: "v1", + groupVersion: "node.k8s.io/v1", + resources: [ + { + name: "runtimeclasses", + singularName: "", + namespaced: false, + kind: "RuntimeClass", + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + storageVersionHash: "WQTu1GL3T2Q=", + }, + ], +}; + +const discoveryK8sIoKindsResponse = { + kind: "APIResourceList", + apiVersion: "v1", + groupVersion: "discovery.k8s.io/v1", + resources: [ + { + name: "endpointslices", + singularName: "", + namespaced: true, + kind: "EndpointSlice", + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + storageVersionHash: "Nx3SIv6I0mE=", + }, + ], +}; + +type CreateSelfSubjectRulesReviewRes = Awaited>; + +const defaultIncompletePermissions = { + body: { + status: { + incomplete: true, + }, + } as PartialDeep, +} as CreateSelfSubjectRulesReviewRes; + +const emptyPermissions = { + body: { + status: { + resourceRules: [], + }, + } as PartialDeep, +} as CreateSelfSubjectRulesReviewRes; + +const defaultSingleListPermissions = { + body: { + status: { + resourceRules: [{ + apiGroups: [""], + resources: ["pods"], + verbs: ["list"], + }], + }, + } as PartialDeep, +} as CreateSelfSubjectRulesReviewRes; + +const defaultMultipleListPermissions = { + body: { + status: { + resourceRules: [ + { + apiGroups: [""], + resources: ["pods"], + verbs: ["get"], + }, + { + apiGroups: [""], + resources: ["pods"], + verbs: ["list"], + }, + ], + }, + } as PartialDeep, +} as CreateSelfSubjectRulesReviewRes; diff --git a/packages/core/src/features/cluster/state-sync/common/channels.ts b/packages/core/src/features/cluster/state-sync/common/channels.ts index 7ceeb82f84..66ce718955 100644 --- a/packages/core/src/features/cluster/state-sync/common/channels.ts +++ b/packages/core/src/features/cluster/state-sync/common/channels.ts @@ -4,8 +4,7 @@ */ import type { ClusterId, ClusterState } from "../../../../common/cluster-types"; -import type { MessageChannel } from "../../../../common/utils/channel/message-channel-listener-injection-token"; -import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; +import type { MessageChannel, RequestChannel } from "@k8slens/messaging"; export interface ClusterStateSync { clusterId: ClusterId; diff --git a/packages/core/src/features/cluster/state-sync/main/emit-update.injectable.ts b/packages/core/src/features/cluster/state-sync/main/emit-update.injectable.ts index 8cadd32864..4499d2ac66 100644 --- a/packages/core/src/features/cluster/state-sync/main/emit-update.injectable.ts +++ b/packages/core/src/features/cluster/state-sync/main/emit-update.injectable.ts @@ -3,18 +3,16 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { MessageChannelHandler } from "../../../../common/utils/channel/message-channel-listener-injection-token"; -import { sendMessageToChannelInjectionToken } from "../../../../common/utils/channel/message-to-channel-injection-token"; +import { sendMessageToChannelInjectionToken } from "@k8slens/messaging"; +import type { ClusterStateSync } from "../common/channels"; import { clusterStateSyncChannel } from "../common/channels"; -export type EmitClusterStateUpdate = MessageChannelHandler; - const emitClusterStateUpdateInjectable = getInjectable({ id: "emit-cluster-state-update", - instantiate: (di): EmitClusterStateUpdate => { + instantiate: (di) => { const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); - return (message) => sendMessageToChannel(clusterStateSyncChannel, message); + return (message: ClusterStateSync) => sendMessageToChannel(clusterStateSyncChannel, message); }, }); diff --git a/packages/core/src/features/cluster/state-sync/main/handle-initial.injectable.ts b/packages/core/src/features/cluster/state-sync/main/handle-initial.injectable.ts index 708f032d48..db65024973 100644 --- a/packages/core/src/features/cluster/state-sync/main/handle-initial.injectable.ts +++ b/packages/core/src/features/cluster/state-sync/main/handle-initial.injectable.ts @@ -3,12 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable"; -import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import { initialClusterStatesChannel } from "../common/channels"; const handleInitialClusterStateSyncInjectable = getRequestChannelListenerInjectable({ + id: "handle-initial-cluster-state-sync", channel: initialClusterStatesChannel, - handler: (di) => { + getHandler: (di) => { const clusterStore = di.inject(clusterStoreInjectable); return () => clusterStore.clustersList.map(cluster => ({ diff --git a/packages/core/src/features/cluster/state-sync/renderer/listener.injectable.ts b/packages/core/src/features/cluster/state-sync/renderer/listener.injectable.ts index 9863a391e8..a3778d096e 100644 --- a/packages/core/src/features/cluster/state-sync/renderer/listener.injectable.ts +++ b/packages/core/src/features/cluster/state-sync/renderer/listener.injectable.ts @@ -3,13 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import getClusterByIdInjectable from "../../../../common/cluster-store/get-by-id.injectable"; -import { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import { getMessageChannelListenerInjectable } from "@k8slens/messaging"; import { clusterStateSyncChannel } from "../common/channels"; const clusterStateListenerInjectable = getMessageChannelListenerInjectable({ channel: clusterStateSyncChannel, id: "main", - handler: (di) => { + getHandler: (di) => { const getClusterById = di.inject(getClusterByIdInjectable); return ({ clusterId, state }) => getClusterById(clusterId)?.setState(state); diff --git a/packages/core/src/features/cluster/state-sync/renderer/request-initial.injectable.ts b/packages/core/src/features/cluster/state-sync/renderer/request-initial.injectable.ts index 89f72fbcf5..497d2c5aaf 100644 --- a/packages/core/src/features/cluster/state-sync/renderer/request-initial.injectable.ts +++ b/packages/core/src/features/cluster/state-sync/renderer/request-initial.injectable.ts @@ -3,8 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { RequestChannelHandler } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; -import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; +import type { RequestChannelHandler } from "@k8slens/messaging"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import { initialClusterStatesChannel } from "../common/channels"; export type RequestInitialClusterStates = RequestChannelHandler; @@ -12,7 +12,7 @@ export type RequestInitialClusterStates = RequestChannelHandler { - const requestFromChannel = di.inject(requestFromChannelInjectable); + const requestFromChannel = di.inject(requestFromChannelInjectionToken); return () => requestFromChannel(initialClusterStatesChannel); }, diff --git a/packages/core/src/features/extension-special-characters-in-page-registrations.test.tsx b/packages/core/src/features/extension-special-characters-in-page-registrations.test.tsx index ac038a5743..b9760eba79 100644 --- a/packages/core/src/features/extension-special-characters-in-page-registrations.test.tsx +++ b/packages/core/src/features/extension-special-characters-in-page-registrations.test.tsx @@ -31,8 +31,7 @@ describe("extension special characters in page registrations", () => { describe("when navigating to route with ID having special characters", () => { beforeEach(() => { - const testExtension = - builder.extensions.get("some-extension-id").applicationWindows.only; + const testExtension = builder.extensions.get("some-extension-id").applicationWindows.only; testExtension.navigate("/some-page-id/"); }); diff --git a/packages/core/src/features/extensions/navigate/common/channel.ts b/packages/core/src/features/extensions/navigate/common/channel.ts index 4c6892e6cb..ec7bc967dc 100644 --- a/packages/core/src/features/extensions/navigate/common/channel.ts +++ b/packages/core/src/features/extensions/navigate/common/channel.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { MessageChannel } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import type { MessageChannel } from "@k8slens/messaging"; export interface NavigateForExtensionArgs { extId: string; diff --git a/packages/core/src/features/extensions/navigate/renderer/listener.injectable.ts b/packages/core/src/features/extensions/navigate/renderer/listener.injectable.ts index fccf0efca6..2397def4f6 100644 --- a/packages/core/src/features/extensions/navigate/renderer/listener.injectable.ts +++ b/packages/core/src/features/extensions/navigate/renderer/listener.injectable.ts @@ -2,15 +2,15 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import { getMessageChannelListenerInjectable } from "@k8slens/messaging"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; import { navigateForExtensionChannel } from "../common/channel"; const navigateForExtensionListenerInjectable = getMessageChannelListenerInjectable({ channel: navigateForExtensionChannel, - id: "main", - handler: (di) => { + id: "renderer", + getHandler: (di) => { const extensionLoader = di.inject(extensionLoaderInjectable); return ({ extId, pageId, params }) => { diff --git a/packages/core/src/features/helm-charts/child-features/preferences/renderer/active-helm-repositories.injectable.ts b/packages/core/src/features/helm-charts/child-features/preferences/renderer/active-helm-repositories.injectable.ts index 7a0d5e6b47..b4195a876f 100644 --- a/packages/core/src/features/helm-charts/child-features/preferences/renderer/active-helm-repositories.injectable.ts +++ b/packages/core/src/features/helm-charts/child-features/preferences/renderer/active-helm-repositories.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import { asyncComputed } from "@ogre-tools/injectable-react"; import { getActiveHelmRepositoriesChannel } from "../../../../../common/helm/get-active-helm-repositories-channel"; -import { requestFromChannelInjectionToken } from "../../../../../common/utils/channel/request-from-channel-injection-token"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import showErrorNotificationInjectable from "../../../../../renderer/components/notifications/show-error-notification.injectable"; import helmRepositoriesErrorStateInjectable from "./helm-repositories-error-state.injectable"; import { runInAction } from "mobx"; diff --git a/packages/core/src/features/helm-charts/child-features/preferences/renderer/adding-of-public-helm-repository/select-helm-repository/add-helm-repository.injectable.ts b/packages/core/src/features/helm-charts/child-features/preferences/renderer/adding-of-public-helm-repository/select-helm-repository/add-helm-repository.injectable.ts index 6b39d8dddf..3c2199c490 100644 --- a/packages/core/src/features/helm-charts/child-features/preferences/renderer/adding-of-public-helm-repository/select-helm-repository/add-helm-repository.injectable.ts +++ b/packages/core/src/features/helm-charts/child-features/preferences/renderer/adding-of-public-helm-repository/select-helm-repository/add-helm-repository.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import type { HelmRepo } from "../../../../../../../common/helm/helm-repo"; -import { requestFromChannelInjectionToken } from "../../../../../../../common/utils/channel/request-from-channel-injection-token"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import activeHelmRepositoriesInjectable from "../../active-helm-repositories.injectable"; import showErrorNotificationInjectable from "../../../../../../../renderer/components/notifications/show-error-notification.injectable"; import showSuccessNotificationInjectable from "../../../../../../../renderer/components/notifications/show-success-notification.injectable"; diff --git a/packages/core/src/features/helm-charts/child-features/preferences/renderer/remove-helm-repository.injectable.ts b/packages/core/src/features/helm-charts/child-features/preferences/renderer/remove-helm-repository.injectable.ts index 3aa56a4b04..4af5543c28 100644 --- a/packages/core/src/features/helm-charts/child-features/preferences/renderer/remove-helm-repository.injectable.ts +++ b/packages/core/src/features/helm-charts/child-features/preferences/renderer/remove-helm-repository.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import type { HelmRepo } from "../../../../../common/helm/helm-repo"; -import { requestFromChannelInjectionToken } from "../../../../../common/utils/channel/request-from-channel-injection-token"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import activeHelmRepositoriesInjectable from "./active-helm-repositories.injectable"; import { removeHelmRepositoryChannel } from "../../../../../common/helm/remove-helm-repository-channel"; diff --git a/packages/core/src/features/navigation/reload-page/common/channel.ts b/packages/core/src/features/navigation/reload-page/common/channel.ts index b920067a4d..50cdaa8918 100644 --- a/packages/core/src/features/navigation/reload-page/common/channel.ts +++ b/packages/core/src/features/navigation/reload-page/common/channel.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { MessageChannel } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import type { MessageChannel } from "@k8slens/messaging"; export type ReloadPageChannel = MessageChannel; diff --git a/packages/core/src/features/navigation/reload-page/renderer/register-listener.global-override-for-injectable.ts b/packages/core/src/features/navigation/reload-page/renderer/register-listener.global-override-for-injectable.ts index 9953ddcbf9..4b9842cf7d 100644 --- a/packages/core/src/features/navigation/reload-page/renderer/register-listener.global-override-for-injectable.ts +++ b/packages/core/src/features/navigation/reload-page/renderer/register-listener.global-override-for-injectable.ts @@ -8,6 +8,7 @@ import { reloadPageChannel } from "../common/channel"; import reloadPageChannelListenerInjectable from "./register-listener.injectable"; export default getGlobalOverride(reloadPageChannelListenerInjectable, () => ({ + id: "reload-page-channel-listener", channel: reloadPageChannel, handler: () => {}, })); diff --git a/packages/core/src/features/navigation/reload-page/renderer/register-listener.injectable.ts b/packages/core/src/features/navigation/reload-page/renderer/register-listener.injectable.ts index a42d818729..f71096bebf 100644 --- a/packages/core/src/features/navigation/reload-page/renderer/register-listener.injectable.ts +++ b/packages/core/src/features/navigation/reload-page/renderer/register-listener.injectable.ts @@ -2,13 +2,13 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import { getMessageChannelListenerInjectable } from "@k8slens/messaging"; import { reloadPageChannel } from "../common/channel"; const reloadPageChannelListenerInjectable = getMessageChannelListenerInjectable({ id: "handler", channel: reloadPageChannel, - handler: () => () => location.reload(), + getHandler: () => () => location.reload(), causesSideEffects: true, }); diff --git a/packages/core/src/features/path-picking-dialog/common/channel.ts b/packages/core/src/features/path-picking-dialog/common/channel.ts index 98c19b4ad6..c5e8b9386d 100644 --- a/packages/core/src/features/path-picking-dialog/common/channel.ts +++ b/packages/core/src/features/path-picking-dialog/common/channel.ts @@ -4,7 +4,7 @@ */ import type { OpenDialogOptions } from "electron"; -import type { RequestChannel } from "../../../common/utils/channel/request-channel-listener-injection-token"; +import type { RequestChannel } from "@k8slens/messaging"; export type PathPickingResponse = { canceled: true; diff --git a/packages/core/src/features/path-picking-dialog/main/handle-pick-paths.injectable.ts b/packages/core/src/features/path-picking-dialog/main/handle-pick-paths.injectable.ts index d566ac9728..dded293c83 100644 --- a/packages/core/src/features/path-picking-dialog/main/handle-pick-paths.injectable.ts +++ b/packages/core/src/features/path-picking-dialog/main/handle-pick-paths.injectable.ts @@ -4,12 +4,13 @@ */ import askUserForFilePathsInjectable from "../../../main/ipc/ask-user-for-file-paths.injectable"; -import { getRequestChannelListenerInjectable } from "../../../main/utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import { openPathPickingDialogChannel } from "../common/channel"; const openPathPickingDialogListener = getRequestChannelListenerInjectable({ + id: "open-path-picking-dialog", channel: openPathPickingDialogChannel, - handler: (di) => di.inject(askUserForFilePathsInjectable), + getHandler: (di) => di.inject(askUserForFilePathsInjectable), }); export default openPathPickingDialogListener; diff --git a/packages/core/src/features/path-picking-dialog/renderer/pick-paths.injectable.ts b/packages/core/src/features/path-picking-dialog/renderer/pick-paths.injectable.ts index 5dde31f46d..528bffc634 100644 --- a/packages/core/src/features/path-picking-dialog/renderer/pick-paths.injectable.ts +++ b/packages/core/src/features/path-picking-dialog/renderer/pick-paths.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import type { PathPickOpts } from "../../../renderer/components/path-picker"; -import requestFromChannelInjectable from "../../../renderer/utils/channel/request-from-channel.injectable"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import { openPathPickingDialogChannel } from "../common/channel"; export type OpenPathPickingDialog = (options: PathPickOpts) => Promise; @@ -12,7 +12,7 @@ export type OpenPathPickingDialog = (options: PathPickOpts) => Promise; const openPathPickingDialogInjectable = getInjectable({ id: "open-path-picking-dialog", instantiate: (di): OpenPathPickingDialog => { - const requestFromChannel = di.inject(requestFromChannelInjectable); + const requestFromChannel = di.inject(requestFromChannelInjectionToken); return async (options) => { const { onPick, onCancel, ...dialogOptions } = options; diff --git a/packages/core/src/features/shell-sync/common/failure-channel.ts b/packages/core/src/features/shell-sync/common/failure-channel.ts index aed25b0da0..268640fe89 100644 --- a/packages/core/src/features/shell-sync/common/failure-channel.ts +++ b/packages/core/src/features/shell-sync/common/failure-channel.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { MessageChannel } from "../../../common/utils/channel/message-channel-listener-injection-token"; +import type { MessageChannel } from "@k8slens/messaging"; export const shellSyncFailedChannel: MessageChannel = { id: "shell-sync-failed-channel", diff --git a/packages/core/src/features/shell-sync/main/emit-failure.injectable.ts b/packages/core/src/features/shell-sync/main/emit-failure.injectable.ts index 5abdfafa52..7dbfb094f2 100644 --- a/packages/core/src/features/shell-sync/main/emit-failure.injectable.ts +++ b/packages/core/src/features/shell-sync/main/emit-failure.injectable.ts @@ -3,16 +3,15 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { MessageChannelSender } from "../../../common/utils/channel/message-to-channel-injection-token"; -import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; +import { sendMessageToChannelInjectionToken } from "@k8slens/messaging"; import { shellSyncFailedChannel } from "../common/failure-channel"; const emitShellSyncFailedInjectable = getInjectable({ id: "emit-shell-sync-failed", - instantiate: (di): MessageChannelSender => { + instantiate: (di) => { const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); - return (error) => sendMessageToChannel(shellSyncFailedChannel, error); + return (error: string) => sendMessageToChannel(shellSyncFailedChannel, error); }, }); diff --git a/packages/core/src/features/shell-sync/renderer/failure-listener.injectable.ts b/packages/core/src/features/shell-sync/renderer/failure-listener.injectable.ts index 2014397fad..135143b3a6 100644 --- a/packages/core/src/features/shell-sync/renderer/failure-listener.injectable.ts +++ b/packages/core/src/features/shell-sync/renderer/failure-listener.injectable.ts @@ -2,14 +2,14 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { getMessageChannelListenerInjectable } from "../../../common/utils/channel/message-channel-listener-injection-token"; +import { getMessageChannelListenerInjectable } from "@k8slens/messaging"; import showErrorNotificationInjectable from "../../../renderer/components/notifications/show-error-notification.injectable"; import { shellSyncFailedChannel } from "../common/failure-channel"; const shellSyncFailureListenerInjectable = getMessageChannelListenerInjectable({ id: "notification", channel: shellSyncFailedChannel, - handler: (di) => { + getHandler: (di) => { const showErrorNotification = di.inject(showErrorNotificationInjectable); return (errorMessage) => showErrorNotification(`Failed to sync shell environment: ${errorMessage}`); diff --git a/packages/core/src/features/theme/system-type/common/channels.ts b/packages/core/src/features/theme/system-type/common/channels.ts index 3134f20378..49fb96ef0a 100644 --- a/packages/core/src/features/theme/system-type/common/channels.ts +++ b/packages/core/src/features/theme/system-type/common/channels.ts @@ -3,8 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { MessageChannel } from "../../../../common/utils/channel/message-channel-listener-injection-token"; -import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; +import type { MessageChannel, RequestChannel } from "@k8slens/messaging"; export type SystemThemeType = "dark" | "light"; diff --git a/packages/core/src/features/theme/system-type/main/emit-update.injectable.ts b/packages/core/src/features/theme/system-type/main/emit-update.injectable.ts index c085e6615a..d078e81e33 100644 --- a/packages/core/src/features/theme/system-type/main/emit-update.injectable.ts +++ b/packages/core/src/features/theme/system-type/main/emit-update.injectable.ts @@ -3,18 +3,16 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { MessageChannelHandler } from "../../../../common/utils/channel/message-channel-listener-injection-token"; -import { sendMessageToChannelInjectionToken } from "../../../../common/utils/channel/message-to-channel-injection-token"; +import { sendMessageToChannelInjectionToken } from "@k8slens/messaging"; +import type { SystemThemeType } from "../common/channels"; import { systemThemeTypeUpdateChannel } from "../common/channels"; -export type EmitSystemThemeTypeUpdate = MessageChannelHandler; - const emitSystemThemeTypeUpdateInjectable = getInjectable({ id: "emit-system-theme-type-update", - instantiate: (di): EmitSystemThemeTypeUpdate => { + instantiate: (di) => { const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); - return (type) => sendMessageToChannel(systemThemeTypeUpdateChannel, type); + return (type: SystemThemeType) => sendMessageToChannel(systemThemeTypeUpdateChannel, type); }, }); diff --git a/packages/core/src/features/theme/system-type/main/handle-initial.injectable.ts b/packages/core/src/features/theme/system-type/main/handle-initial.injectable.ts index 1468b1eeda..7af8a0a069 100644 --- a/packages/core/src/features/theme/system-type/main/handle-initial.injectable.ts +++ b/packages/core/src/features/theme/system-type/main/handle-initial.injectable.ts @@ -4,12 +4,13 @@ */ import operatingSystemThemeInjectable from "../../../../main/theme/operating-system-theme.injectable"; -import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import { initialSystemThemeTypeChannel } from "../common/channels"; const initialSystemThemeTypeHandler = getRequestChannelListenerInjectable({ + id: "initial-system-theme-type-listener", channel: initialSystemThemeTypeChannel, - handler: (di) => { + getHandler: (di) => { const operatingSystemTheme = di.inject(operatingSystemThemeInjectable); return () => operatingSystemTheme.get(); diff --git a/packages/core/src/features/theme/system-type/renderer/request-initial.injectable.ts b/packages/core/src/features/theme/system-type/renderer/request-initial.injectable.ts index 74fe73cf03..fd84d7f90c 100644 --- a/packages/core/src/features/theme/system-type/renderer/request-initial.injectable.ts +++ b/packages/core/src/features/theme/system-type/renderer/request-initial.injectable.ts @@ -3,8 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { RequestChannelHandler } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; -import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; +import type { RequestChannelHandler } from "@k8slens/messaging"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import { initialSystemThemeTypeChannel } from "../common/channels"; export type RequestInitialSystemThemeType = RequestChannelHandler; @@ -12,7 +12,7 @@ export type RequestInitialSystemThemeType = RequestChannelHandler { - const requestFromChannel = di.inject(requestFromChannelInjectable); + const requestFromChannel = di.inject(requestFromChannelInjectionToken); return () => requestFromChannel(initialSystemThemeTypeChannel); }, diff --git a/packages/core/src/features/theme/system-type/renderer/update-listener.injectable.ts b/packages/core/src/features/theme/system-type/renderer/update-listener.injectable.ts index 4eb57c9b85..dac9061c28 100644 --- a/packages/core/src/features/theme/system-type/renderer/update-listener.injectable.ts +++ b/packages/core/src/features/theme/system-type/renderer/update-listener.injectable.ts @@ -2,14 +2,14 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import { getMessageChannelListenerInjectable } from "@k8slens/messaging"; import systemThemeConfigurationInjectable from "../../../../renderer/themes/system-theme.injectable"; import { systemThemeTypeUpdateChannel } from "../common/channels"; const systemThemeTypeUpdateListenerInjectable = getMessageChannelListenerInjectable({ channel: systemThemeTypeUpdateChannel, id: "main", - handler: (di) => { + getHandler: (di) => { const systemThemeConfiguration = di.inject(systemThemeConfigurationInjectable); return (type) => systemThemeConfiguration.set(type); diff --git a/packages/core/src/jest-after-env.setup.ts b/packages/core/src/jest-after-env.setup.ts index 69b18161a8..df0fcfff4d 100644 --- a/packages/core/src/jest-after-env.setup.ts +++ b/packages/core/src/jest-after-env.setup.ts @@ -4,3 +4,8 @@ */ import "@testing-library/jest-dom"; + +// Note: This is a kludge to prevent "Hooks cannot be defined inside tests" error +// when importing a test util inside a test suite. +import { render } from "@testing-library/react"; +void render; diff --git a/packages/core/src/main/__test__/cluster.test.ts b/packages/core/src/main/__test__/cluster.test.ts index bb495af847..205a23d5ab 100644 --- a/packages/core/src/main/__test__/cluster.test.ts +++ b/packages/core/src/main/__test__/cluster.test.ts @@ -5,10 +5,6 @@ import { Cluster } from "../../common/cluster/cluster"; import { Kubectl } from "../kubectl/kubectl"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; -import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable"; -import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; -import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; @@ -19,6 +15,10 @@ import clusterConnectionInjectable from "../cluster/cluster-connection.injectabl import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable"; import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable"; +import createCanIInjectable from "../../common/cluster/create-can-i.injectable"; +import createRequestNamespaceListPermissionsInjectable from "../../common/cluster/create-request-namespace-list-permissions.injectable"; +import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; +import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable"; describe("create clusters", () => { let cluster: Cluster; @@ -34,8 +34,8 @@ describe("create clusters", () => { di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(normalizedPlatformInjectable, () => "darwin"); di.override(broadcastConnectionUpdateInjectable, () => async () => {}); - di.override(createAuthorizationReviewInjectable, () => () => () => Promise.resolve(true)); - di.override(requestNamespaceListPermissionsForInjectable, () => () => async () => () => true); + di.override(createCanIInjectable, () => () => () => Promise.resolve(true)); + di.override(createRequestNamespaceListPermissionsInjectable, () => () => async () => () => true); di.override(createListNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); di.override(prometheusHandlerInjectable, () => ({ getPrometheusDetails: jest.fn(), diff --git a/packages/core/src/main/__test__/context-handler.test.ts b/packages/core/src/main/__test__/context-handler.test.ts deleted file mode 100644 index 61bd12398d..0000000000 --- a/packages/core/src/main/__test__/context-handler.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import { Cluster } from "../../common/cluster/cluster"; -import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; -import type { DiContainer } from "@ogre-tools/injectable"; -import { getInjectable } from "@ogre-tools/injectable"; -import type { PrometheusProvider } from "../prometheus/provider"; -import { prometheusProviderInjectionToken } from "../prometheus/provider"; -import { runInAction } from "mobx"; -import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable"; -import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; -import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable"; -import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; -import loadProxyKubeconfigInjectable from "../cluster/load-proxy-kubeconfig.injectable"; -import type { KubeConfig } from "@kubernetes/client-node"; - -enum ServiceResult { - Success, - Failure, -} - -const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult): PrometheusProvider => ({ - kind, - name: "TestProvider1", - isConfigurable: false, - getQuery: () => { - throw new Error("getQuery is not implemented."); - }, - getPrometheusService: async () => { - switch (alwaysFail) { - case ServiceResult.Success: - return { - kind, - namespace: "default", - port: 7000, - service: "", - }; - case ServiceResult.Failure: - throw new Error("does fail"); - } - }, -}); - -describe("ContextHandler", () => { - let di: DiContainer; - let cluster: Cluster; - - beforeEach(() => { - di = getDiForUnitTesting(); - - di.override(loadProxyKubeconfigInjectable, () => async () => ({ - makeApiClient: () => ({} as any), - } as Partial)); - - di.override(createKubeAuthProxyInjectable, () => () => ({ - run: async () => {}, - } as KubeAuthProxy)); - di.override(directoryForTempInjectable, () => "/some-directory-for-tmp"); - di.inject(lensProxyPortInjectable).set(9968); - - cluster = new Cluster({ - contextName: "some-context-name", - id: "some-cluster-id", - kubeConfigPath: "/some-kubeconfig-path", - }, { - clusterServerUrl: "https://some-website.com", - }); - }); - - describe("getPrometheusService", () => { - it.each([ - [0], - [1], - [2], - [3], - ])("should throw after %d failure(s)", async (failures) => { - runInAction(() => { - for (let i = 0; i < failures; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-failure-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), - })); - } - }); - - expect(() => di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails()).rejects.toThrowError(); - }); - - it.each([ - [1, 0], - [1, 1], - [1, 2], - [1, 3], - [2, 0], - [2, 1], - [2, 2], - [2, 3], - ])("should pick the first provider of %d success(es) after %d failure(s)", async (successes, failures) => { - runInAction(() => { - for (let i = 0; i < failures; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-failure-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), - })); - } - - for (let i = 0; i < successes; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-success-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), - })); - } - }); - - const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails(); - - expect(details.provider.kind === `id_failure_${failures}`); - }); - - it.each([ - [1, 0], - [1, 1], - [1, 2], - [1, 3], - [2, 0], - [2, 1], - [2, 2], - [2, 3], - ])("should pick the first provider of %d success(es) before %d failure(s)", async (successes, failures) => { - runInAction(() => { - for (let i = 0; i < failures; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-failure-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), - })); - } - - for (let i = 0; i < successes; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-success-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), - })); - } - }); - - const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails(); - - expect(details.provider.kind === "id_failure_0"); - }); - - it.each([ - [1, 0], - [1, 1], - [1, 2], - [1, 3], - [2, 0], - [2, 1], - [2, 2], - [2, 3], - ])("should pick the first provider of %d success(es) between %d failure(s)", async (successes, failures) => { - const beforeSuccesses = Math.floor(successes / 2); - - runInAction(() => { - for (let i = 0; i < beforeSuccesses; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-success-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), - })); - } - - for (let i = 0; i < failures; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-failure-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), - })); - } - - for (let i = beforeSuccesses; i < successes; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-success-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), - })); - } - }); - - const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails(); - - expect(details.provider.kind === "id_success_0"); - }); - }); -}); diff --git a/packages/core/src/main/__test__/kube-auth-proxy.test.ts b/packages/core/src/main/__test__/kube-auth-proxy.test.ts index 8fa8a175a5..dc4878f806 100644 --- a/packages/core/src/main/__test__/kube-auth-proxy.test.ts +++ b/packages/core/src/main/__test__/kube-auth-proxy.test.ts @@ -5,7 +5,6 @@ import waitUntilPortIsUsedInjectable from "../kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable"; import { Cluster } from "../../common/cluster/cluster"; -import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; import type { ChildProcess } from "child_process"; import { Kubectl } from "../kubectl/kubectl"; import type { DeepMockProxy } from "jest-mock-extended"; @@ -13,6 +12,7 @@ import { mockDeep, mock } from "jest-mock-extended"; import type { Readable } from "stream"; import { EventEmitter } from "stream"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import type { CreateKubeAuthProxy, KubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import spawnInjectable from "../child-process/spawn.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; @@ -29,7 +29,7 @@ import getBasenameOfPathInjectable from "../../common/path/get-basename.injectab const clusterServerUrl = "https://192.168.64.3:8443"; describe("kube auth proxy tests", () => { - let createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; + let createKubeAuthProxy: CreateKubeAuthProxy; let spawnMock: jest.Mock; let waitUntilPortIsUsedMock: jest.Mock; let broadcastMessageMock: jest.Mock; diff --git a/packages/core/src/main/__test__/prometheus-handler.test.ts b/packages/core/src/main/__test__/prometheus-handler.test.ts index 59671553bb..74cab1ea74 100644 --- a/packages/core/src/main/__test__/prometheus-handler.test.ts +++ b/packages/core/src/main/__test__/prometheus-handler.test.ts @@ -43,7 +43,7 @@ const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult): }, }); -describe("ContextHandler", () => { +describe("PrometheusHandler", () => { let di: DiContainer; let cluster: Cluster; diff --git a/packages/core/src/main/app-paths/app-paths-request-channel-listener.injectable.ts b/packages/core/src/main/app-paths/app-paths-request-channel-listener.injectable.ts index 568d63f1ea..ccf8be5054 100644 --- a/packages/core/src/main/app-paths/app-paths-request-channel-listener.injectable.ts +++ b/packages/core/src/main/app-paths/app-paths-request-channel-listener.injectable.ts @@ -4,11 +4,12 @@ */ import { appPathsChannel } from "../../common/app-paths/app-paths-channel"; import appPathsInjectable from "../../common/app-paths/app-paths.injectable"; -import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; const appPathsRequestChannelListenerInjectable = getRequestChannelListenerInjectable({ + id: "app-paths-request-channel-listener", channel: appPathsChannel, - handler: (di) => { + getHandler: (di) => { const appPaths = di.inject(appPathsInjectable); return () => appPaths; diff --git a/packages/core/src/main/app-paths/setup-app-paths.injectable.ts b/packages/core/src/main/app-paths/setup-app-paths.injectable.ts index 34178547c3..255beffae0 100644 --- a/packages/core/src/main/app-paths/setup-app-paths.injectable.ts +++ b/packages/core/src/main/app-paths/setup-app-paths.injectable.ts @@ -34,6 +34,7 @@ const setupAppPathsInjectable = getInjectable({ const appDataPath = getElectronAppPath("appData"); setElectronAppPath("userData", joinPaths(appDataPath, appName)); + setElectronAppPath("sessionData", getElectronAppPath("userData")); const appPaths = pipeline( pathNames, diff --git a/packages/core/src/main/build-version/setup-channel.injectable.ts b/packages/core/src/main/build-version/setup-channel.injectable.ts index 05da92db6e..88661c899f 100644 --- a/packages/core/src/main/build-version/setup-channel.injectable.ts +++ b/packages/core/src/main/build-version/setup-channel.injectable.ts @@ -3,12 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { buildVersionChannel } from "../../common/vars/build-semantic-version.injectable"; -import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import buildVersionInjectable from "../vars/build-version/build-version.injectable"; const buildVersionChannelListenerInjectable = getRequestChannelListenerInjectable({ + id: "build-version-channel-listener", channel: buildVersionChannel, - handler: (di) => { + getHandler: (di) => { const buildVersion = di.inject(buildVersionInjectable); return () => buildVersion.get(); diff --git a/packages/core/src/main/catalog-sync-to-renderer/catalog-sync-to-renderer.injectable.ts b/packages/core/src/main/catalog-sync-to-renderer/catalog-sync-to-renderer.injectable.ts index cd77332185..a2fe077c1d 100644 --- a/packages/core/src/main/catalog-sync-to-renderer/catalog-sync-to-renderer.injectable.ts +++ b/packages/core/src/main/catalog-sync-to-renderer/catalog-sync-to-renderer.injectable.ts @@ -7,7 +7,7 @@ import { reaction } from "mobx"; import ipcMainInjectionToken from "../../common/ipc/ipc-main-injection-token"; import { catalogInitChannel } from "../../common/ipc/catalog"; import { disposer } from "@k8slens/utilities"; -import { getStartableStoppable } from "../../common/utils/get-startable-stoppable"; +import { getStartableStoppable } from "@k8slens/startable-stoppable"; import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable"; import catalogSyncBroadcasterInjectable from "./broadcaster.injectable"; import { toJS } from "../../common/utils"; diff --git a/packages/core/src/main/cluster/request-api-versions.ts b/packages/core/src/main/cluster/api-versions-requester.ts similarity index 64% rename from packages/core/src/main/cluster/request-api-versions.ts rename to packages/core/src/main/cluster/api-versions-requester.ts index a9b5074549..0bb2c65f6c 100644 --- a/packages/core/src/main/cluster/request-api-versions.ts +++ b/packages/core/src/main/cluster/api-versions-requester.ts @@ -15,8 +15,11 @@ export interface ClusterData { readonly id: string; } -export type RequestApiVersions = (cluster: ClusterData) => AsyncResult; +export interface ApiVersionsRequester { + request(cluster: ClusterData): AsyncResult; + readonly orderNumber: number; +} -export const requestApiVersionsInjectionToken = getInjectionToken({ +export const apiVersionsRequesterInjectionToken = getInjectionToken({ id: "request-api-versions-token", }); diff --git a/packages/core/src/main/cluster/cluster-connection.injectable.ts b/packages/core/src/main/cluster/cluster-connection.injectable.ts index c5d9a5f165..3f40898fc7 100644 --- a/packages/core/src/main/cluster/cluster-connection.injectable.ts +++ b/packages/core/src/main/cluster/cluster-connection.injectable.ts @@ -6,10 +6,7 @@ import { type KubeConfig, HttpError } from "@kubernetes/client-node"; import { reaction, comparer, runInAction } from "mobx"; import { ClusterStatus } from "../../common/cluster-types"; -import type { CreateAuthorizationReview } from "../../common/cluster/authorization-review.injectable"; -import type { Cluster } from "../../common/cluster/cluster"; import type { CreateListNamespaces } from "../../common/cluster/list-namespaces.injectable"; -import type { RequestNamespaceListPermissionsFor, RequestNamespaceListPermissions } from "../../common/cluster/request-namespace-list-permissions.injectable"; import type { BroadcastMessage } from "../../common/ipc/broadcast-message.injectable"; import { clusterListNamespaceForbiddenChannel } from "../../common/ipc/cluster"; import type { Logger } from "../../common/logger"; @@ -25,7 +22,6 @@ import type { RequestApiResources } from "./request-api-resources.injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable"; import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; -import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; import kubeAuthProxyServerInjectable from "./kube-auth-proxy-server.injectable"; import loadProxyKubeconfigInjectable from "./load-proxy-kubeconfig.injectable"; @@ -33,21 +29,31 @@ import loggerInjectable from "../../common/logger.injectable"; import prometheusHandlerInjectable from "./prometheus-handler/prometheus-handler.injectable"; import removeProxyKubeconfigInjectable from "./remove-proxy-kubeconfig.injectable"; import requestApiResourcesInjectable from "./request-api-resources.injectable"; -import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable"; import type { DetectClusterMetadata } from "../cluster-detectors/detect-cluster-metadata.injectable"; import type { FallibleOnlyClusterMetadataDetector } from "../cluster-detectors/token"; import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable"; import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable"; import { replaceObservableObject } from "../../common/utils/replace-observable-object"; +import type { CreateAuthorizationApi } from "../../common/cluster/create-authorization-api.injectable"; +import type { CreateCanI } from "../../common/cluster/create-can-i.injectable"; +import type { CreateRequestNamespaceListPermissions, RequestNamespaceListPermissions } from "../../common/cluster/create-request-namespace-list-permissions.injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import createAuthorizationApiInjectable from "../../common/cluster/create-authorization-api.injectable"; +import createCanIInjectable from "../../common/cluster/create-can-i.injectable"; +import createRequestNamespaceListPermissionsInjectable from "../../common/cluster/create-request-namespace-list-permissions.injectable"; +import type { CreateCoreApi } from "../../common/cluster/create-core-api.injectable"; +import createCoreApiInjectable from "../../common/cluster/create-core-api.injectable"; interface Dependencies { readonly logger: Logger; readonly prometheusHandler: ClusterPrometheusHandler; readonly kubeAuthProxyServer: KubeAuthProxyServer; readonly clusterVersionDetector: FallibleOnlyClusterMetadataDetector; - createAuthorizationReview: CreateAuthorizationReview; + createCanI: CreateCanI; requestApiResources: RequestApiResources; - requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor; + createRequestNamespaceListPermissions: CreateRequestNamespaceListPermissions; + createAuthorizationApi: CreateAuthorizationApi; + createCoreApi: CreateCoreApi; createListNamespaces: CreateListNamespaces; detectClusterMetadata: DetectClusterMetadata; broadcastMessage: BroadcastMessage; @@ -224,8 +230,9 @@ class ClusterConnection { private async refreshAccessibility(): Promise { this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.cluster.getMeta()); const proxyConfig = await this.dependencies.loadProxyKubeconfig(); - const canI = this.dependencies.createAuthorizationReview(proxyConfig); - const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig); + const api = this.dependencies.createAuthorizationApi(proxyConfig); + const canI = this.dependencies.createCanI(api); + const requestNamespaceListPermissions = this.dependencies.createRequestNamespaceListPermissions(api); const isAdmin = await canI({ namespace: "kube-system", @@ -360,7 +367,8 @@ class ClusterConnection { } try { - const listNamespaces = this.dependencies.createListNamespaces(proxyConfig); + const api = this.dependencies.createCoreApi(proxyConfig); + const listNamespaces = this.dependencies.createListNamespaces(api); return await listNamespaces(); } catch (error) { @@ -403,13 +411,15 @@ const clusterConnectionInjectable = getInjectable({ prometheusHandler: di.inject(prometheusHandlerInjectable, cluster), broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster), broadcastMessage: di.inject(broadcastMessageInjectable), - createAuthorizationReview: di.inject(createAuthorizationReviewInjectable), createListNamespaces: di.inject(createListNamespacesInjectable), detectClusterMetadata: di.inject(detectClusterMetadataInjectable), loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster), removeProxyKubeconfig: di.inject(removeProxyKubeconfigInjectable, cluster), requestApiResources: di.inject(requestApiResourcesInjectable), - requestNamespaceListPermissionsFor: di.inject(requestNamespaceListPermissionsForInjectable), + createAuthorizationApi: di.inject(createAuthorizationApiInjectable), + createCoreApi: di.inject(createCoreApiInjectable), + createCanI: di.inject(createCanIInjectable), + createRequestNamespaceListPermissions: di.inject(createRequestNamespaceListPermissionsInjectable), }, cluster, ), diff --git a/packages/core/src/main/cluster/kube-auth-proxy-server.injectable.ts b/packages/core/src/main/cluster/kube-auth-proxy-server.injectable.ts index 9942c40168..ff70af3a7b 100644 --- a/packages/core/src/main/cluster/kube-auth-proxy-server.injectable.ts +++ b/packages/core/src/main/cluster/kube-auth-proxy-server.injectable.ts @@ -8,7 +8,7 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import type { Cluster } from "../../common/cluster/cluster"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import kubeAuthProxyCertificateInjectable from "../kube-auth-proxy/kube-auth-proxy-certificate.injectable"; -import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; +import type { KubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; export interface KubeAuthProxyServer { getApiTarget(isLongRunningRequest?: boolean): Promise; diff --git a/packages/core/src/main/cluster/request-api-resources.injectable.ts b/packages/core/src/main/cluster/request-api-resources.injectable.ts index d6a21e3ce4..14776bb93a 100644 --- a/packages/core/src/main/cluster/request-api-resources.injectable.ts +++ b/packages/core/src/main/cluster/request-api-resources.injectable.ts @@ -7,11 +7,12 @@ import { getInjectable } from "@ogre-tools/injectable"; import loggerInjectable from "../../common/logger.injectable"; import type { KubeApiResource } from "../../common/rbac"; import type { Cluster } from "../../common/cluster/cluster"; -import { requestApiVersionsInjectionToken } from "./request-api-versions"; +import { apiVersionsRequesterInjectionToken } from "./api-versions-requester"; import { backoffCaller, withConcurrencyLimit } from "@k8slens/utilities"; import requestKubeApiResourcesForInjectable from "./request-kube-api-resources-for.injectable"; import type { AsyncResult } from "@k8slens/utilities"; import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable"; +import { byOrderNumber } from "../../common/utils/composable-responsibilities/orderable/orderable"; export type RequestApiResources = (cluster: Cluster) => AsyncResult; @@ -24,7 +25,8 @@ const requestApiResourcesInjectable = getInjectable({ id: "request-api-resources", instantiate: (di): RequestApiResources => { const logger = di.inject(loggerInjectable); - const apiVersionRequesters = di.injectMany(requestApiVersionsInjectionToken); + const apiVersionRequesters = di.injectMany(apiVersionsRequesterInjectionToken) + .sort(byOrderNumber); const requestKubeApiResourcesFor = di.inject(requestKubeApiResourcesForInjectable); return async (...args) => { @@ -35,7 +37,7 @@ const requestApiResourcesInjectable = getInjectable({ const groupLists: KubeResourceListGroup[] = []; for (const apiVersionRequester of apiVersionRequesters) { - const result = await backoffCaller(() => apiVersionRequester(cluster), { + const result = await backoffCaller(() => apiVersionRequester.request(cluster), { onIntermediateError: (error, attempt) => { broadcastConnectionUpdate({ message: `Failed to list kube API resource kinds, attempt ${attempt}: ${error}`, diff --git a/packages/core/src/main/cluster/request-core-api-versions.injectable.ts b/packages/core/src/main/cluster/request-core-api-versions.injectable.ts index b709eb9356..5b059d6f7f 100644 --- a/packages/core/src/main/cluster/request-core-api-versions.injectable.ts +++ b/packages/core/src/main/cluster/request-core-api-versions.injectable.ts @@ -5,33 +5,36 @@ import type { V1APIVersions } from "@kubernetes/client-node"; import { getInjectable } from "@ogre-tools/injectable"; import k8sRequestInjectable from "../k8s-request.injectable"; -import { requestApiVersionsInjectionToken } from "./request-api-versions"; +import { apiVersionsRequesterInjectionToken } from "./api-versions-requester"; const requestCoreApiVersionsInjectable = getInjectable({ id: "request-core-api-versions", instantiate: (di) => { const k8sRequest = di.inject(k8sRequestInjectable); - return async (cluster) => { - try { - const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions; + return { + request: async (cluster) => { + try { + const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions; - return { - callWasSuccessful: true, - response: versions.map(version => ({ - group: "", - path: `/api/${version}`, - })), - }; - } catch (error) { - return { - callWasSuccessful: false, - error: error as Error, - }; - } + return { + callWasSuccessful: true, + response: versions.map(version => ({ + group: "", + path: `/api/${version}`, + })), + }; + } catch (error) { + return { + callWasSuccessful: false, + error: error as Error, + }; + } + }, + orderNumber: 10, }; }, - injectionToken: requestApiVersionsInjectionToken, + injectionToken: apiVersionsRequesterInjectionToken, }); export default requestCoreApiVersionsInjectable; diff --git a/packages/core/src/main/cluster/request-kube-api-resources-for.injectable.ts b/packages/core/src/main/cluster/request-kube-api-resources-for.injectable.ts index 65e326eed7..80720a9ef1 100644 --- a/packages/core/src/main/cluster/request-kube-api-resources-for.injectable.ts +++ b/packages/core/src/main/cluster/request-kube-api-resources-for.injectable.ts @@ -8,7 +8,7 @@ import type { Cluster } from "../../common/cluster/cluster"; import type { KubeApiResource } from "../../common/rbac"; import type { AsyncResult } from "@k8slens/utilities"; import k8sRequestInjectable from "../k8s-request.injectable"; -import type { KubeResourceListGroup } from "./request-api-versions"; +import type { KubeResourceListGroup } from "./api-versions-requester"; export type RequestKubeApiResources = (grouping: KubeResourceListGroup) => AsyncResult; diff --git a/packages/core/src/main/cluster/request-non-core-api-versions.injectable.ts b/packages/core/src/main/cluster/request-non-core-api-versions.injectable.ts index 84e62f6b80..7e5abbc44d 100644 --- a/packages/core/src/main/cluster/request-non-core-api-versions.injectable.ts +++ b/packages/core/src/main/cluster/request-non-core-api-versions.injectable.ts @@ -6,35 +6,40 @@ import type { V1APIGroupList } from "@kubernetes/client-node"; import { getInjectable } from "@ogre-tools/injectable"; import { iter } from "@k8slens/utilities"; import k8sRequestInjectable from "../k8s-request.injectable"; -import { requestApiVersionsInjectionToken } from "./request-api-versions"; +import { apiVersionsRequesterInjectionToken } from "./api-versions-requester"; const requestNonCoreApiVersionsInjectable = getInjectable({ id: "request-non-core-api-versions", instantiate: (di) => { const k8sRequest = di.inject(k8sRequestInjectable); - return async (cluster) => { - try { - const { groups } = await k8sRequest(cluster, "/apis") as V1APIGroupList; + return { + request: async (cluster) => { + try { + const { groups } = (await k8sRequest(cluster, "/apis")) as V1APIGroupList; - return { - callWasSuccessful: true, - response: iter.chain(groups.values()) - .flatMap(group => group.versions.map(version => ({ - group: group.name, - path: `/apis/${version.groupVersion}`, - }))) - .collect(v => [...v]), - }; - } catch (error) { - return { - callWasSuccessful: false, - error: error as Error, - }; - } + return { + callWasSuccessful: true, + response: iter.chain(groups.values()) + .flatMap((group) => + group.versions.map((version) => ({ + group: group.name, + path: `/apis/${version.groupVersion}`, + })), + ) + .collect((v) => [...v]), + }; + } catch (error) { + return { + callWasSuccessful: false, + error: error as Error, + }; + } + }, + orderNumber: 20, }; }, - injectionToken: requestApiVersionsInjectionToken, + injectionToken: apiVersionsRequesterInjectionToken, }); export default requestNonCoreApiVersionsInjectable; diff --git a/packages/core/src/main/cluster/request-non-core-api-versions.test.ts b/packages/core/src/main/cluster/request-non-core-api-versions.test.ts index c38359b6a5..ca4dcd01b7 100644 --- a/packages/core/src/main/cluster/request-non-core-api-versions.test.ts +++ b/packages/core/src/main/cluster/request-non-core-api-versions.test.ts @@ -10,13 +10,13 @@ import type { DiContainer } from "@ogre-tools/injectable"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; import type { K8sRequest } from "../k8s-request.injectable"; import k8sRequestInjectable from "../k8s-request.injectable"; -import type { RequestApiVersions } from "./request-api-versions"; +import type { ApiVersionsRequester } from "./api-versions-requester"; import requestNonCoreApiVersionsInjectable from "./request-non-core-api-versions.injectable"; describe("requestNonCoreApiVersions", () => { let di: DiContainer; let k8sRequestMock: AsyncFnMock; - let requestNonCoreApiVersions: RequestApiVersions; + let requestNonCoreApiVersions: ApiVersionsRequester; beforeEach(() => { di = getDiForUnitTesting(); @@ -28,10 +28,10 @@ describe("requestNonCoreApiVersions", () => { }); describe("when called", () => { - let versionsRequest: ReturnType; + let versionsRequest: ReturnType; beforeEach(() => { - versionsRequest = requestNonCoreApiVersions({ id: "some-cluster-id" }); + versionsRequest = requestNonCoreApiVersions.request({ id: "some-cluster-id" }); }); it("should request all api groups", () => { diff --git a/packages/core/src/main/cluster/visibility-handler.injectable.ts b/packages/core/src/main/cluster/visibility-handler.injectable.ts index 31f6ff13ec..ef4277e905 100644 --- a/packages/core/src/main/cluster/visibility-handler.injectable.ts +++ b/packages/core/src/main/cluster/visibility-handler.injectable.ts @@ -3,13 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { clusterVisibilityChannel } from "../../common/cluster/visibility-channel"; -import { getMessageChannelListenerInjectable } from "../../common/utils/channel/message-channel-listener-injection-token"; +import { getMessageChannelListenerInjectable } from "@k8slens/messaging"; import visibleClusterInjectable from "./visible-cluster.injectable"; const clusterVisibilityHandlerInjectable = getMessageChannelListenerInjectable({ channel: clusterVisibilityChannel, id: "base", - handler: (di) => { + getHandler: (di) => { const visibleCluster = di.inject(visibleClusterInjectable); return (clusterId) => visibleCluster.set(clusterId); diff --git a/packages/core/src/main/electron-app/features/sync-theme-from-operating-system.injectable.ts b/packages/core/src/main/electron-app/features/sync-theme-from-operating-system.injectable.ts index 818bf938ec..02f0c916b6 100644 --- a/packages/core/src/main/electron-app/features/sync-theme-from-operating-system.injectable.ts +++ b/packages/core/src/main/electron-app/features/sync-theme-from-operating-system.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable"; +import { getStartableStoppable } from "@k8slens/startable-stoppable"; import operatingSystemThemeStateInjectable from "../../theme/operating-system-theme-state.injectable"; import nativeThemeInjectable from "./native-theme.injectable"; import getElectronThemeInjectable from "./get-electron-theme.injectable"; diff --git a/packages/core/src/main/getDiForUnitTesting.ts b/packages/core/src/main/getDiForUnitTesting.ts index b71ab53239..b374eec1ca 100644 --- a/packages/core/src/main/getDiForUnitTesting.ts +++ b/packages/core/src/main/getDiForUnitTesting.ts @@ -22,7 +22,6 @@ import electronQuitAndInstallUpdateInjectable from "./electron-app/features/elec import electronUpdaterIsActiveInjectable from "./electron-app/features/electron-updater-is-active.injectable"; import setUpdateOnQuitInjectable from "./electron-app/features/set-update-on-quit.injectable"; import waitUntilBundledExtensionsAreLoadedInjectable from "./start-main-application/lens-window/application-window/wait-until-bundled-extensions-are-loaded.injectable"; -import electronInjectable from "./utils/resolve-system-proxy/electron.injectable"; import initializeClusterManagerInjectable from "./cluster/initialize-manager.injectable"; import type { GlobalOverride } from "@k8slens/test-utils"; import { getOverrideFsWithFakes } from "../test-utils/override-fs-with-fakes"; @@ -30,6 +29,8 @@ import { setLegacyGlobalDiForExtensionApi, } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; +import { registerFeature } from "@k8slens/feature-core"; +import { messagingFeature, testUtils as messagingTestUtils } from "@k8slens/messaging"; export function getDiForUnitTesting() { const di = createContainer("main"); @@ -37,6 +38,11 @@ export function getDiForUnitTesting() { registerMobX(di); setLegacyGlobalDiForExtensionApi(di, "main"); + runInAction(() => { + registerFeature(di, messagingFeature, messagingTestUtils.messagingFeatureForUnitTesting); + + }); + di.preventSideEffects(); runInAction(() => { @@ -56,7 +62,6 @@ export function getDiForUnitTesting() { di.override(globalOverride.injectable, globalOverride.overridingInstantiate); } - di.override(electronInjectable, () => ({})); di.override(waitUntilBundledExtensionsAreLoadedInjectable, () => async () => {}); overrideRunnablesHavingSideEffects(di); diff --git a/packages/core/src/main/helm/repositories/add-helm-repository/add-helm-repository-channel-listener.injectable.ts b/packages/core/src/main/helm/repositories/add-helm-repository/add-helm-repository-channel-listener.injectable.ts index 1c68bf6fd7..d5343b1d4f 100644 --- a/packages/core/src/main/helm/repositories/add-helm-repository/add-helm-repository-channel-listener.injectable.ts +++ b/packages/core/src/main/helm/repositories/add-helm-repository/add-helm-repository-channel-listener.injectable.ts @@ -3,12 +3,14 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { addHelmRepositoryChannel } from "../../../../common/helm/add-helm-repository-channel"; -import { getRequestChannelListenerInjectable } from "../../../utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import addHelmRepositoryInjectable from "./add-helm-repository.injectable"; -const addHelmRepositoryChannelListenerInjectable = getRequestChannelListenerInjectable({ - channel: addHelmRepositoryChannel, - handler: (di) => di.inject(addHelmRepositoryInjectable), -}); +const addHelmRepositoryChannelListenerInjectable = + getRequestChannelListenerInjectable({ + id: "add-helm-repository-channel-listener", + channel: addHelmRepositoryChannel, + getHandler: (di) => di.inject(addHelmRepositoryInjectable), + }); export default addHelmRepositoryChannelListenerInjectable; diff --git a/packages/core/src/main/helm/repositories/add-helm-repository/add-helm-repository.injectable.ts b/packages/core/src/main/helm/repositories/add-helm-repository/add-helm-repository.injectable.ts index 5292f8a662..27272a76f3 100644 --- a/packages/core/src/main/helm/repositories/add-helm-repository/add-helm-repository.injectable.ts +++ b/packages/core/src/main/helm/repositories/add-helm-repository/add-helm-repository.injectable.ts @@ -5,17 +5,16 @@ import { getInjectable } from "@ogre-tools/injectable"; import execHelmInjectable from "../../exec-helm/exec-helm.injectable"; import loggerInjectable from "../../../../common/logger.injectable"; -import type { AddHelmRepositoryChannel } from "../../../../common/helm/add-helm-repository-channel"; -import type { RequestChannelHandler } from "../../../utils/channel/channel-listeners/listener-tokens"; +import type { HelmRepo } from "../../../../common/helm/helm-repo"; const addHelmRepositoryInjectable = getInjectable({ id: "add-helm-repository", - instantiate: (di): RequestChannelHandler => { + instantiate: (di) => { const execHelm = di.inject(execHelmInjectable); const logger = di.inject(loggerInjectable); - return async (repo) => { + return async (repo: HelmRepo) => { const { name, url, @@ -59,12 +58,12 @@ const addHelmRepositoryInjectable = getInjectable({ if (result.callWasSuccessful) { return { - callWasSuccessful: true, + callWasSuccessful: true as const, }; } return { - callWasSuccessful: false, + callWasSuccessful: false as const, error: result.error.stderr || result.error.message, }; }; diff --git a/packages/core/src/main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories-channel-listener.injectable.ts b/packages/core/src/main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories-channel-listener.injectable.ts index 5ef0f2b5a4..a3cd9f2529 100644 --- a/packages/core/src/main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories-channel-listener.injectable.ts +++ b/packages/core/src/main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories-channel-listener.injectable.ts @@ -3,12 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getActiveHelmRepositoriesChannel } from "../../../../common/helm/get-active-helm-repositories-channel"; -import { getRequestChannelListenerInjectable } from "../../../utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import getActiveHelmRepositoriesInjectable from "./get-active-helm-repositories.injectable"; const getActiveHelmRepositoriesChannelListenerInjectable = getRequestChannelListenerInjectable({ + id: "get-active-helm-repositories-channel-listener", channel: getActiveHelmRepositoriesChannel, - handler: (di) => di.inject(getActiveHelmRepositoriesInjectable), + getHandler: (di) => di.inject(getActiveHelmRepositoriesInjectable), }); export default getActiveHelmRepositoriesChannelListenerInjectable; diff --git a/packages/core/src/main/helm/repositories/remove-helm-repository/remove-helm-repository-channel-listener.injectable.ts b/packages/core/src/main/helm/repositories/remove-helm-repository/remove-helm-repository-channel-listener.injectable.ts index 8a4e04b87c..84b147fed1 100644 --- a/packages/core/src/main/helm/repositories/remove-helm-repository/remove-helm-repository-channel-listener.injectable.ts +++ b/packages/core/src/main/helm/repositories/remove-helm-repository/remove-helm-repository-channel-listener.injectable.ts @@ -4,11 +4,12 @@ */ import removeHelmRepositoryInjectable from "./remove-helm-repository.injectable"; import { removeHelmRepositoryChannel } from "../../../../common/helm/remove-helm-repository-channel"; -import { getRequestChannelListenerInjectable } from "../../../utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; const removeHelmRepositoryChannelListenerInjectable = getRequestChannelListenerInjectable({ + id: "remove-helm-repository-channel-listener", channel: removeHelmRepositoryChannel, - handler: (di) => di.inject(removeHelmRepositoryInjectable), + getHandler: (di) => di.inject(removeHelmRepositoryInjectable), }); export default removeHelmRepositoryChannelListenerInjectable; diff --git a/packages/core/src/main/ipc/ask-user-for-file-paths.injectable.ts b/packages/core/src/main/ipc/ask-user-for-file-paths.injectable.ts index f8a0685d7c..3d0326711e 100644 --- a/packages/core/src/main/ipc/ask-user-for-file-paths.injectable.ts +++ b/packages/core/src/main/ipc/ask-user-for-file-paths.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable"; -import type { RequestChannelHandler } from "../utils/channel/channel-listeners/listener-tokens"; +import type { RequestChannelHandler } from "@k8slens/messaging"; import type { openPathPickingDialogChannel } from "../../features/path-picking-dialog/common/channel"; import showOpenDialogInjectable from "../electron-app/features/show-open-dialog.injectable"; diff --git a/packages/core/src/main/k8s-request.injectable.ts b/packages/core/src/main/k8s-request.injectable.ts index 70ca3c3b6e..efa9df1fe7 100644 --- a/packages/core/src/main/k8s-request.injectable.ts +++ b/packages/core/src/main/k8s-request.injectable.ts @@ -34,8 +34,8 @@ const k8sRequestInjectable = getInjectable({ ) => { const controller = timeout ? withTimeout(timeout) : undefined; - if (controller) { - signal?.addEventListener("abort", () => controller.abort()); + if (controller && signal) { + signal.addEventListener("abort", () => controller.abort()); } const response = await lensFetch(`/${cluster.id}${pathnameAndQuery}`, { diff --git a/packages/core/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts b/packages/core/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts index 56260347bf..fd4acf0d7a 100644 --- a/packages/core/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts +++ b/packages/core/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import type { KubeAuthProxyDependencies } from "./kube-auth-proxy"; -import { KubeAuthProxy } from "./kube-auth-proxy"; +import { KubeAuthProxyImpl } from "./kube-auth-proxy"; import type { Cluster } from "../../common/cluster/cluster"; import spawnInjectable from "../child-process/spawn.injectable"; import kubeAuthProxyCertificateInjectable from "./kube-auth-proxy-certificate.injectable"; @@ -15,6 +15,13 @@ import getPortFromStreamInjectable from "../utils/get-port-from-stream.injectabl import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable"; +export interface KubeAuthProxy { + readonly apiPrefix: string; + readonly port: number; + run: () => Promise; + exit: () => void; +} + export type CreateKubeAuthProxy = (cluster: Cluster, env: NodeJS.ProcessEnv) => KubeAuthProxy; const createKubeAuthProxyInjectable = getInjectable({ @@ -33,7 +40,7 @@ const createKubeAuthProxyInjectable = getInjectable({ return (cluster, env) => { const clusterUrl = new URL(cluster.apiUrl.get()); - return new KubeAuthProxy({ + return new KubeAuthProxyImpl({ ...dependencies, proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname), broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster), diff --git a/packages/core/src/main/kube-auth-proxy/kube-auth-proxy.ts b/packages/core/src/main/kube-auth-proxy/kube-auth-proxy.ts index 9a9b5a249f..160566c3a5 100644 --- a/packages/core/src/main/kube-auth-proxy/kube-auth-proxy.ts +++ b/packages/core/src/main/kube-auth-proxy/kube-auth-proxy.ts @@ -16,6 +16,7 @@ import type { Logger } from "../../common/logger"; import type { WaitUntilPortIsUsed } from "./wait-until-port-is-used/wait-until-port-is-used.injectable"; import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; import type { BroadcastConnectionUpdate } from "../cluster/broadcast-connection-update.injectable"; +import type { KubeAuthProxy } from "./create-kube-auth-proxy.injectable"; const startingServeMatcher = "starting to serve on (?
.+)"; const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), { @@ -33,7 +34,7 @@ export interface KubeAuthProxyDependencies { broadcastConnectionUpdate: BroadcastConnectionUpdate; } -export class KubeAuthProxy { +export class KubeAuthProxyImpl implements KubeAuthProxy { public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`; public get port(): number { diff --git a/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.ts b/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.ts index 9d28306bbf..276692c500 100644 --- a/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.ts +++ b/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.ts @@ -85,7 +85,7 @@ export class KubeconfigManager { return this.tempFilePath = await this.createProxyKubeconfig(); } catch (error) { - throw new Error(`Failed to creat temp config for auth-proxy: ${error}`); + throw new Error(`Failed to create temp config for auth-proxy: ${error}`); } } diff --git a/packages/core/src/main/kubectl/apply-all-handler.injectable.ts b/packages/core/src/main/kubectl/apply-all-handler.injectable.ts index 7f62ca14fc..00f58e5e3f 100644 --- a/packages/core/src/main/kubectl/apply-all-handler.injectable.ts +++ b/packages/core/src/main/kubectl/apply-all-handler.injectable.ts @@ -6,11 +6,12 @@ import emitAppEventInjectable from "../../common/app-event-bus/emit-event.inject import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable"; import { kubectlApplyAllChannel } from "../../common/kube-helpers/channels"; import resourceApplierInjectable from "../resource-applier/create-resource-applier.injectable"; -import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; const kubectlApplyAllChannelHandlerInjectable = getRequestChannelListenerInjectable({ + id: "kubectl-apply-all-channel-handler-listener", channel: kubectlApplyAllChannel, - handler: (di) => { + getHandler: (di) => { const getClusterById = di.inject(getClusterByIdInjectable); const emitAppEvent = di.inject(emitAppEventInjectable); diff --git a/packages/core/src/main/kubectl/delete-all-handler.injectable.ts b/packages/core/src/main/kubectl/delete-all-handler.injectable.ts index 715eb0caf9..5d207be482 100644 --- a/packages/core/src/main/kubectl/delete-all-handler.injectable.ts +++ b/packages/core/src/main/kubectl/delete-all-handler.injectable.ts @@ -6,11 +6,12 @@ import emitAppEventInjectable from "../../common/app-event-bus/emit-event.inject import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable"; import { kubectlDeleteAllChannel } from "../../common/kube-helpers/channels"; import resourceApplierInjectable from "../resource-applier/create-resource-applier.injectable"; -import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; const kubectlDeleteAllChannelHandlerInjectable = getRequestChannelListenerInjectable({ + id: "kubectl-delete-all-channel-handler-listener", channel: kubectlDeleteAllChannel, - handler: (di) => { + getHandler: (di) => { const emitAppEvent = di.inject(emitAppEventInjectable); const getClusterById = di.inject(getClusterByIdInjectable); diff --git a/packages/core/src/main/lens-proxy/lens-proxy-certificate-request-handler.injectable.ts b/packages/core/src/main/lens-proxy/lens-proxy-certificate-request-handler.injectable.ts index 5f6938912a..6f35450273 100644 --- a/packages/core/src/main/lens-proxy/lens-proxy-certificate-request-handler.injectable.ts +++ b/packages/core/src/main/lens-proxy/lens-proxy-certificate-request-handler.injectable.ts @@ -3,12 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { lensProxyCertificateChannel } from "../../common/certificate/lens-proxy-certificate-channel"; -import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable"; const lensProxyCertificateRequestHandlerInjectable = getRequestChannelListenerInjectable({ + id: "lens-proxy-certificate-request-handler-listener", channel: lensProxyCertificateChannel, - handler: (di) => { + getHandler: (di) => { const lensProxyCertificate = di.inject(lensProxyCertificateInjectable).get(); return () => ({ diff --git a/packages/core/src/main/start-main-application/lens-window/current-cluster-frame/listener.injectable.ts b/packages/core/src/main/start-main-application/lens-window/current-cluster-frame/listener.injectable.ts index d29210cda1..11d23e6a4f 100644 --- a/packages/core/src/main/start-main-application/lens-window/current-cluster-frame/listener.injectable.ts +++ b/packages/core/src/main/start-main-application/lens-window/current-cluster-frame/listener.injectable.ts @@ -3,17 +3,17 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { currentClusterMessageChannel } from "../../../../common/cluster/current-cluster-channel"; -import { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import { getMessageChannelListenerInjectable } from "@k8slens/messaging"; import currentClusterFrameClusterIdStateInjectable from "./current-cluster-frame-cluster-id-state.injectable"; -const currentVisibileClusterListenerInjectable = getMessageChannelListenerInjectable({ - id: "current-visibile-cluster", +const currentVisibleClusterListenerInjectable = getMessageChannelListenerInjectable({ + id: "current-visible-cluster", channel: currentClusterMessageChannel, - handler: (di) => { + getHandler: (di) => { const currentClusterFrameState = di.inject(currentClusterFrameClusterIdStateInjectable); return clusterId => currentClusterFrameState.set(clusterId); }, }); -export default currentVisibileClusterListenerInjectable; +export default currentVisibleClusterListenerInjectable; diff --git a/packages/core/src/main/start-main-application/runnables/root-frame-has-rendered/channel-listener.injectable.ts b/packages/core/src/main/start-main-application/runnables/root-frame-has-rendered/channel-listener.injectable.ts index 8b906c79e9..4769c12256 100644 --- a/packages/core/src/main/start-main-application/runnables/root-frame-has-rendered/channel-listener.injectable.ts +++ b/packages/core/src/main/start-main-application/runnables/root-frame-has-rendered/channel-listener.injectable.ts @@ -2,7 +2,7 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import { getMessageChannelListenerInjectable } from "@k8slens/messaging"; import { rootFrameHasRenderedChannel } from "../../../../common/root-frame/root-frame-rendered-channel"; import { runManyFor } from "@k8slens/run-many"; import { afterRootFrameIsReadyInjectionToken } from "../../runnable-tokens/phases"; @@ -10,7 +10,7 @@ import { afterRootFrameIsReadyInjectionToken } from "../../runnable-tokens/phase const rootFrameRenderedChannelListenerInjectable = getMessageChannelListenerInjectable({ id: "action", channel: rootFrameHasRenderedChannel, - handler: (di) => { + getHandler: (di) => { const runMany = runManyFor(di); return runMany(afterRootFrameIsReadyInjectionToken); diff --git a/packages/core/src/main/tray/menu-icon/reactive.injectable.ts b/packages/core/src/main/tray/menu-icon/reactive.injectable.ts index 8c6d358477..4fbc8c2448 100644 --- a/packages/core/src/main/tray/menu-icon/reactive.injectable.ts +++ b/packages/core/src/main/tray/menu-icon/reactive.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { reaction } from "mobx"; -import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable"; +import { getStartableStoppable } from "@k8slens/startable-stoppable"; import electronTrayInjectable from "../electron-tray/electron-tray.injectable"; import trayIconInjectable from "./tray-icon.injectable"; diff --git a/packages/core/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts b/packages/core/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts index 378bceafd9..a216675b01 100644 --- a/packages/core/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts +++ b/packages/core/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable"; +import { getStartableStoppable } from "@k8slens/startable-stoppable"; import { reaction } from "mobx"; import type { MinimalTrayMenuItem } from "../electron-tray/electron-tray.injectable"; import electronTrayInjectable from "../electron-tray/electron-tray.injectable"; diff --git a/packages/core/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts b/packages/core/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts deleted file mode 100644 index db6435ea56..0000000000 --- a/packages/core/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import type { IpcMainEvent } from "electron"; -import { enlistMessageChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token"; -import ipcMainInjectionToken from "../../../../common/ipc/ipc-main-injection-token"; - -const enlistMessageChannelListenerInjectable = getInjectable({ - id: "enlist-message-channel-listener-for-main", - - instantiate: (di) => { - const ipcMain = di.inject(ipcMainInjectionToken); - - return ({ channel, handler }) => { - const nativeOnCallback = (_: IpcMainEvent, message: unknown) => { - handler(message); - }; - - ipcMain.on(channel.id, nativeOnCallback); - - return () => { - ipcMain.off(channel.id, nativeOnCallback); - }; - }; - }, - - injectionToken: enlistMessageChannelListenerInjectionToken, -}); - -export default enlistMessageChannelListenerInjectable; diff --git a/packages/core/src/main/utils/channel/channel-listeners/listening-on-request-channels.injectable.ts b/packages/core/src/main/utils/channel/channel-listeners/listening-on-request-channels.injectable.ts deleted file mode 100644 index 1e6d0f296c..0000000000 --- a/packages/core/src/main/utils/channel/channel-listeners/listening-on-request-channels.injectable.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { disposer } from "@k8slens/utilities"; -import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; -import { getStartableStoppable } from "../../../../common/utils/get-startable-stoppable"; -import enlistRequestChannelListenerInjectable from "./enlist-request-channel-listener.injectable"; -import { requestChannelListenerInjectionToken } from "./listener-tokens"; - -const listeningOnRequestChannelsInjectable = getInjectable({ - id: "listening-on-request-channels", - instantiate: (di) => { - const enlistRequestChannelListener = di.inject(enlistRequestChannelListenerInjectable); - const requestChannelListeners = di.injectMany(requestChannelListenerInjectionToken); - - return getStartableStoppable("listening-on-request-channels", () => { - const seenChannels = new Set>(); - - for (const listener of requestChannelListeners) { - if (seenChannels.has(listener.channel)) { - throw new Error(`Tried to register a multiple channel handlers for "${listener.channel.id}", only one handler is supported for a request channel.`); - } - - seenChannels.add(listener.channel); - } - - return disposer(requestChannelListeners.map(enlistRequestChannelListener)); - }); - }, -}); - -export default listeningOnRequestChannelsInjectable; diff --git a/packages/core/src/main/utils/channel/channel-listeners/start-listening-on-channels.injectable.ts b/packages/core/src/main/utils/channel/channel-listeners/start-listening-on-channels.injectable.ts deleted file mode 100644 index c83425109c..0000000000 --- a/packages/core/src/main/utils/channel/channel-listeners/start-listening-on-channels.injectable.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { onLoadOfApplicationInjectionToken } from "@k8slens/application"; -import listeningOnMessageChannelsInjectable from "../../../../common/utils/channel/listening-on-message-channels.injectable"; -import listeningOnRequestChannelsInjectable from "./listening-on-request-channels.injectable"; - -const startListeningOnChannelsInjectable = getInjectable({ - id: "start-listening-on-channels-main", - - instantiate: (di) => ({ - run: () => { - const listeningOnMessageChannels = di.inject(listeningOnMessageChannelsInjectable); - const listeningOnRequestChannels = di.inject(listeningOnRequestChannelsInjectable); - - listeningOnMessageChannels.start(); - listeningOnRequestChannels.start(); - }, - }), - - injectionToken: onLoadOfApplicationInjectionToken, -}); - -export default startListeningOnChannelsInjectable; diff --git a/packages/core/src/main/utils/channel/message-to-channel.injectable.ts b/packages/core/src/main/utils/channel/message-to-channel.injectable.ts deleted file mode 100644 index 392120509d..0000000000 --- a/packages/core/src/main/utils/channel/message-to-channel.injectable.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import type { SendMessageToChannel } from "../../../common/utils/channel/message-to-channel-injection-token"; -import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; -import getVisibleWindowsInjectable from "../../start-main-application/lens-window/get-visible-windows.injectable"; -import clusterFramesInjectable from "../../../common/cluster-frames.injectable"; - -const messageToChannelInjectable = getInjectable({ - id: "message-to-channel", - - instantiate: (di) => { - const getVisibleWindows = di.inject(getVisibleWindowsInjectable); - const clusterFrames = di.inject(clusterFramesInjectable); - - return ((channel, data) => { - for (const window of getVisibleWindows()) { - window.send({ channel: channel.id, data }); - - clusterFrames.forEach(frameInfo => { - window.send({ channel: channel.id, data, frameInfo }); - }); - } - }) as SendMessageToChannel; - }, - - injectionToken: sendMessageToChannelInjectionToken, -}); - -export default messageToChannelInjectable; diff --git a/packages/core/src/main/utils/channel/message-to-channel.test.ts b/packages/core/src/main/utils/channel/message-to-channel.test.ts deleted file mode 100644 index 8e544c47dd..0000000000 --- a/packages/core/src/main/utils/channel/message-to-channel.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getDiForUnitTesting } from "../../getDiForUnitTesting"; -import getVisibleWindowsInjectable from "../../start-main-application/lens-window/get-visible-windows.injectable"; -import clusterFramesInjectable from "../../../common/cluster-frames.injectable"; -import type { MessageChannel } from "../../../common/utils/channel/message-channel-listener-injection-token"; -import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; -import type { DiContainer } from "@ogre-tools/injectable"; -import type { ClusterFrameInfo } from "../../../common/cluster-frames.injectable"; - -describe("message-to-channel", () => { - let di: DiContainer; - let sendToWindowMock: jest.Mock; - - beforeEach(() => { - di = getDiForUnitTesting(); - - sendToWindowMock = jest.fn(); - - di.override(getVisibleWindowsInjectable, () => () => [ - { - id: "some-window", - send: sendToWindowMock, - show: () => {}, - reload: () => {}, - isStarting: false, - start: async () => {}, - close: () => {}, - isVisible: true, - }, - - { - id: "some-other-window", - send: sendToWindowMock, - show: () => {}, - reload: () => {}, - isStarting: false, - start: async () => {}, - close: () => {}, - isVisible: true, - }, - ]); - - di.override( - clusterFramesInjectable, - () => - new Map([ - [ - "some-cluster-id", - { frameId: 42, processId: 84 }, - ], - [ - "some-other-cluster-id", - { frameId: 126, processId: 168 }, - ], - ]), - ); - }); - - describe("when sending message", () => { - beforeEach(() => { - const sendMessageToChannel = di.inject( - sendMessageToChannelInjectionToken, - ); - - sendMessageToChannel(someChannel, 42); - }); - - it("sends to each window and cluster frames", () => { - expect(sendToWindowMock.mock.calls).toEqual([ - [{ channel: "some-channel-id", data: 42 }], - - [ - { - channel: "some-channel-id", - data: 42, - frameInfo: { frameId: 42, processId: 84 }, - }, - ], - - [ - { - channel: "some-channel-id", - data: 42, - frameInfo: { frameId: 126, processId: 168 }, - }, - ], - - [{ channel: "some-channel-id", data: 42 }], - - [ - { - channel: "some-channel-id", - data: 42, - frameInfo: { frameId: 42, processId: 84 }, - }, - ], - - [ - { - channel: "some-channel-id", - data: 42, - frameInfo: { frameId: 126, processId: 168 }, - }, - ], - ]); - }); - }); -}); - -const someChannel: MessageChannel = { - id: "some-channel-id", -}; diff --git a/packages/core/src/main/utils/resolve-system-proxy/electron.injectable.ts b/packages/core/src/main/utils/resolve-system-proxy/electron.injectable.ts deleted file mode 100644 index a5999c9e59..0000000000 --- a/packages/core/src/main/utils/resolve-system-proxy/electron.injectable.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import electron from "electron"; - -const electronInjectable = getInjectable({ - id: "electron", - instantiate: () => electron, - causesSideEffects: true, -}); - -export default electronInjectable; diff --git a/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-channel-responder.injectable.ts b/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-channel-responder.injectable.ts index a2cd605633..16229a0fb7 100644 --- a/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-channel-responder.injectable.ts +++ b/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-channel-responder.injectable.ts @@ -3,12 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { resolveSystemProxyChannel } from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-channel"; -import { getRequestChannelListenerInjectable } from "../channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; import resolveSystemProxyInjectable from "./resolve-system-proxy.injectable"; const resolveSystemProxyChannelResponderInjectable = getRequestChannelListenerInjectable({ + id: "resolve-system-proxy-channel-responder-listener", channel: resolveSystemProxyChannel, - handler: (di) => di.inject(resolveSystemProxyInjectable), + getHandler: (di) => di.inject(resolveSystemProxyInjectable), }); export default resolveSystemProxyChannelResponderInjectable; diff --git a/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.injectable.ts b/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.injectable.ts index c663d59fde..4ad2da39b8 100644 --- a/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.injectable.ts +++ b/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.injectable.ts @@ -3,28 +3,19 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import electronInjectable from "./electron.injectable"; import withErrorLoggingInjectable from "../../../common/utils/with-error-logging/with-error-logging.injectable"; +import resolveSystemProxyWindowInjectable from "./resolve-system-proxy-window.injectable"; const resolveSystemProxyFromElectronInjectable = getInjectable({ id: "resolve-system-proxy-from-electron", instantiate: (di) => { - const electron = di.inject(electronInjectable); + const helperWindow = di.inject(resolveSystemProxyWindowInjectable); const withErrorLoggingFor = di.inject(withErrorLoggingInjectable); - const withErrorLogging = withErrorLoggingFor(() => "Error resolving proxy"); - + return withErrorLogging(async (url: string) => { - const webContent = electron.webContents - .getAllWebContents() - .find((x) => !x.isDestroyed()); - - if (!webContent) { - throw new Error(`Tried to resolve proxy for "${url}", but no browser window was available`); - } - - return await webContent.session.resolveProxy(url); + return await helperWindow.webContents.session.resolveProxy(url); }); }, }); diff --git a/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.test.ts b/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.test.ts index c197566a10..a2e6d01627 100644 --- a/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.test.ts +++ b/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-from-electron.test.ts @@ -5,13 +5,13 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import resolveSystemProxyFromElectronInjectable from "./resolve-system-proxy-from-electron.injectable"; -import electronInjectable from "./electron.injectable"; +import resolveSystemProxyWindowInjectable from "./resolve-system-proxy-window.injectable"; import type { AsyncFnMock } from "@async-fn/jest"; import asyncFn from "@async-fn/jest"; -import type electron from "electron"; import { getPromiseStatus } from "@k8slens/test-utils"; import logErrorInjectable from "../../../common/log-error.injectable"; import type { DiContainer } from "@ogre-tools/injectable"; +import type { BrowserWindow, Session, WebContents } from "electron"; describe("technical: resolve-system-proxy-from-electron", () => { let resolveSystemProxyMock: AsyncFnMock<(url: string) => Promise>; @@ -26,44 +26,19 @@ describe("technical: resolve-system-proxy-from-electron", () => { di.override(logErrorInjectable, () => logErrorMock); }); - describe("given there are non-destroyed Lens windows, when called with URL", () => { + describe("given there are no unexpected issues, when called with URL", () => { beforeEach(() => { resolveSystemProxyMock = asyncFn(); di.override( - electronInjectable, - - () => - ({ - webContents: { - getAllWebContents: () => [ - { - isDestroyed: () => true, - - session: { - resolveProxy: () => { - throw new Error("should never come here"); - }, - }, - }, - - { - isDestroyed: () => false, - session: { resolveProxy: resolveSystemProxyMock }, - }, - - { - isDestroyed: () => false, - - session: { - resolveProxy: () => { - throw new Error("should never come here"); - }, - }, - }, - ], - }, - } as unknown as typeof electron), + resolveSystemProxyWindowInjectable, + () => ({ + webContents: { + session: { + resolveProxy: resolveSystemProxyMock, + } as unknown as Session, + } as unknown as WebContents, + } as unknown as BrowserWindow), ); const resolveSystemProxyFromElectron = di.inject( @@ -73,7 +48,7 @@ describe("technical: resolve-system-proxy-from-electron", () => { actualPromise = resolveSystemProxyFromElectron("some-url"); }); - it("calls to resolve proxy from the first window", () => { + it("calls to resolve proxy from the browser window", () => { expect(resolveSystemProxyMock).toHaveBeenCalledWith("some-url"); }); @@ -90,28 +65,23 @@ describe("technical: resolve-system-proxy-from-electron", () => { }); }); - describe("given there are only destroyed Lens windows, when called with URL", () => { + describe("given there are unexpected issues, when called with URL", () => { let error: any; beforeEach(async () => { - di.override( - electronInjectable, - () => - ({ - webContents: { - getAllWebContents: () => [ - { - isDestroyed: () => true, + resolveSystemProxyMock = asyncFn(); - session: { - resolveProxy: () => { - throw new Error("should never come here"); - }, - }, - }, - ], - }, - } as unknown as typeof electron), + di.override( + resolveSystemProxyWindowInjectable, + () => ({ + webContents: { + session: { + resolveProxy: () => { + throw new Error("unexpected error"); + }, + } as unknown as Session, + } as unknown as WebContents, + } as unknown as BrowserWindow), ); resolveSystemProxyMock = asyncFn(); @@ -128,7 +98,7 @@ describe("technical: resolve-system-proxy-from-electron", () => { }); it("throws error", () => { - expect(error.message).toBe('Tried to resolve proxy for "some-url", but no browser window was available'); + expect(error.message).toBe("unexpected error"); }); it("logs error", () => { diff --git a/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-window.global-override-for-injectable.ts b/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-window.global-override-for-injectable.ts new file mode 100644 index 0000000000..4bf1ada952 --- /dev/null +++ b/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-window.global-override-for-injectable.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "@k8slens/test-utils"; +import type { BrowserWindow, Session, WebContents } from "electron"; +import resolveSystemProxyWindowInjectable from "./resolve-system-proxy-window.injectable"; + +export default getGlobalOverride( + resolveSystemProxyWindowInjectable, + () => ({ + webContents: { + session: { + resolveProxy: () => "DIRECT", + } as unknown as Session, + } as unknown as WebContents, + } as unknown as BrowserWindow), +); diff --git a/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-window.injectable.ts b/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-window.injectable.ts new file mode 100644 index 0000000000..baa0da6c39 --- /dev/null +++ b/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-window.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { BrowserWindow } from "electron"; + +const resolveSystemProxyWindowInjectable = getInjectable({ + id: "resolve-system-proxy-window", + instantiate: () => { + const window = new BrowserWindow({ show: false }); + + window.hide(); + + return window; + }, + causesSideEffects: true, +}); + +export default resolveSystemProxyWindowInjectable; diff --git a/packages/core/src/main/utils/sync-box/sync-box-initial-value-channel-listener.injectable.ts b/packages/core/src/main/utils/sync-box/sync-box-initial-value-channel-listener.injectable.ts index da78d76c36..fafb32e7e9 100644 --- a/packages/core/src/main/utils/sync-box/sync-box-initial-value-channel-listener.injectable.ts +++ b/packages/core/src/main/utils/sync-box/sync-box-initial-value-channel-listener.injectable.ts @@ -4,11 +4,12 @@ */ import { syncBoxInitialValueChannel } from "../../../common/utils/sync-box/channels"; import { syncBoxInjectionToken } from "../../../common/utils/sync-box/sync-box-injection-token"; -import { getRequestChannelListenerInjectable } from "../channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "@k8slens/messaging"; const syncBoxInitialValueChannelListenerInjectable = getRequestChannelListenerInjectable({ + id: "sync-box-initial-value-channel-listener", channel: syncBoxInitialValueChannel, - handler: (di) => { + getHandler: (di) => { const syncBoxes = di.injectMany(syncBoxInjectionToken); return () => syncBoxes.map((box) => ({ diff --git a/packages/core/src/renderer/app-paths/setup-app-paths.injectable.ts b/packages/core/src/renderer/app-paths/setup-app-paths.injectable.ts index d80cc668ac..af5c3cebc6 100644 --- a/packages/core/src/renderer/app-paths/setup-app-paths.injectable.ts +++ b/packages/core/src/renderer/app-paths/setup-app-paths.injectable.ts @@ -4,9 +4,9 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import appPathsStateInjectable from "../../common/app-paths/app-paths-state.injectable"; -import { beforeFrameStartsFirstInjectionToken } from "../before-frame-starts/tokens"; import { appPathsChannel } from "../../common/app-paths/app-paths-channel"; -import { requestFromChannelInjectionToken } from "../../common/utils/channel/request-from-channel-injection-token"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; +import { beforeApplicationIsLoadingInjectionToken } from "@k8slens/application"; const setupAppPathsInjectable = getInjectable({ id: "setup-app-paths", @@ -21,7 +21,7 @@ const setupAppPathsInjectable = getInjectable({ }, }), - injectionToken: beforeFrameStartsFirstInjectionToken, + injectionToken: beforeApplicationIsLoadingInjectionToken, }); export default setupAppPathsInjectable; diff --git a/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-crd-api-creations.injectable.ts b/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-crd-api-creations.injectable.ts new file mode 100644 index 0000000000..c23a34d767 --- /dev/null +++ b/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-crd-api-creations.injectable.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { reaction } from "mobx"; +import { customResourceDefinitionApiInjectionToken } from "../../../common/k8s-api/api-manager/crd-api-token"; +import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints"; +import { KubeApi } from "../../../common/k8s-api/kube-api"; +import { KubeObject } from "../../../common/k8s-api/kube-object"; +import maybeKubeApiInjectable from "../../../common/k8s-api/maybe-kube-api.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import { injectableDifferencingRegistratorWith } from "../../../common/utils/registrator-helper"; +import customResourceDefinitionStoreInjectable from "../../components/+custom-resources/definition.store.injectable"; +import { beforeClusterFrameStartsSecondInjectionToken } from "../tokens"; + +const setupAutoCrdApiCreationsInjectable = getInjectable({ + id: "setup-auto-crd-api-creations", + instantiate: (di) => ({ + run: () => { + const customResourceDefinitionStore = di.inject(customResourceDefinitionStoreInjectable); + const injectableDifferencingRegistrator = injectableDifferencingRegistratorWith(di); + + reaction( + () => customResourceDefinitionStore.getItems().map(toCrdApiInjectable), + injectableDifferencingRegistrator, + { + fireImmediately: true, + }, + ); + }, + }), + injectionToken: beforeClusterFrameStartsSecondInjectionToken, +}); + +export default setupAutoCrdApiCreationsInjectable; + +const toCrdApiInjectable = (crd: CustomResourceDefinition) => getInjectable({ + id: `default-kube-api-for-custom-resource-definition-${crd.getResourceApiBase()}`, + instantiate: (di) => { + const objectConstructor = class extends KubeObject { + static readonly kind = crd.getResourceKind(); + static readonly namespaced = crd.isNamespaced(); + static readonly apiBase = crd.getResourceApiBase(); + }; + + return new KubeApi({ + logger: di.inject(loggerInjectable), + maybeKubeApi: di.inject(maybeKubeApiInjectable), + }, { objectConstructor }); + }, + injectionToken: customResourceDefinitionApiInjectionToken, +}); diff --git a/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-registration.injectable.ts b/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-registration.injectable.ts index dbc77a92b8..78b1f90015 100644 --- a/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-registration.injectable.ts +++ b/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-registration.injectable.ts @@ -5,71 +5,22 @@ import { getInjectable } from "@ogre-tools/injectable"; import autoRegistrationEmitterInjectable from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable"; import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable"; -import { CustomResourceStore } from "../../../common/k8s-api/api-manager/resource.store"; -import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints"; -import type { KubeApiDependencies } from "../../../common/k8s-api/kube-api"; -import { KubeApi } from "../../../common/k8s-api/kube-api"; -import { KubeObject } from "../../../common/k8s-api/kube-object"; +import type { KubeApi } from "../../../common/k8s-api/kube-api"; import { beforeClusterFrameStartsSecondInjectionToken } from "../tokens"; -import type { KubeObjectStoreDependencies } from "../../../common/k8s-api/kube-object.store"; -import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable"; -import loggerInjectable from "../../../common/logger.injectable"; -import maybeKubeApiInjectable from "../../../common/k8s-api/maybe-kube-api.injectable"; const setupAutoRegistrationInjectable = getInjectable({ id: "setup-auto-registration", instantiate: (di) => ({ run: () => { const autoRegistrationEmitter = di.inject(autoRegistrationEmitterInjectable); - const beforeApiManagerInitializationCrds: CustomResourceDefinition[] = []; const beforeApiManagerInitializationApis: KubeApi[] = []; - const kubeApiDependencies: KubeApiDependencies = { - logger: di.inject(loggerInjectable), - maybeKubeApi: di.inject(maybeKubeApiInjectable), - }; - const kubeObjectStoreDependencies: KubeObjectStoreDependencies = { - context: di.inject(clusterFrameContextForNamespacedResourcesInjectable), - logger: di.inject(loggerInjectable), - }; let initialized = false; - const autoInitCustomResourceStore = (crd: CustomResourceDefinition) => { - const objectConstructor = class extends KubeObject { - static readonly kind = crd.getResourceKind(); - static readonly namespaced = crd.isNamespaced(); - static readonly apiBase = crd.getResourceApiBase(); - }; - - const api = (() => { - const rawApi = apiManager.getApi(objectConstructor.apiBase); - - if (rawApi) { - return rawApi; - } - - const api = new KubeApi(kubeApiDependencies, { objectConstructor }); - - apiManager.registerApi(api); - - return api; - })(); - - if (!apiManager.getStore(api)) { - apiManager.registerStore(new CustomResourceStore(kubeObjectStoreDependencies, api)); - } - }; const autoInitKubeApi = (api: KubeApi) => { apiManager.registerApi(api); }; autoRegistrationEmitter - .on("customResourceDefinition", (crd) => { - if (initialized) { - autoInitCustomResourceStore(crd); - } else { - beforeApiManagerInitializationCrds.push(crd); - } - }) .on("kubeApi", (api) => { if (initialized) { autoInitKubeApi(api); @@ -81,7 +32,6 @@ const setupAutoRegistrationInjectable = getInjectable({ // NOTE: this MUST happen after the event emitter listeners are registered const apiManager = di.inject(apiManagerInjectable); - beforeApiManagerInitializationCrds.forEach(autoInitCustomResourceStore); beforeApiManagerInitializationApis.forEach(autoInitKubeApi); initialized = true; }, diff --git a/packages/core/src/renderer/before-frame-starts/runnables/setup-current-cluster-broadcast.injectable.ts b/packages/core/src/renderer/before-frame-starts/runnables/setup-current-cluster-broadcast.injectable.ts index 0d9c8bc65a..e60eb3fae1 100644 --- a/packages/core/src/renderer/before-frame-starts/runnables/setup-current-cluster-broadcast.injectable.ts +++ b/packages/core/src/renderer/before-frame-starts/runnables/setup-current-cluster-broadcast.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import { reaction } from "mobx"; import { currentClusterMessageChannel } from "../../../common/cluster/current-cluster-channel"; -import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; +import { sendMessageToChannelInjectionToken } from "@k8slens/messaging"; import matchedClusterIdInjectable from "../../navigation/matched-cluster-id.injectable"; import { beforeMainFrameStartsFirstInjectionToken } from "../tokens"; diff --git a/packages/core/src/renderer/certificate/request-lens-proxy-certificate.injectable.ts b/packages/core/src/renderer/certificate/request-lens-proxy-certificate.injectable.ts index e1322096bf..f80911c7a5 100644 --- a/packages/core/src/renderer/certificate/request-lens-proxy-certificate.injectable.ts +++ b/packages/core/src/renderer/certificate/request-lens-proxy-certificate.injectable.ts @@ -4,12 +4,12 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { lensProxyCertificateChannel } from "../../common/certificate/lens-proxy-certificate-channel"; -import requestFromChannelInjectable from "../utils/channel/request-from-channel.injectable"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; const requestLensProxyCertificateInjectable = getInjectable({ id: "request-lens-proxy-certificate", instantiate: (di) => { - const requestFromChannel = di.inject(requestFromChannelInjectable); + const requestFromChannel = di.inject(requestFromChannelInjectionToken); return () => requestFromChannel(lensProxyCertificateChannel); }, diff --git a/packages/core/src/renderer/components/+catalog/catalog.module.scss b/packages/core/src/renderer/components/+catalog/catalog.module.scss index 4fe31c4f27..85976301ba 100644 --- a/packages/core/src/renderer/components/+catalog/catalog.module.scss +++ b/packages/core/src/renderer/components/+catalog/catalog.module.scss @@ -8,6 +8,10 @@ opacity: 1; } + :global(.TableCell) { + padding: 6px 8px; + } + flex-grow: 1; } diff --git a/packages/core/src/renderer/components/+catalog/catalog.tsx b/packages/core/src/renderer/components/+catalog/catalog.tsx index b521f39730..1d385d78c4 100644 --- a/packages/core/src/renderer/components/+catalog/catalog.tsx +++ b/packages/core/src/renderer/components/+catalog/catalog.tsx @@ -276,6 +276,9 @@ class NonInjectedCatalog extends React.Component { {...getCategoryColumns({ activeCategory })} onDetails={this.props.onCatalogEntityListClick} renderItemMenu={this.renderItemMenu} + tableProps={{ + customRowHeights: () => 36, // Entity avatar size + padding + }} data-testid={`catalog-list-for-${activeCategory?.metadata.name ?? "browse-all"}`} /> ); diff --git a/packages/core/src/renderer/components/+config-horizontal-pod-autoscalers/hpa-details.test.tsx b/packages/core/src/renderer/components/+config-horizontal-pod-autoscalers/hpa-details.test.tsx index 48c2183055..aa2d2a3b32 100644 --- a/packages/core/src/renderer/components/+config-horizontal-pod-autoscalers/hpa-details.test.tsx +++ b/packages/core/src/renderer/components/+config-horizontal-pod-autoscalers/hpa-details.test.tsx @@ -4,8 +4,13 @@ */ import type { RenderResult } from "@testing-library/react"; import React from "react"; +import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; +import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import { Cluster } from "../../../common/cluster/cluster"; import { HorizontalPodAutoscaler, HpaMetricType } from "../../../common/k8s-api/endpoints"; +import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; import type { DiRender } from "../test-utils/renderFor"; import { renderFor } from "../test-utils/renderFor"; import { HpaDetails } from "./hpa-details"; @@ -41,6 +46,17 @@ describe("", () => { beforeEach(() => { const di = getDiForUnitTesting(); + di.override(directoryForUserDataInjectable, () => "/some-user-store-path"); + di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); + di.override(storesAndApisCanBeCreatedInjectable, () => true); + di.override(hostedClusterInjectable, () => new Cluster({ + contextName: "some-context-name", + id: "some-cluster-id", + kubeConfigPath: "/some-path-to-a-kubeconfig", + }, { + clusterServerUrl: "https://localhost:8080", + })); + render = renderFor(di); }); diff --git a/packages/core/src/renderer/components/+custom-resources/definition.store.injectable.ts b/packages/core/src/renderer/components/+custom-resources/definition.store.injectable.ts index f1d8f635b8..dbbd2460aa 100644 --- a/packages/core/src/renderer/components/+custom-resources/definition.store.injectable.ts +++ b/packages/core/src/renderer/components/+custom-resources/definition.store.injectable.ts @@ -4,7 +4,6 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import assert from "assert"; -import autoRegistrationEmitterInjectable from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable"; import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/kube-object-store-token"; import customResourceDefinitionApiInjectable from "../../../common/k8s-api/endpoints/custom-resource-definition.api.injectable"; import loggerInjectable from "../../../common/logger.injectable"; @@ -20,7 +19,6 @@ const customResourceDefinitionStoreInjectable = getInjectable({ const api = di.inject(customResourceDefinitionApiInjectable); return new CustomResourceDefinitionStore({ - autoRegistration: di.inject(autoRegistrationEmitterInjectable), context: di.inject(clusterFrameContextForClusterScopedResourcesInjectable), logger: di.inject(loggerInjectable), }, api); diff --git a/packages/core/src/renderer/components/+custom-resources/definition.store.ts b/packages/core/src/renderer/components/+custom-resources/definition.store.ts index 310e51709a..78ef3982a9 100644 --- a/packages/core/src/renderer/components/+custom-resources/definition.store.ts +++ b/packages/core/src/renderer/components/+custom-resources/definition.store.ts @@ -3,37 +3,22 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { computed, reaction, makeObservable } from "mobx"; +import { computed, makeObservable } from "mobx"; import type { KubeObjectStoreDependencies, KubeObjectStoreOptions } from "../../../common/k8s-api/kube-object.store"; import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { CustomResourceDefinition, CustomResourceDefinitionApi } from "../../../common/k8s-api/endpoints/custom-resource-definition.api"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; -import type TypedEventEmitter from "typed-emitter"; -import type { LegacyAutoRegistration } from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable"; import autoBind from "auto-bind"; -export interface CustomResourceDefinitionStoreDependencies extends KubeObjectStoreDependencies { - readonly autoRegistration: TypedEventEmitter; -} - export class CustomResourceDefinitionStore extends KubeObjectStore { constructor( - protected readonly dependencies: CustomResourceDefinitionStoreDependencies, + dependencies: KubeObjectStoreDependencies, api: CustomResourceDefinitionApi, opts?: KubeObjectStoreOptions, ) { super(dependencies, api, opts); makeObservable(this); autoBind(this); - - reaction( - () => this.getItems(), - crds => { - for (const crd of crds) { - this.dependencies.autoRegistration.emit("customResourceDefinition", crd); - } - }, - ); } protected sortItems(items: CustomResourceDefinition[]) { diff --git a/packages/core/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx b/packages/core/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx index 523a91596e..8f2e5692d1 100644 --- a/packages/core/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx @@ -26,6 +26,7 @@ export interface ExtensionInfo { requireConfirmation?: boolean; } +// @ts-ignore interface NpmPackageVersionDescriptor extends PackageJson { dist: { integrity: string; diff --git a/packages/core/src/renderer/components/+network-ingresses/ingresses.scss b/packages/core/src/renderer/components/+network-ingresses/ingresses.scss index 4c6d5a75c1..f0b754d03d 100644 --- a/packages/core/src/renderer/components/+network-ingresses/ingresses.scss +++ b/packages/core/src/renderer/components/+network-ingresses/ingresses.scss @@ -12,7 +12,7 @@ &.rules { flex-grow: 3.0; overflow-x: scroll; - text-overflow: unset; + flex-wrap: wrap; &::-webkit-scrollbar { display: none; @@ -20,7 +20,6 @@ .ingressRule { overflow: hidden; - text-overflow: ellipsis; } .ingressRule + .ingressRule { diff --git a/packages/core/src/renderer/components/+workloads-pods/pods.tsx b/packages/core/src/renderer/components/+workloads-pods/pods.tsx index 849a101ae1..1132c00a0c 100644 --- a/packages/core/src/renderer/components/+workloads-pods/pods.tsx +++ b/packages/core/src/renderer/components/+workloads-pods/pods.tsx @@ -29,6 +29,7 @@ import nodeApiInjectable from "../../../common/k8s-api/endpoints/node.api.inject import eventStoreInjectable from "../+events/store.injectable"; import podStoreInjectable from "./store.injectable"; import { NamespaceSelectBadge } from "../+namespaces/namespace-select-badge"; +import { Tooltip } from "../tooltip"; enum columnId { name = "name", @@ -163,13 +164,14 @@ class NonInjectedPods extends React.Component { { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, ]} renderTableContents={pod => [ - , + <> + + {pod.getName()} + + + {pod.getName()} + + , , void; } export class ClusterFrameHandler { diff --git a/packages/core/src/renderer/components/cluster-manager/emit-cluster-visibility.injectable.ts b/packages/core/src/renderer/components/cluster-manager/emit-cluster-visibility.injectable.ts index 261582d136..5c941c60e6 100644 --- a/packages/core/src/renderer/components/cluster-manager/emit-cluster-visibility.injectable.ts +++ b/packages/core/src/renderer/components/cluster-manager/emit-cluster-visibility.injectable.ts @@ -3,18 +3,16 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { MessageChannelHandler } from "../../../common/utils/channel/message-channel-listener-injection-token"; -import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; +import { sendMessageToChannelInjectionToken } from "@k8slens/messaging"; import { clusterVisibilityChannel } from "../../../common/cluster/visibility-channel"; - -export type EmitClusterVisibility = MessageChannelHandler; +import type { ClusterId } from "../../../common/cluster-types"; const emitClusterVisibilityInjectable = getInjectable({ id: "emit-cluster-visibility", - instantiate: (di): EmitClusterVisibility => { + instantiate: (di) => { const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); - return (id) => sendMessageToChannel(clusterVisibilityChannel, id); + return (id: ClusterId | null) => sendMessageToChannel(clusterVisibilityChannel, id); }, }); diff --git a/packages/core/src/renderer/components/cluster-settings/__tests__/__snapshots__/icon-settings.test.tsx.snap b/packages/core/src/renderer/components/cluster-settings/__tests__/__snapshots__/icon-settings.test.tsx.snap index 97eac8fca2..6c22436089 100644 --- a/packages/core/src/renderer/components/cluster-settings/__tests__/__snapshots__/icon-settings.test.tsx.snap +++ b/packages/core/src/renderer/components/cluster-settings/__tests__/__snapshots__/icon-settings.test.tsx.snap @@ -51,3 +51,55 @@ exports[`Icon settings given no external registrations for cluster settings menu
`; + +exports[`Icon settings given no registrations for cluster settings component injection token renders 1`] = ` + +
+
+
+
+
+ + +
+
+ + + more_horiz + + +
+
+
+ +`; diff --git a/packages/core/src/renderer/components/cluster-settings/__tests__/icon-settings.test.tsx b/packages/core/src/renderer/components/cluster-settings/__tests__/icon-settings.test.tsx index 74fd2a6654..31bf671854 100644 --- a/packages/core/src/renderer/components/cluster-settings/__tests__/icon-settings.test.tsx +++ b/packages/core/src/renderer/components/cluster-settings/__tests__/icon-settings.test.tsx @@ -2,8 +2,7 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { DiContainer } from "@ogre-tools/injectable"; -import { getInjectable } from "@ogre-tools/injectable"; + import type { RenderResult } from "@testing-library/react"; import React from "react"; import { KubernetesCluster } from "../../../../common/catalog-entities"; @@ -13,13 +12,18 @@ import { renderFor } from "../../test-utils/renderFor"; import { ClusterIconSetting } from "../icon-settings"; import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { clusterIconSettingsMenuInjectionToken } from "../cluster-settings-menu-injection-token"; +import type { ClusterIconSettingComponentProps } from "@k8slens/cluster-settings"; +import { clusterIconSettingsComponentInjectionToken, clusterIconSettingsMenuInjectionToken } from "@k8slens/cluster-settings"; import { runInAction } from "mobx"; +import { getInjectable, type DiContainer } from "@ogre-tools/injectable"; const cluster = new Cluster({ contextName: "some-context", id: "some-id", kubeConfigPath: "/some/path/to/kubeconfig", + preferences: { + clusterName: "some-cluster-name", + }, }, { clusterServerUrl: "https://localhost:9999", }); @@ -53,6 +57,29 @@ const newMenuItem = getInjectable({ injectionToken: clusterIconSettingsMenuInjectionToken, }); +function CustomSettingsComponent(props: ClusterIconSettingComponentProps) { + return ( +
+ Test React Component + + Cluster + {props.preferences.clusterName} + +
+ ); +} + +const newSettingsReactComponent = getInjectable({ + id: "cluster-icon-settings-react-component", + + instantiate: () => ({ + id: "test-react-component", + Component: CustomSettingsComponent, + }), + + injectionToken: clusterIconSettingsComponentInjectionToken, +}); + describe("Icon settings", () => { let rendered: RenderResult; let di: DiContainer; @@ -98,4 +125,30 @@ describe("Icon settings", () => { expect(rendered.getByText("Hello World")).toBeInTheDocument(); }); }); + + describe("given no registrations for cluster settings component injection token", () => { + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not have any external components", async () => { + expect(rendered.queryByTestId("test-react-component")).not.toBeInTheDocument(); + }); + }); + + describe("given registration for cluster settings component injection token", () => { + beforeEach(() => { + runInAction(() => { + di.register(newSettingsReactComponent); + }); + }); + + it("renders external component", async () => { + expect(rendered.queryByTestId("my-react-component")).toBeInTheDocument(); + }); + + it("external component has cluster preferences in props", async () => { + expect(rendered.getByText(/some-cluster-name/)).toBeInTheDocument(); + }); + }); }); diff --git a/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-clear-item.injectable.ts b/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-clear-item.injectable.ts index 697c68bb40..87f8c87940 100644 --- a/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-clear-item.injectable.ts +++ b/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-clear-item.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { clusterIconSettingsMenuInjectionToken } from "./cluster-settings-menu-injection-token"; +import { clusterIconSettingsMenuInjectionToken } from "@k8slens/cluster-settings"; const clusterIconSettingsMenuClearItem = getInjectable({ id: "cluster-icon-settings-menu-clear-item", diff --git a/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-injection-token.ts b/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-injection-token.ts deleted file mode 100644 index 15dbba2754..0000000000 --- a/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-injection-token.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectionToken } from "@ogre-tools/injectable"; -import type { ClusterPreferences } from "../../../common/cluster-types"; - -export interface ClusterIconMenuItem { - id: string; - title: string; - disabled?: (preferences: ClusterPreferences) => boolean; - onClick: (preferences: ClusterPreferences) => void; -} - -export const clusterIconSettingsMenuInjectionToken = getInjectionToken({ - id: "cluster-icon-settings-menu-injection-token", -}); diff --git a/packages/core/src/renderer/components/cluster-settings/icon-settings.tsx b/packages/core/src/renderer/components/cluster-settings/icon-settings.tsx index 94706f17a5..a802807c17 100644 --- a/packages/core/src/renderer/components/cluster-settings/icon-settings.tsx +++ b/packages/core/src/renderer/components/cluster-settings/icon-settings.tsx @@ -15,8 +15,8 @@ import { FilePicker, OverSizeLimitStyle } from "../file-picker"; import { MenuActions, MenuItem } from "../menu"; import type { ShowNotification } from "../notifications"; import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable"; -import type { ClusterIconMenuItem } from "./cluster-settings-menu-injection-token"; -import { clusterIconSettingsMenuInjectionToken } from "./cluster-settings-menu-injection-token"; +import { clusterIconSettingsComponentInjectionToken, clusterIconSettingsMenuInjectionToken } from "@k8slens/cluster-settings"; +import type { ClusterIconMenuItem, ClusterIconSettingsComponent } from "@k8slens/cluster-settings"; export interface ClusterIconSettingProps { cluster: Cluster; @@ -25,6 +25,7 @@ export interface ClusterIconSettingProps { interface Dependencies { menuItems: IComputedValue; + settingComponents: IComputedValue; showErrorNotification: ShowNotification; } @@ -95,6 +96,14 @@ const NonInjectedClusterIconSetting = observer((props: ClusterIconSettingProps & )}
+ {props.settingComponents.get().map(item => { + return ( + + ); + })}
); }); @@ -106,6 +115,7 @@ export const ClusterIconSetting = withInjectables and there is not enough space to show all the content &.sorting.nowrap { display: flex; align-items: center; + } - > * { - flex-shrink: 0; - } + .sortIcon { + transition: 150ms opacity; + opacity: 0; + position: absolute; + right: 0; + background: var(--tableHeaderBackground); - > .content { - flex-shrink: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - > .sortIcon { - margin: 0; + &.enabled { + opacity: 1; } } - .Table.sortable > .TableHead & { - user-select: none; - white-space: nowrap; + &:hover .sortIcon { + opacity: 1; + } - &.sorting { - cursor: pointer; - } - - .sortIcon { - transition: 350ms opacity; - opacity: .3; - - &.enabled { - opacity: 1; - } - } + &.sorting { + cursor: pointer; } a { diff --git a/packages/core/src/renderer/components/table/table-head.scss b/packages/core/src/renderer/components/table/table-head.scss index ca51e39d63..c58600b038 100644 --- a/packages/core/src/renderer/components/table/table-head.scss +++ b/packages/core/src/renderer/components/table/table-head.scss @@ -9,6 +9,8 @@ color: var(--tableHeaderColor); display: flex; flex-shrink: 0; + user-select: none; + white-space: nowrap; &.topLine { border-top: 1px solid var(--borderFaintColor); diff --git a/packages/core/src/renderer/components/test-utils/get-application-builder.tsx b/packages/core/src/renderer/components/test-utils/get-application-builder.tsx index 0d202aa1e5..731c5ba8c7 100644 --- a/packages/core/src/renderer/components/test-utils/get-application-builder.tsx +++ b/packages/core/src/renderer/components/test-utils/get-application-builder.tsx @@ -29,7 +29,6 @@ import type { MinimalTrayMenuItem } from "../../../main/tray/electron-tray/elect import electronTrayInjectable from "../../../main/tray/electron-tray/electron-tray.injectable"; import { getDiForUnitTesting as getRendererDi } from "../../getDiForUnitTesting"; import { getDiForUnitTesting as getMainDi } from "../../../main/getDiForUnitTesting"; -import { overrideChannels } from "../../../test-utils/channel-fakes/override-channels"; import assert from "assert"; import { openMenu } from "react-select-event"; import userEvent from "@testing-library/user-event"; @@ -40,7 +39,7 @@ import { navigateToRouteInjectionToken } from "../../../common/front-end-routing import type { LensMainExtension } from "../../../extensions/lens-main-extension"; import type { LensExtension } from "../../../extensions/lens-extension"; import extensionInjectable from "../../../extensions/extension-loader/extension/extension.injectable"; -import { renderFor } from "./renderFor"; +import { renderFor } from "@k8slens/test-utils"; import { RootFrame } from "../../frames/root-frame/root-frame"; import { ClusterFrame } from "../../frames/cluster-frame/cluster-frame"; import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable"; @@ -54,7 +53,7 @@ import { applicationWindowInjectionToken } from "../../../main/start-main-applic import closeAllWindowsInjectable from "../../../main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable"; import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable"; import type { FakeExtensionOptions } from "./get-extension-fake"; -import { getExtensionFakeForMain, getExtensionFakeForRenderer } from "./get-extension-fake"; +import { getMainExtensionFakeWith, getRendererExtensionFakeWith } from "./get-extension-fake"; import namespaceApiInjectable from "../../../common/k8s-api/endpoints/namespace.api.injectable"; import { Namespace } from "../../../common/k8s-api/endpoints"; import { getOverrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes"; @@ -71,6 +70,8 @@ import { registerFeature } from "@k8slens/feature-core"; import { applicationFeatureForElectronMain, testUtils as applicationForElectronTestUtils } from "@k8slens/application-for-electron-main"; import { applicationFeature, startApplicationInjectionToken } from "@k8slens/application"; import { testUsingFakeTime } from "../../../test-utils/use-fake-time"; +import { sendMessageToChannelInjectionToken } from "@k8slens/messaging"; +import { getMessageBridgeFake } from "@k8slens/messaging-fake-bridge"; type MainDiCallback = (container: { mainDi: DiContainer }) => void | Promise; type WindowDiCallback = (container: { windowDi: DiContainer }) => void | Promise; @@ -179,7 +180,9 @@ export const getApplicationBuilder = () => { testUsingFakeTime(); - const { overrideForWindow, sendToWindow } = overrideChannels(mainDi); + const messageBridgeFake = getMessageBridgeFake(); + + messageBridgeFake.involve(mainDi); const beforeApplicationStartCallbacks: MainDiCallback[] = []; const afterApplicationStartCallbacks: MainDiCallback[] = []; @@ -229,7 +232,8 @@ export const getApplicationBuilder = () => { const windowDi = getRendererDi(); - overrideForWindow(windowDi, windowId); + messageBridgeFake.involve(windowDi); + overrideFsWithFakes(windowDi); runInAction(() => { @@ -284,8 +288,10 @@ export const getApplicationBuilder = () => { ); }, - send: (arg) => { - sendToWindow(windowId, arg); + send: ({ channel: channelId, data }) => { + const sendMessageToChannel = mainDi.inject(sendMessageToChannelInjectionToken); + + sendMessageToChannel({ id: channelId }, data); }, reload: () => { @@ -588,49 +594,28 @@ export const getApplicationBuilder = () => { }, enable: (...extensions) => { - builder.afterWindowStart(({ windowDi }) => { - const rendererExtensionInstances = extensions.map((options) => - getExtensionFakeForRenderer( - windowDi, - options.id, - options.name, - options.rendererOptions || {}, - ), - ); + builder.afterWindowStart(action(({ windowDi }) => { + extensions + .map(getRendererExtensionFakeWith(windowDi)) + .forEach(enableExtensionFor(windowDi, rendererExtensionsStateInjectable)); + })); - rendererExtensionInstances.forEach( - enableExtensionFor(windowDi, rendererExtensionsStateInjectable), - ); - }); - - builder.afterApplicationStart(({ mainDi }) => { - const mainExtensionInstances = extensions.map((extension) => - getExtensionFakeForMain(mainDi, extension.id, extension.name, extension.mainOptions || {}), - ); - - runInAction(() => { - mainExtensionInstances.forEach( - enableExtensionFor(mainDi, mainExtensionsStateInjectable), - ); - }); - }); + builder.afterApplicationStart(action(({ mainDi }) => { + extensions + .map(getMainExtensionFakeWith(mainDi)) + .forEach(enableExtensionFor(mainDi, mainExtensionsStateInjectable)); + })); }, disable: (...extensions) => { builder.afterWindowStart(({ windowDi }) => { extensions - .map((ext) => ext.id) - .forEach( - disableExtensionFor(windowDi, rendererExtensionsStateInjectable), - ); + .forEach(disableExtensionFor(windowDi, rendererExtensionsStateInjectable)); }); builder.afterApplicationStart(({ mainDi }) => { extensions - .map((ext) => ext.id) - .forEach( - disableExtensionFor(mainDi, mainExtensionsStateInjectable), - ); + .forEach(disableExtensionFor(mainDi, mainExtensionsStateInjectable)); }); }, }, @@ -829,49 +814,29 @@ const selectOptionFor = (builder: ApplicationBuilder, menuId: string) => (labelT userEvent.click(option); }; -const enableExtensionFor = ( - di: DiContainer, - stateInjectable: Injectable, any, any>, -) => { +function enableExtensionFor(di: DiContainer, stateInjectable: Injectable, any, any>) { const extensionState = di.inject(stateInjectable); - const getExtension = (extension: LensExtension) => - di.inject(extensionInjectable, extension); + return (instance: LensExtension) => { + const extension = di.inject(extensionInjectable, instance); - return (extensionInstance: LensExtension) => { - const extension = getExtension(extensionInstance); - - runInAction(() => { - extension.register(); - extensionState.set(extensionInstance.id, extensionInstance); - }); + extension.register(); + extensionState.set(instance.id, instance); }; -}; +} -const disableExtensionFor = - ( - di: DiContainer, - stateInjectable: Injectable, unknown, void>, - ) => - (id: string) => { - const getExtension = (extension: LensExtension) => - di.inject(extensionInjectable, extension); +function disableExtensionFor(di: DiContainer, stateInjectable: Injectable, unknown, void>) { + return (extension: FakeExtensionOptions) => { + const extensionsState = di.inject(stateInjectable); + const instance = extensionsState.get(extension.id); - const extensionsState = di.inject(stateInjectable); + if (!instance) { + throw new Error(`Tried to disable extension with ID "${extension.id}", but it wasn't enabled`); + } - const instance = extensionsState.get(id); + const injectable = di.inject(extensionInjectable, instance); - if (!instance) { - throw new Error( - `Tried to disable extension with ID "${id}", but it wasn't enabled`, - ); - } - - const injectable = getExtension(instance); - - runInAction(() => { - injectable.deregister(); - - extensionsState.delete(id); - }); - }; + injectable.deregister(); + extensionsState.delete(extension.id); + }; +} diff --git a/packages/core/src/renderer/components/test-utils/get-extension-fake.ts b/packages/core/src/renderer/components/test-utils/get-extension-fake.ts index 8079207daa..675b7e93e2 100644 --- a/packages/core/src/renderer/components/test-utils/get-extension-fake.ts +++ b/packages/core/src/renderer/components/test-utils/get-extension-fake.ts @@ -27,7 +27,7 @@ export interface FakeExtensionOptions { mainOptions?: Partial; } -export const getExtensionFakeForMain = (di: DiContainer, id: string, name: string, options: Partial) => { +export const getMainExtensionFakeWith = (di: DiContainer) => ({ id, name, mainOptions = {}}: FakeExtensionOptions) => { const instance = new TestExtensionMain({ id, absolutePath: "irrelevant", @@ -44,7 +44,7 @@ export const getExtensionFakeForMain = (di: DiContainer, id: string, name: strin manifestPath: "irrelevant", }); - Object.assign(instance, options); + Object.assign(instance, mainOptions); (instance as Writable)[lensExtensionDependencies] = { fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable), @@ -56,7 +56,7 @@ export const getExtensionFakeForMain = (di: DiContainer, id: string, name: strin return instance; }; -export const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: string, options: Partial) => { +export const getRendererExtensionFakeWith = (di: DiContainer) => ({ id, name, rendererOptions = {}}: FakeExtensionOptions) => { const instance = new TestExtensionRenderer({ id, absolutePath: "irrelevant", @@ -73,7 +73,7 @@ export const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: s manifestPath: "irrelevant", }); - Object.assign(instance, options); + Object.assign(instance, rendererOptions); (instance as Writable)[lensExtensionDependencies] = { categoryRegistry: di.inject(catalogCategoryRegistryInjectable), diff --git a/packages/core/src/renderer/frames/root-frame/broadcast-that-root-frame-is-rendered.injectable.ts b/packages/core/src/renderer/frames/root-frame/broadcast-that-root-frame-is-rendered.injectable.ts index 841d19e002..dd43cbd301 100644 --- a/packages/core/src/renderer/frames/root-frame/broadcast-that-root-frame-is-rendered.injectable.ts +++ b/packages/core/src/renderer/frames/root-frame/broadcast-that-root-frame-is-rendered.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; +import { sendMessageToChannelInjectionToken } from "@k8slens/messaging"; import { rootFrameHasRenderedChannel } from "../../../common/root-frame/root-frame-rendered-channel"; const broadcastThatRootFrameIsRenderedInjectable = getInjectable({ diff --git a/packages/core/src/renderer/getDiForUnitTesting.tsx b/packages/core/src/renderer/getDiForUnitTesting.tsx index fbee046de2..ec389e1b10 100644 --- a/packages/core/src/renderer/getDiForUnitTesting.tsx +++ b/packages/core/src/renderer/getDiForUnitTesting.tsx @@ -5,7 +5,6 @@ import { noop, chunk } from "lodash/fp"; import { createContainer, isInjectable } from "@ogre-tools/injectable"; -import requestFromChannelInjectable from "./utils/channel/request-from-channel.injectable"; import { getOverrideFsWithFakes } from "../test-utils/override-fs-with-fakes"; import terminalSpawningPoolInjectable from "./components/dock/terminal/terminal-spawning-pool.injectable"; import hostedClusterIdInjectable from "./cluster-frame-context/hosted-cluster-id.injectable"; @@ -18,6 +17,8 @@ import type { GlobalOverride } from "@k8slens/test-utils"; import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; import { registerInjectableReact } from "@ogre-tools/injectable-react"; +import { registerFeature } from "@k8slens/feature-core"; +import { messagingFeature, testUtils as messagingTestUtils } from "@k8slens/messaging"; export const getDiForUnitTesting = () => { const environment = "renderer"; @@ -27,6 +28,10 @@ export const getDiForUnitTesting = () => { registerInjectableReact(di); setLegacyGlobalDiForExtensionApi(di, environment); + runInAction(() => { + registerFeature(di, messagingFeature, messagingTestUtils.messagingFeatureForUnitTesting); + }); + di.preventSideEffects(); runInAction(() => { @@ -63,8 +68,6 @@ export const getDiForUnitTesting = () => { di.override(requestAnimationFrameInjectable, () => (callback) => callback()); di.override(watchHistoryStateInjectable, () => () => () => {}); - di.override(requestFromChannelInjectable, () => () => Promise.resolve(undefined as never)); - getOverrideFsWithFakes()(di); return di; diff --git a/packages/core/src/renderer/kube-watch-api/kube-watch-api.ts b/packages/core/src/renderer/kube-watch-api/kube-watch-api.ts index f3dc0ae237..cf6e6486e8 100644 --- a/packages/core/src/renderer/kube-watch-api/kube-watch-api.ts +++ b/packages/core/src/renderer/kube-watch-api/kube-watch-api.ts @@ -8,7 +8,6 @@ import { disposer, getOrInsert, noop, WrappedAbortController } from "@k8slens/ut import { once } from "lodash"; import type { Logger } from "../../common/logger"; import type { KubeObjectStoreLoadAllParams, KubeObjectStoreSubscribeParams } from "../../common/k8s-api/kube-object.store"; -import AbortController from "abort-controller"; import type { ClusterContext } from "../cluster-frame-context/cluster-frame-context"; // Kubernetes watch-api client diff --git a/packages/core/src/renderer/kubectl/apply-all.injectable.ts b/packages/core/src/renderer/kubectl/apply-all.injectable.ts index 989017f9d9..ffc5be14ba 100644 --- a/packages/core/src/renderer/kubectl/apply-all.injectable.ts +++ b/packages/core/src/renderer/kubectl/apply-all.injectable.ts @@ -4,12 +4,12 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { kubectlApplyAllChannel, kubectlApplyAllInjectionToken } from "../../common/kube-helpers/channels"; -import requestFromChannelInjectable from "../utils/channel/request-from-channel.injectable"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; const kubectlApplyAllInjectable = getInjectable({ id: "kubectl-apply-all", instantiate: (di) => { - const requestFromChannel = di.inject(requestFromChannelInjectable); + const requestFromChannel = di.inject(requestFromChannelInjectionToken); return (req) => requestFromChannel(kubectlApplyAllChannel, req); }, diff --git a/packages/core/src/renderer/kubectl/delete-all.injectable.ts b/packages/core/src/renderer/kubectl/delete-all.injectable.ts index 586895c75c..4a3ee05f54 100644 --- a/packages/core/src/renderer/kubectl/delete-all.injectable.ts +++ b/packages/core/src/renderer/kubectl/delete-all.injectable.ts @@ -4,12 +4,12 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { kubectlDeleteAllChannel, kubectlDeleteAllInjectionToken } from "../../common/kube-helpers/channels"; -import requestFromChannelInjectable from "../utils/channel/request-from-channel.injectable"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; const kubectlDeleteAllInjectable = getInjectable({ id: "kubectl-delete-all", instantiate: (di) => { - const requestFromChannel = di.inject(requestFromChannelInjectable); + const requestFromChannel = di.inject(requestFromChannelInjectionToken); return (req) => requestFromChannel(kubectlDeleteAllChannel, req); }, diff --git a/packages/core/src/renderer/navigation/navigation-channel-listener.injectable.ts b/packages/core/src/renderer/navigation/navigation-channel-listener.injectable.ts index 1b33b4fe2c..fbb66689e1 100644 --- a/packages/core/src/renderer/navigation/navigation-channel-listener.injectable.ts +++ b/packages/core/src/renderer/navigation/navigation-channel-listener.injectable.ts @@ -2,25 +2,25 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { InjectionToken } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable"; import currentlyInClusterFrameInjectable from "../routes/currently-in-cluster-frame.injectable"; import { appNavigationChannel } from "../../common/front-end-routing/app-navigation-channel"; import { clusterFrameNavigationChannel } from "../../common/front-end-routing/cluster-frame-navigation-channel"; import focusWindowInjectable from "./focus-window.injectable"; import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token"; -import type { MessageChannel, MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token"; -import { messageChannelListenerInjectionToken } from "../../common/utils/channel/message-channel-listener-injection-token"; +import type { MessageChannel, MessageChannelListener } from "@k8slens/messaging"; +import { messageChannelListenerInjectionToken } from "@k8slens/messaging"; const navigationChannelListenerInjectable = getInjectable({ id: "navigation-channel-listener", - instantiate: (di) => { + instantiate: (di): MessageChannelListener> => { const currentlyInClusterFrame = di.inject(currentlyInClusterFrameInjectable); const focusWindow = di.inject(focusWindowInjectable); const navigateToUrl = di.inject(navigateToUrlInjectionToken); return { + id: "navigation-channel-listener", channel: currentlyInClusterFrame ? clusterFrameNavigationChannel : appNavigationChannel, @@ -34,7 +34,8 @@ const navigationChannelListenerInjectable = getInjectable({ }, }; }, - injectionToken: messageChannelListenerInjectionToken as InjectionToken>, void>, + + injectionToken: messageChannelListenerInjectionToken, }); export default navigationChannelListenerInjectable; diff --git a/packages/core/src/renderer/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts b/packages/core/src/renderer/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts deleted file mode 100644 index c11b34e7e3..0000000000 --- a/packages/core/src/renderer/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { beforeFrameStartsSecondInjectionToken } from "../../../before-frame-starts/tokens"; -import listeningOnMessageChannelsInjectable from "../../../../common/utils/channel/listening-on-message-channels.injectable"; - -const startListeningOfChannelsInjectable = getInjectable({ - id: "start-listening-of-channels-renderer", - - instantiate: (di) => ({ - run: () => { - const listeningOfChannels = di.inject(listeningOnMessageChannelsInjectable); - - listeningOfChannels.start(); - }, - }), - - injectionToken: beforeFrameStartsSecondInjectionToken, -}); - -export default startListeningOfChannelsInjectable; diff --git a/packages/core/src/renderer/utils/channel/message-to-channel.injectable.ts b/packages/core/src/renderer/utils/channel/message-to-channel.injectable.ts deleted file mode 100644 index eab7e4ec7f..0000000000 --- a/packages/core/src/renderer/utils/channel/message-to-channel.injectable.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import type { SendMessageToChannel } from "../../../common/utils/channel/message-to-channel-injection-token"; -import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; -import sendToMainInjectable from "./send-to-main.injectable"; - -const messageToChannelInjectable = getInjectable({ - id: "message-to-channel", - - instantiate: (di) => { - const sendToMain = di.inject(sendToMainInjectable); - - return ((channel, message) => { - sendToMain(channel.id, message); - }) as SendMessageToChannel; - }, - - injectionToken: sendMessageToChannelInjectionToken, -}); - -export default messageToChannelInjectable; diff --git a/packages/core/src/renderer/utils/channel/request-from-channel.injectable.ts b/packages/core/src/renderer/utils/channel/request-from-channel.injectable.ts deleted file mode 100644 index fd0815b1a3..0000000000 --- a/packages/core/src/renderer/utils/channel/request-from-channel.injectable.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import ipcRendererInjectable from "./ipc-renderer.injectable"; -import type { RequestFromChannel } from "../../../common/utils/channel/request-from-channel-injection-token"; -import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token"; - -const requestFromChannelInjectable = getInjectable({ - id: "request-from-channel", - - instantiate: (di) => { - const ipcRenderer = di.inject(ipcRendererInjectable); - - return ((channel, request) => ipcRenderer.invoke(channel.id, request)) as RequestFromChannel; - }, - - injectionToken: requestFromChannelInjectionToken, -}); - -export default requestFromChannelInjectable; diff --git a/packages/core/src/renderer/utils/channel/send-to-main.injectable.ts b/packages/core/src/renderer/utils/channel/send-to-main.injectable.ts deleted file mode 100644 index 6eb4540062..0000000000 --- a/packages/core/src/renderer/utils/channel/send-to-main.injectable.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import ipcRendererInjectable from "./ipc-renderer.injectable"; - -const sendToMainInjectable = getInjectable({ - id: "send-to-main", - - instantiate: (di) => { - const ipcRenderer = di.inject(ipcRendererInjectable); - - return (channelId: string, message: any) => { - ipcRenderer.send(channelId, message); - }; - }, -}); - -export default sendToMainInjectable; diff --git a/packages/core/src/renderer/utils/create-storage/initialize-state.injectable.ts b/packages/core/src/renderer/utils/create-storage/initialize-state.injectable.ts index 75d61908cd..804fe4ebf1 100644 --- a/packages/core/src/renderer/utils/create-storage/initialize-state.injectable.ts +++ b/packages/core/src/renderer/utils/create-storage/initialize-state.injectable.ts @@ -11,7 +11,7 @@ import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectab import loggerInjectable from "../../../common/logger.injectable"; import joinPathsInjectable from "../../../common/path/join-paths.injectable"; import setupAppPathsInjectable from "../../app-paths/setup-app-paths.injectable"; -import { beforeFrameStartsFirstInjectionToken } from "../../before-frame-starts/tokens"; +import { beforeApplicationIsLoadingInjectionToken } from "@k8slens/application"; import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable"; import { storageHelperLogPrefix } from "../storage-helper"; import lensLocalStorageStateInjectable from "./state.injectable"; @@ -68,7 +68,7 @@ const initializeStateInjectable = getInjectable({ }, runAfter: setupAppPathsInjectable, }), - injectionToken: beforeFrameStartsFirstInjectionToken, + injectionToken: beforeApplicationIsLoadingInjectionToken, }); export default initializeStateInjectable; diff --git a/packages/core/src/renderer/utils/resolve-proxy/resolve-system-proxy.injectable.ts b/packages/core/src/renderer/utils/resolve-proxy/resolve-system-proxy.injectable.ts index 5468a8c7b7..55cd0d66d5 100644 --- a/packages/core/src/renderer/utils/resolve-proxy/resolve-system-proxy.injectable.ts +++ b/packages/core/src/renderer/utils/resolve-proxy/resolve-system-proxy.injectable.ts @@ -4,14 +4,14 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { resolveSystemProxyInjectionToken } from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-injection-token"; -import requestFromChannelInjectable from "../channel/request-from-channel.injectable"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import { resolveSystemProxyChannel } from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-channel"; const resolveSystemProxyInjectable = getInjectable({ id: "resolve-system-proxy-for-renderer", instantiate: (di) => { - const requestFromChannel = di.inject(requestFromChannelInjectable); + const requestFromChannel = di.inject(requestFromChannelInjectionToken); return async (url) => requestFromChannel(resolveSystemProxyChannel, url); }, diff --git a/packages/core/src/renderer/utils/sync-box/provide-initial-values-for-sync-boxes.injectable.ts b/packages/core/src/renderer/utils/sync-box/provide-initial-values-for-sync-boxes.injectable.ts index aceb20cea4..b4eb648c87 100644 --- a/packages/core/src/renderer/utils/sync-box/provide-initial-values-for-sync-boxes.injectable.ts +++ b/packages/core/src/renderer/utils/sync-box/provide-initial-values-for-sync-boxes.injectable.ts @@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import { beforeFrameStartsSecondInjectionToken } from "../../before-frame-starts/tokens"; import { syncBoxInitialValueChannel } from "../../../common/utils/sync-box/channels"; import createSyncBoxStateInjectable from "../../../common/utils/sync-box/sync-box-state.injectable"; -import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import { runInAction } from "mobx"; import { syncBoxInjectionToken } from "../../../common/utils/sync-box/sync-box-injection-token"; import assert from "assert"; diff --git a/packages/core/src/renderer/vars/build-version/build-version.injectable.ts b/packages/core/src/renderer/vars/build-version/build-version.injectable.ts index a63a4102b0..f9ba5e34b0 100644 --- a/packages/core/src/renderer/vars/build-version/build-version.injectable.ts +++ b/packages/core/src/renderer/vars/build-version/build-version.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { createInitializableState } from "../../../common/initializable-state/create"; -import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; import { buildVersionChannel, buildVersionInjectionToken } from "../../../common/vars/build-semantic-version.injectable"; const buildVersionInjectable = createInitializableState({ diff --git a/packages/core/src/test-utils/cast.ts b/packages/core/src/test-utils/cast.ts new file mode 100644 index 0000000000..a3e7329cdd --- /dev/null +++ b/packages/core/src/test-utils/cast.ts @@ -0,0 +1,6 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export const cast = (data: Partial): T => data as T; diff --git a/packages/core/src/test-utils/channel-fakes/override-channels.ts b/packages/core/src/test-utils/channel-fakes/override-channels.ts deleted file mode 100644 index 798fcd84af..0000000000 --- a/packages/core/src/test-utils/channel-fakes/override-channels.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { DiContainer } from "@ogre-tools/injectable"; -import type { SendToViewArgs } from "../../main/start-main-application/lens-window/application-window/create-lens-window.injectable"; -import { overrideMessagingFromMainToWindow } from "./override-messaging-from-main-to-window"; -import { overrideMessagingFromWindowToMain } from "./override-messaging-from-window-to-main"; -import { overrideRequestingFromWindowToMain } from "./override-requesting-from-window-to-main"; - -export interface OverrideChannels { - overrideForWindow: (windowDi: DiContainer, windowId: string) => void; - sendToWindow: (windowId: string, args: SendToViewArgs) => void; -} - -export const overrideChannels = (mainDi: DiContainer): OverrideChannels => { - const { overrideEnlistForWindow, sendToWindow } = overrideMessagingFromMainToWindow(); - const overrideMessagingFromWindowToForWindow = overrideMessagingFromWindowToMain(mainDi); - const overrideRequestingFromWindowToMainForWindow = overrideRequestingFromWindowToMain(mainDi); - - return { - overrideForWindow: (windowDi, windowId) => { - overrideEnlistForWindow(windowDi, windowId); - overrideMessagingFromWindowToForWindow(windowDi); - overrideRequestingFromWindowToMainForWindow(windowDi); - }, - sendToWindow, - }; -}; diff --git a/packages/core/src/test-utils/channel-fakes/override-messaging-from-main-to-window.ts b/packages/core/src/test-utils/channel-fakes/override-messaging-from-main-to-window.ts deleted file mode 100644 index 1b69f13016..0000000000 --- a/packages/core/src/test-utils/channel-fakes/override-messaging-from-main-to-window.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token"; -import enlistMessageChannelListenerInjectableInRenderer from "../../renderer/utils/channel/channel-listeners/enlist-message-channel-listener.injectable"; -import type { DiContainer } from "@ogre-tools/injectable"; -import { getOrInsert, getOrInsertSet } from "@k8slens/utilities"; -import type { SendToViewArgs } from "../../main/start-main-application/lens-window/application-window/create-lens-window.injectable"; -import { deserialize, serialize } from "v8"; - -type ListenerSet = Set>; -type WindowListenerMap = Map; -type ListenerFakeMap = Map; - -export interface OverriddenWindowMessaging { - sendToWindow(windowId: string, args: SendToViewArgs): void; - overrideEnlistForWindow(windowDi: DiContainer, windowId: string): void; -} - -export const overrideMessagingFromMainToWindow = (): OverriddenWindowMessaging => { - const messageChannelListenerFakesForRenderer: ListenerFakeMap = new Map(); - - const getWindowListeners = (channelId: string, windowId: string) => { - const channelListeners = getOrInsert( - messageChannelListenerFakesForRenderer, - channelId, - new Map(), - ); - - return getOrInsertSet(channelListeners, windowId); - }; - - return { - overrideEnlistForWindow: (windowDi, windowId) => { - windowDi.override( - enlistMessageChannelListenerInjectableInRenderer, - - () => (listener) => { - const windowListeners = getWindowListeners( - listener.channel.id, - windowId, - ); - - windowListeners.add(listener); - - return () => { - windowListeners.delete(listener); - }; - }, - ); - }, - sendToWindow: (windowId, { channel, data, frameInfo }) => { - try { - data = deserialize(serialize(data)); - } catch (error) { - throw new Error(`Tried to send a message to channel "${channel}" that is not compatible with StructuredClone: ${error}`); - } - - const windowListeners = getWindowListeners(channel, windowId); - - if (frameInfo) { - throw new Error( - `Tried to send message to frame "${frameInfo.frameId}" in process "${frameInfo.processId}" using channel "${channel}" which isn't supported yet.`, - ); - } - - if (windowListeners.size === 0) { - throw new Error( - `Tried to send message to channel "${channel}" but there where no listeners. Current channels with listeners: "${[ - ...messageChannelListenerFakesForRenderer.keys(), - ].join('", "')}"`, - ); - } - - windowListeners.forEach((listener) => listener.handler(data)); - }, - }; -}; diff --git a/packages/core/src/test-utils/channel-fakes/override-messaging-from-window-to-main.ts b/packages/core/src/test-utils/channel-fakes/override-messaging-from-window-to-main.ts deleted file mode 100644 index 8adc35c4d5..0000000000 --- a/packages/core/src/test-utils/channel-fakes/override-messaging-from-window-to-main.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { DiContainer } from "@ogre-tools/injectable"; -import { deserialize, serialize } from "v8"; -import type { MessageChannel, MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token"; -import enlistMessageChannelListenerInjectableInMain from "../../main/utils/channel/channel-listeners/enlist-message-channel-listener.injectable"; -import { getOrInsertSet } from "@k8slens/utilities"; -import sendToMainInjectable from "../../renderer/utils/channel/send-to-main.injectable"; - -export const overrideMessagingFromWindowToMain = (mainDi: DiContainer) => { - const messageChannelListenerFakesForMain = new Map< - string, - Set>> - >(); - - mainDi.override( - enlistMessageChannelListenerInjectableInMain, - - () => (listener) => { - const listeners = getOrInsertSet(messageChannelListenerFakesForMain, listener.channel.id); - - listeners.add(listener); - - return () => { - listeners.delete(listener); - }; - }, - ); - - return (windowDi: DiContainer) => { - windowDi.override(sendToMainInjectable, () => (channelId, message) => { - const listeners = messageChannelListenerFakesForMain.get(channelId); - - if (!listeners || listeners.size === 0) { - throw new Error( - `Tried to send message to channel "${channelId}" but there where no listeners. Current channels with listeners: "${[ - ...messageChannelListenerFakesForMain.keys(), - ].join('", "')}"`, - ); - } - - try { - message = deserialize(serialize(message)); - } catch (error) { - throw new Error(`Tried to send a message to channel "${channelId}" that is not compatible with StructuredClone: ${error}`); - } - - listeners.forEach((listener) => listener.handler(message)); - }); - }; -}; diff --git a/packages/core/src/test-utils/channel-fakes/override-requesting-from-window-to-main.ts b/packages/core/src/test-utils/channel-fakes/override-requesting-from-window-to-main.ts deleted file mode 100644 index 70526bd84c..0000000000 --- a/packages/core/src/test-utils/channel-fakes/override-requesting-from-window-to-main.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { DiContainer } from "@ogre-tools/injectable"; -import { deserialize, serialize } from "v8"; -import type { RequestChannel } from "../../common/utils/channel/request-channel-listener-injection-token"; -import type { RequestFromChannel } from "../../common/utils/channel/request-from-channel-injection-token"; -import enlistRequestChannelListenerInjectableInMain from "../../main/utils/channel/channel-listeners/enlist-request-channel-listener.injectable"; -import type { RequestChannelListener } from "../../main/utils/channel/channel-listeners/listener-tokens"; -import requestFromChannelInjectable from "../../renderer/utils/channel/request-from-channel.injectable"; - -export const overrideRequestingFromWindowToMain = (mainDi: DiContainer) => { - const requestChannelListenerFakesForMain = new Map< - string, - RequestChannelListener> - >(); - - mainDi.override( - enlistRequestChannelListenerInjectableInMain, - - () => (listener) => { - if (requestChannelListenerFakesForMain.has(listener.channel.id)) { - throw new Error( - `Tried to enlist listener for channel "${listener.channel.id}", but it was already enlisted`, - ); - } - - requestChannelListenerFakesForMain.set(listener.channel.id, listener); - - return () => { - requestChannelListenerFakesForMain.delete(listener.channel.id); - }; - }, - ); - - return (windowDi: DiContainer) => { - windowDi.override( - requestFromChannelInjectable, - - () => (async (channel, request) => { - const requestListener = requestChannelListenerFakesForMain.get(channel.id); - - if (!requestListener) { - throw new Error( - `Tried to get value from channel "${channel.id}", but no listeners were registered`, - ); - } - - try { - request = deserialize(serialize(request)); - } catch (error) { - throw new Error(`Tried to request from channel "${channel.id}" with data that is not compatible with StructuredClone: ${error}`); - } - - return requestListener.handler(request); - }) as RequestFromChannel, - ); - }; -}; diff --git a/packages/core/src/test-utils/mock-interface.ts b/packages/core/src/test-utils/mock-interface.ts new file mode 100644 index 0000000000..e9870282b3 --- /dev/null +++ b/packages/core/src/test-utils/mock-interface.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AsyncFnMock } from "@async-fn/jest"; + +type GetMockedType = + T extends (...args: any[]) => Promise + ? AsyncFnMock + : T extends (...args: any[]) => any + ? jest.MockedFunction + : T; + +export type Mocked = { + -readonly [P in keyof T]: GetMockedType; +}; diff --git a/packages/infrastructure/eslint-config/bin/lint b/packages/infrastructure/eslint-config/bin/lint index 2abf180561..22d262e0cb 100755 --- a/packages/infrastructure/eslint-config/bin/lint +++ b/packages/infrastructure/eslint-config/bin/lint @@ -8,13 +8,7 @@ const shouldDoTheFix = argv.includes('--fix'); try { execSync(`eslint ${shouldDoTheFix ? "--fix " : " "}--ext ts,tsx --max-warnings=0 .`); } catch (error) { - console.log(error.stdout.toString()); -} + console.error(error.stdout.toString()); -try { - const result = execSync(`prettier ${shouldDoTheFix ? "--write" : "--check"} "**/*.{js,ts,tsx}"`); - - console.log(result.toString()); -} catch (error) { - console.log(error.stdout.toString()); + process.exit(1); } diff --git a/packages/infrastructure/eslint-config/eslint-config.js b/packages/infrastructure/eslint-config/eslint-config.js index 561541838d..2e59a31da7 100644 --- a/packages/infrastructure/eslint-config/eslint-config.js +++ b/packages/infrastructure/eslint-config/eslint-config.js @@ -15,6 +15,7 @@ module.exports = { "xss", "no-unsanitized" ], + ignorePatterns: ["dist/*"], rules: { "react/react-in-jsx-scope": 0, "security/detect-object-injection": "off", @@ -125,7 +126,7 @@ module.exports = { "@typescript-eslint/ban-types": "off", "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/interface-name-prefix": "off", "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-module-boundary-types": "off", diff --git a/packages/infrastructure/jest/jest-28-resolver.js b/packages/infrastructure/jest/jest-28-resolver.js new file mode 100644 index 0000000000..118a17f09d --- /dev/null +++ b/packages/infrastructure/jest/jest-28-resolver.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +module.exports = (path, options) => { + // Call the defaultResolver, so we leverage its cache, error handling, etc. + return options.defaultResolver(path, { + ...options, + // Use packageFilter to process parsed `package.json` before the resolution (see https://www.npmjs.com/package/resolve#resolveid-opts-cb) + packageFilter: pkg => { + // This is a workaround for https://github.com/uuidjs/uuid/pull/616 + // + // jest-environment-jsdom 28+ tries to use browser exports instead of default exports, + // but uuid only offers an ESM browser export and not a CommonJS one. Jest does not yet + // support ESM modules natively, so this causes a Jest error related to trying to parse + // "export" syntax. + // + // This workaround prevents Jest from considering uuid's module-based exports at all; + // it falls back to uuid's CommonJS+node "main" property. + // + // Once we're able to migrate our Jest config to ESM and a browser crypto + // implementation is available for the browser+ESM version of uuid to use (eg, via + // https://github.com/jsdom/jsdom/pull/3352 or a similar polyfill), this can go away. + switch (pkg.name) { + case "uuid": + delete pkg["exports"]; + delete pkg["module"]; + break; + } + + return pkg; + }, + }); +}; diff --git a/packages/infrastructure/jest/monorepo-package-config.js b/packages/infrastructure/jest/monorepo-package-config.js index 6c68fcd8d8..17b24ba342 100644 --- a/packages/infrastructure/jest/monorepo-package-config.js +++ b/packages/infrastructure/jest/monorepo-package-config.js @@ -1,5 +1,9 @@ +const path = require('path'); + module.exports = (rootDir) => { const shared = { + "resolver": path.join(__dirname, "jest-28-resolver.js"), + transform: { "^.+\\.(t|j)sx?$": ["@swc/jest", { cwd: rootDir }], }, @@ -15,6 +19,8 @@ module.exports = (rootDir) => { collectCoverageFrom: [ "/src/**/*.{ts,tsx}", "!/src/**/*.no-coverage.ts", + "!/src/**/test-utils/**/*.{ts,tsx}", + "!/src/**/index.{ts,tsx}", ], moduleNameMapper: { diff --git a/packages/legacy-extension-example/jest.config.js b/packages/legacy-extension-example/jest.config.js index c6074967eb..6d3d6ff231 100644 --- a/packages/legacy-extension-example/jest.config.js +++ b/packages/legacy-extension-example/jest.config.js @@ -1 +1,2 @@ -module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode; +module.exports = + require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode; diff --git a/packages/legacy-extension-example/package.json b/packages/legacy-extension-example/package.json index 857be916fa..e175eeb1f1 100644 --- a/packages/legacy-extension-example/package.json +++ b/packages/legacy-extension-example/package.json @@ -33,7 +33,6 @@ "clean": "rimraf dist/", "build": "webpack --config webpack.ts", "dev": "webpack --mode=development --watch --config webpack.ts", - "test": "jest --coverage --runInBand", "lint": "lens-lint", "lint:fix": "lens-lint --fix" }, diff --git a/packages/open-lens/package.json b/packages/open-lens/package.json index c6fe1d227a..577c1a2a6b 100644 --- a/packages/open-lens/package.json +++ b/packages/open-lens/package.json @@ -96,7 +96,7 @@ }, "build": { "npmRebuild": false, - "electronVersion": "19.1.9", + "electronVersion": "22.3.3", "generateUpdatesFilesForAllChannels": true, "files": [ "static/**/*", @@ -203,7 +203,11 @@ "@k8slens/generate-tray-icons": "^6.5.0-alpha.1", "@k8slens/legacy-extension-example": "^1.0.0-alpha.1", "@k8slens/legacy-extensions": "^1.0.0-alpha.1", + "@k8slens/messaging": "^1.0.0-alpha.1", + "@k8slens/messaging-for-main": "^1.0.0-alpha.1", + "@k8slens/messaging-for-renderer": "^1.0.0-alpha.1", "@k8slens/run-many": "^1.0.0-alpha.1", + "@k8slens/startable-stoppable": "^1.0.0-alpha.1", "@k8slens/test-utils": "^1.0.0-alpha.1", "@k8slens/utilities": "^1.0.0-alpha.1", "@ogre-tools/fp": "^15.1.2", @@ -241,14 +245,13 @@ "@types/webpack-dev-server": "^4.7.2", "@types/webpack-env": "^1.18.0", "@types/webpack-node-externals": "2.5.3", - "abort-controller": "^3.0.0", "autoprefixer": "^10.4.13", "circular-dependency-plugin": "^5.2.2", "concurrently": "^7.6.0", "copy-webpack-plugin": "^11.0.0", "cross-env": "^7.0.3", "css-loader": "^6.7.2", - "electron": "^19.1.9", + "electron": "^22.3.3", "electron-builder": "^23.6.0", "electron-notarize": "^0.3.0", "esbuild-loader": "^2.20.0", diff --git a/packages/open-lens/src/main/index.ts b/packages/open-lens/src/main/index.ts index 5329be4595..84e187ba5c 100644 --- a/packages/open-lens/src/main/index.ts +++ b/packages/open-lens/src/main/index.ts @@ -10,6 +10,7 @@ import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; import { registerFeature } from "@k8slens/feature-core"; import { applicationFeature, startApplicationInjectionToken } from '@k8slens/application' import { applicationFeatureForElectronMain } from '@k8slens/application-for-electron-main' +import { messagingFeatureForMain } from "@k8slens/messaging-for-main"; const environment = "main"; @@ -20,7 +21,7 @@ registerMobX(di); runInAction(() => { registerLensCore(di, environment); - registerFeature(di, applicationFeature, applicationFeatureForElectronMain); + registerFeature(di, applicationFeature, applicationFeatureForElectronMain, messagingFeatureForMain); try { autoRegister({ diff --git a/packages/open-lens/src/renderer/index.ts b/packages/open-lens/src/renderer/index.ts index 9f0770de26..207fc2265c 100644 --- a/packages/open-lens/src/renderer/index.ts +++ b/packages/open-lens/src/renderer/index.ts @@ -14,6 +14,7 @@ import { import { createContainer } from "@ogre-tools/injectable"; import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; import { registerInjectableReact } from "@ogre-tools/injectable-react"; +import { messagingFeatureForRenderer } from "@k8slens/messaging-for-renderer"; const environment = "renderer"; @@ -23,7 +24,7 @@ runInAction(() => { registerMobX(di); registerInjectableReact(di); registerLensCore(di, environment); - registerFeature(di, applicationFeature); + registerFeature(di, applicationFeature, messagingFeatureForRenderer); autoRegister({ di, diff --git a/packages/release-tool/src/index.ts b/packages/release-tool/src/index.ts index 7d795acc38..9e30cf29a5 100755 --- a/packages/release-tool/src/index.ts +++ b/packages/release-tool/src/index.ts @@ -120,7 +120,7 @@ function findClosestVersionTagLessThanVersion(tags: string[], version: SemVer): .filter(isDefined) .filter(version => !version.prerelease.includes("cron")) .sort(semver.rcompare) - .filter(version => semver.lte(version, version)); + .filter(v => semver.lte(v, version)); assert(lessThanTags.length > 0, `Cannot find version tag less than ${version.format()}`); diff --git a/packages/technical-features/application/agnostic/index.ts b/packages/technical-features/application/agnostic/index.ts index 239e938cd2..2e5595a816 100644 --- a/packages/technical-features/application/agnostic/index.ts +++ b/packages/technical-features/application/agnostic/index.ts @@ -5,5 +5,5 @@ export * from "./src/start-application/time-slots"; export type { StartApplication } from "./src/start-application/start-application.injectable"; export { startApplicationInjectionToken } from "./src/start-application/start-application.injectable"; -export { applicationInformationToken } from "./src/application-information-token"; -export type { ApplicationInformation } from "./src/application-information-token"; +export { applicationInformationToken } from "./src/application-information-token.no-coverage"; +export type { ApplicationInformation } from "./src/application-information-token.no-coverage"; diff --git a/packages/technical-features/application/agnostic/package.json b/packages/technical-features/application/agnostic/package.json index 78069b1ae6..ab869a87c0 100644 --- a/packages/technical-features/application/agnostic/package.json +++ b/packages/technical-features/application/agnostic/package.json @@ -26,7 +26,7 @@ "scripts": { "build": "webpack", "dev": "webpack --mode=development --watch", - "test": "jest --coverage --runInBand", + "test:unit": "jest --coverage --runInBand", "lint": "lens-lint", "lint:fix": "lens-lint --fix" }, diff --git a/packages/technical-features/application/agnostic/src/application-information-token.ts b/packages/technical-features/application/agnostic/src/application-information-token.no-coverage.ts similarity index 100% rename from packages/technical-features/application/agnostic/src/application-information-token.ts rename to packages/technical-features/application/agnostic/src/application-information-token.no-coverage.ts diff --git a/packages/technical-features/application/agnostic/src/start-application/start-application.injectable.ts b/packages/technical-features/application/agnostic/src/start-application/start-application.injectable.ts index 895462f08e..87f6d1f7db 100644 --- a/packages/technical-features/application/agnostic/src/start-application/start-application.injectable.ts +++ b/packages/technical-features/application/agnostic/src/start-application/start-application.injectable.ts @@ -4,17 +4,18 @@ import * as timeSlots from "./time-slots"; export type StartApplication = () => Promise; -export const startApplicationInjectionToken = - getInjectionToken({ - id: "start-application-injection-token", - }); +export const startApplicationInjectionToken = getInjectionToken({ + id: "start-application-injection-token", +}); const startApplicationInjectable = getInjectable({ id: "start-application", instantiate: (di): StartApplication => { - const runManyAsync = runManyFor(di) - const beforeApplicationIsLoading = runManyAsync(timeSlots.beforeApplicationIsLoadingInjectionToken); + const runManyAsync = runManyFor(di); + const beforeApplicationIsLoading = runManyAsync( + timeSlots.beforeApplicationIsLoadingInjectionToken, + ); const onLoadOfApplication = runManyAsync(timeSlots.onLoadOfApplicationInjectionToken); const afterApplicationIsLoaded = runManyAsync(timeSlots.afterApplicationIsLoadedInjectionToken); diff --git a/packages/technical-features/application/agnostic/src/start-application/starting-of-application.test.ts b/packages/technical-features/application/agnostic/src/start-application/starting-of-application.test.ts index 69219d4a08..672e3a0f01 100644 --- a/packages/technical-features/application/agnostic/src/start-application/starting-of-application.test.ts +++ b/packages/technical-features/application/agnostic/src/start-application/starting-of-application.test.ts @@ -4,6 +4,7 @@ import { applicationFeature } from "../feature"; import { startApplicationInjectionToken } from "./start-application.injectable"; import * as timeSlots from "./time-slots"; import asyncFn, { AsyncFnMock } from "@async-fn/jest"; +import { getPromiseStatus } from "@k8slens/test-utils"; describe("starting-of-application", () => { let di: DiContainer; @@ -47,10 +48,12 @@ describe("starting-of-application", () => { }); describe("when application is started", () => { + let actualPromise: Promise; + beforeEach(() => { const startApplication = di.inject(startApplicationInjectionToken); - void startApplication(); + actualPromise = startApplication(); }); it("calls runnables registered in before application is loading", () => { @@ -66,10 +69,28 @@ describe("starting-of-application", () => { expect(onLoadOfApplicationMock).toHaveBeenCalled(); }); - it("when runnables in before application is loading resolve, calls runnables registered in after load of application", async () => { - await onLoadOfApplicationMock.resolve(); + describe("when runnables in before application is loading resolve", () => { + beforeEach(async () => { + await onLoadOfApplicationMock.resolve(); + }); - expect(afterApplicationIsLoadedMock).toHaveBeenCalled(); + it("calls runnables registered in after load of application", async () => { + expect(afterApplicationIsLoadedMock).toHaveBeenCalled(); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("when runnables in after application is loaded resolve, resolves", async () => { + await afterApplicationIsLoadedMock.resolve(); + + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(true); + }); }); }); }); diff --git a/packages/technical-features/application/electron-main/index.ts b/packages/technical-features/application/electron-main/index.ts index 238b564a47..da354760c2 100644 --- a/packages/technical-features/application/electron-main/index.ts +++ b/packages/technical-features/application/electron-main/index.ts @@ -1,4 +1,4 @@ -import { overrideSideEffectsWithFakes } from "./src/override-side-effects-with-fakes"; +import { overrideSideEffectsWithFakes } from "./src/test-utils/override-side-effects-with-fakes"; export * from "./src/start-application/time-slots"; diff --git a/packages/technical-features/application/electron-main/package.json b/packages/technical-features/application/electron-main/package.json index 625c23fe93..96d649a362 100644 --- a/packages/technical-features/application/electron-main/package.json +++ b/packages/technical-features/application/electron-main/package.json @@ -26,7 +26,7 @@ "scripts": { "build": "webpack", "dev": "webpack --mode=development --watch", - "test": "jest --coverage --runInBand", + "test:unit": "jest --coverage --runInBand", "lint": "lens-lint", "lint:fix": "lens-lint --fix" }, @@ -35,7 +35,7 @@ "@k8slens/feature-core": "^6.5.0-alpha.0", "@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", - "electron": "^19.1.9" + "electron": "^22.3.3" }, "devDependencies": { "@async-fn/jest": "^1.6.4", diff --git a/packages/technical-features/application/electron-main/src/override-side-effects-with-fakes.ts b/packages/technical-features/application/electron-main/src/test-utils/override-side-effects-with-fakes.ts similarity index 69% rename from packages/technical-features/application/electron-main/src/override-side-effects-with-fakes.ts rename to packages/technical-features/application/electron-main/src/test-utils/override-side-effects-with-fakes.ts index a795dbd04b..8873bf7f99 100644 --- a/packages/technical-features/application/electron-main/src/override-side-effects-with-fakes.ts +++ b/packages/technical-features/application/electron-main/src/test-utils/override-side-effects-with-fakes.ts @@ -1,5 +1,5 @@ import type { DiContainer } from "@ogre-tools/injectable"; -import whenAppIsReadyInjectable from "./start-application/when-app-is-ready.injectable"; +import whenAppIsReadyInjectable from "../start-application/when-app-is-ready.injectable"; export const overrideSideEffectsWithFakes = (di: DiContainer) => { di.override(whenAppIsReadyInjectable, () => () => Promise.resolve()); diff --git a/packages/technical-features/application/legacy-extensions/jest.config.js b/packages/technical-features/application/legacy-extensions/jest.config.js index c6074967eb..6d3d6ff231 100644 --- a/packages/technical-features/application/legacy-extensions/jest.config.js +++ b/packages/technical-features/application/legacy-extensions/jest.config.js @@ -1 +1,2 @@ -module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode; +module.exports = + require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode; diff --git a/packages/technical-features/application/legacy-extensions/package.json b/packages/technical-features/application/legacy-extensions/package.json index 5b7c436da5..6e92ca7c02 100644 --- a/packages/technical-features/application/legacy-extensions/package.json +++ b/packages/technical-features/application/legacy-extensions/package.json @@ -26,7 +26,6 @@ "scripts": { "build": "webpack", "dev": "webpack --mode=development --watch", - "test": "jest --coverage --runInBand", "lint": "lens-lint", "lint:fix": "lens-lint --fix" }, diff --git a/packages/technical-features/feature-core/package.json b/packages/technical-features/feature-core/package.json index 59f4186c6d..cabb6e17a0 100644 --- a/packages/technical-features/feature-core/package.json +++ b/packages/technical-features/feature-core/package.json @@ -26,7 +26,7 @@ "scripts": { "build": "webpack", "dev": "webpack --mode=development --watch", - "test": "jest --coverage --runInBand", + "test:unit": "jest --coverage --runInBand", "lint": "lens-lint", "lint:fix": "lens-lint --fix" }, diff --git a/packages/technical-features/messaging/agnostic/.eslintrc.json b/packages/technical-features/messaging/agnostic/.eslintrc.json new file mode 100644 index 0000000000..b15115cb69 --- /dev/null +++ b/packages/technical-features/messaging/agnostic/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "@k8slens/eslint-config/eslint", + "parserOptions": { + "project": "./tsconfig.json" + } +} diff --git a/packages/technical-features/messaging/agnostic/.prettierrc b/packages/technical-features/messaging/agnostic/.prettierrc new file mode 100644 index 0000000000..edd47b479e --- /dev/null +++ b/packages/technical-features/messaging/agnostic/.prettierrc @@ -0,0 +1 @@ +"@k8slens/eslint-config/prettier" diff --git a/packages/technical-features/messaging/agnostic/index.ts b/packages/technical-features/messaging/agnostic/index.ts new file mode 100644 index 0000000000..75b20812f1 --- /dev/null +++ b/packages/technical-features/messaging/agnostic/index.ts @@ -0,0 +1,2 @@ +export * from "./src/features/actual"; +export * as testUtils from "./src/features/unit-testing"; diff --git a/packages/technical-features/messaging/agnostic/jest.config.js b/packages/technical-features/messaging/agnostic/jest.config.js new file mode 100644 index 0000000000..38d54ab7b6 --- /dev/null +++ b/packages/technical-features/messaging/agnostic/jest.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact; diff --git a/packages/technical-features/messaging/agnostic/package.json b/packages/technical-features/messaging/agnostic/package.json new file mode 100644 index 0000000000..085304d4e0 --- /dev/null +++ b/packages/technical-features/messaging/agnostic/package.json @@ -0,0 +1,49 @@ +{ + "name": "@k8slens/messaging", + "private": false, + "version": "1.0.0-alpha.1", + "description": "An abstraction for messaging between Lens environments", + "type": "commonjs", + + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/lensapp/lens.git" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "author": { + "name": "OpenLens Authors", + "email": "info@k8slens.dev" + }, + "license": "MIT", + "homepage": "https://github.com/lensapp/lens", + "scripts": { + "build": "webpack", + "dev": "webpack --mode=development --watch", + "test:unit": "jest --coverage --runInBand", + "lint:fix": "lens-lint --fix", + "lint": "lens-lint" + }, + "peerDependencies": { + "@k8slens/application": "^6.5.0-alpha.0", + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@k8slens/startable-stoppable": "^1.0.0-alpha.1", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", + "@ogre-tools/injectable-extension-for-mobx": "^15.1.2", + "lodash": "^4.17.21", + "mobx": "^6.7.0" + }, + + "devDependencies": { + "@k8slens/eslint-config": "^6.5.0-alpha.1" + } +} diff --git a/packages/core/src/common/utils/channel/channel-injection-token.ts b/packages/technical-features/messaging/agnostic/src/features/actual/channel.no-coverage.ts similarity index 52% rename from packages/core/src/common/utils/channel/channel-injection-token.ts rename to packages/technical-features/messaging/agnostic/src/features/actual/channel.no-coverage.ts index 6006290f89..62a2ea1490 100644 --- a/packages/core/src/common/utils/channel/channel-injection-token.ts +++ b/packages/technical-features/messaging/agnostic/src/features/actual/channel.no-coverage.ts @@ -1,12 +1,5 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - - export interface Channel { id: string; _messageTemplate?: MessageTemplate; _returnTemplate?: ReturnTemplate; } - diff --git a/packages/technical-features/messaging/agnostic/src/features/actual/feature.ts b/packages/technical-features/messaging/agnostic/src/features/actual/feature.ts new file mode 100644 index 0000000000..5f302e1bcd --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/features/actual/feature.ts @@ -0,0 +1,18 @@ +import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration"; +import { applicationFeature } from "@k8slens/application"; +import { getFeature } from "@k8slens/feature-core"; + +export const messagingFeature = getFeature({ + id: "messaging", + + dependencies: [applicationFeature], + + register: (di) => { + autoRegister({ + di, + targetModule: module, + + getRequireContexts: () => [require.context("./", true, /\.injectable\.(ts|tsx)$/)], + }); + }, +}); diff --git a/packages/technical-features/messaging/agnostic/src/features/actual/index.ts b/packages/technical-features/messaging/agnostic/src/features/actual/index.ts new file mode 100644 index 0000000000..e8209f26f0 --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/features/actual/index.ts @@ -0,0 +1,53 @@ +/* c8 ignore next */ +export { messagingFeature } from "./feature"; + +export { getRequestChannel } from "./request/get-request-channel"; +export { getMessageChannel } from "./message/get-message-channel"; + +export { requestFromChannelInjectionToken } from "./request/request-from-channel-injection-token"; + +export type { Channel } from "./channel.no-coverage"; + +export { sendMessageToChannelInjectionToken } from "./message/message-to-channel-injection-token"; +export type { SendMessageToChannel } from "./message/message-to-channel-injection-token"; + +export type { + GetMessageChannelListenerInfo, + MessageChannel, + MessageChannelHandler, + MessageChannelListener, +} from "./message/message-channel-listener-injection-token"; + +export { + messageChannelListenerInjectionToken, + getMessageChannelListenerInjectable, +} from "./message/message-channel-listener-injection-token"; + +export type { + RequestChannel, + RequestChannelHandler, +} from "./request/request-channel-listener-injection-token"; + +export type { + RequestFromChannel, + ChannelRequester, +} from "./request/request-from-channel-injection-token"; + +export type { EnlistMessageChannelListener } from "./message/enlist-message-channel-listener-injection-token"; +export { enlistMessageChannelListenerInjectionToken } from "./message/enlist-message-channel-listener-injection-token"; + +export type { EnlistRequestChannelListener } from "./request/enlist-request-channel-listener-injection-token"; +export { enlistRequestChannelListenerInjectionToken } from "./request/enlist-request-channel-listener-injection-token"; + +export type { ListeningOfChannels } from "./listening-of-channels/listening-of-channels.injectable"; +export { listeningOfChannelsInjectionToken } from "./listening-of-channels/listening-of-channels.injectable"; + +export type { + GetRequestChannelListenerInjectableInfo, + RequestChannelListener, +} from "./request/request-channel-listener-injection-token"; + +export { + getRequestChannelListenerInjectable, + requestChannelListenerInjectionToken, +} from "./request/request-channel-listener-injection-token"; diff --git a/packages/technical-features/messaging/agnostic/src/features/actual/listening-of-channels/listening-of-channels.injectable.ts b/packages/technical-features/messaging/agnostic/src/features/actual/listening-of-channels/listening-of-channels.injectable.ts new file mode 100644 index 0000000000..c28d6cbc5a --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/features/actual/listening-of-channels/listening-of-channels.injectable.ts @@ -0,0 +1,106 @@ +import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; +import { enlistMessageChannelListenerInjectionToken } from "../message/enlist-message-channel-listener-injection-token"; + +import { getStartableStoppable, StartableStoppable } from "@k8slens/startable-stoppable"; + +import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; +import { IComputedValue, reaction } from "mobx"; + +import { messageChannelListenerInjectionToken } from "../message/message-channel-listener-injection-token"; +import { requestChannelListenerInjectionToken } from "../request/request-channel-listener-injection-token"; +import { enlistRequestChannelListenerInjectionToken } from "../request/enlist-request-channel-listener-injection-token"; +import type { Channel } from "../channel.no-coverage"; + +export type ListeningOfChannels = StartableStoppable; +export const listeningOfChannelsInjectionToken = getInjectionToken({ + id: "listening-of-channels-injection-token", +}); + +const listening = }>( + channelListeners: IComputedValue, + enlistChannelListener: (listener: T) => () => void, + getId: (listener: T) => string, +) => { + const listenerDisposers = new Map void>(); + + const reactionDisposer = reaction( + () => channelListeners.get(), + (newValues, oldValues = []) => { + const addedListeners = newValues.filter( + (newValue) => !oldValues.some((oldValue) => oldValue.id === newValue.id), + ); + + const removedListeners = oldValues.filter( + (oldValue) => !newValues.some((newValue) => newValue.id === oldValue.id), + ); + + addedListeners.forEach((listener) => { + const id = getId(listener); + + if (listenerDisposers.has(id)) { + throw new Error( + `Tried to add listener for channel "${listener.channel.id}" but listener already exists.`, + ); + } + + const disposer = enlistChannelListener(listener); + + listenerDisposers.set(id, disposer); + }); + + removedListeners.forEach((listener) => { + const dispose = listenerDisposers.get(getId(listener)); + + dispose?.(); + + listenerDisposers.delete(getId(listener)); + }); + }, + + { fireImmediately: true }, + ); + + return () => { + reactionDisposer(); + listenerDisposers.forEach((dispose) => dispose()); + }; +}; + +const listeningOfChannelsInjectable = getInjectable({ + id: "listening-of-channels", + + instantiate: (di) => { + const enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken); + + const enlistRequestChannelListener = di.inject(enlistRequestChannelListenerInjectionToken); + + const computedInjectMany = di.inject(computedInjectManyInjectable); + + const messageChannelListeners = computedInjectMany(messageChannelListenerInjectionToken); + + const requestChannelListeners = computedInjectMany(requestChannelListenerInjectionToken); + + return getStartableStoppable("listening-of-channels", () => { + const stopListeningOfMessageChannels = listening( + messageChannelListeners, + enlistMessageChannelListener, + (x) => x.id, + ); + + const stopListeningOfRequestChannels = listening( + requestChannelListeners, + enlistRequestChannelListener, + (x) => x.channel.id, + ); + + return () => { + stopListeningOfMessageChannels(); + stopListeningOfRequestChannels(); + }; + }); + }, + + injectionToken: listeningOfChannelsInjectionToken, +}); + +export default listeningOfChannelsInjectable; diff --git a/packages/technical-features/messaging/agnostic/src/features/actual/listening-of-channels/start-listening-of-channels.injectable.ts b/packages/technical-features/messaging/agnostic/src/features/actual/listening-of-channels/start-listening-of-channels.injectable.ts new file mode 100644 index 0000000000..eb265b1ca2 --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/features/actual/listening-of-channels/start-listening-of-channels.injectable.ts @@ -0,0 +1,21 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import { onLoadOfApplicationInjectionToken } from "@k8slens/application"; +import { listeningOfChannelsInjectionToken } from "./listening-of-channels.injectable"; + +const startListeningOfChannelsInjectable = getInjectable({ + id: "start-listening-of-channels", + + instantiate: (di) => { + const listeningOfChannels = di.inject(listeningOfChannelsInjectionToken); + + return { + run: async () => { + await listeningOfChannels.start(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default startListeningOfChannelsInjectable; diff --git a/packages/technical-features/messaging/agnostic/src/features/actual/message/enlist-message-channel-listener-injection-token.ts b/packages/technical-features/messaging/agnostic/src/features/actual/message/enlist-message-channel-listener-injection-token.ts new file mode 100644 index 0000000000..9ec6f8b93a --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/features/actual/message/enlist-message-channel-listener-injection-token.ts @@ -0,0 +1,15 @@ +import { getInjectionToken } from "@ogre-tools/injectable"; + +import type { + MessageChannel, + MessageChannelListener, +} from "./message-channel-listener-injection-token"; + +export type EnlistMessageChannelListener = ( + listener: MessageChannelListener>, +) => () => void; + +export const enlistMessageChannelListenerInjectionToken = + getInjectionToken({ + id: "listening-to-a-message-channel", + }); diff --git a/packages/technical-features/messaging/agnostic/src/features/actual/message/get-message-channel.ts b/packages/technical-features/messaging/agnostic/src/features/actual/message/get-message-channel.ts new file mode 100644 index 0000000000..076a7af464 --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/features/actual/message/get-message-channel.ts @@ -0,0 +1,5 @@ +import type { MessageChannel } from "./message-channel-listener-injection-token"; + +export const getMessageChannel = (id: string): MessageChannel => ({ + id, +}); diff --git a/packages/technical-features/messaging/agnostic/src/features/actual/message/message-channel-listener-injection-token.ts b/packages/technical-features/messaging/agnostic/src/features/actual/message/message-channel-listener-injection-token.ts new file mode 100644 index 0000000000..0558bf6598 --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/features/actual/message/message-channel-listener-injection-token.ts @@ -0,0 +1,51 @@ +import type { DiContainerForInjection } from "@ogre-tools/injectable"; +import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; + +export interface MessageChannel { + id: string; + _messageSignature?: Message; +} + +export type ExtraData = { processId: number; frameId: number }; + +export type MessageChannelHandler = Channel extends MessageChannel + ? (message: Message, data?: ExtraData) => void + : never; + +export interface MessageChannelListener { + id: string; + channel: Channel; + handler: MessageChannelHandler; +} + +export const messageChannelListenerInjectionToken = getInjectionToken< + MessageChannelListener> +>({ + id: "message-channel-listener", +}); + +export interface GetMessageChannelListenerInfo, Message> { + id: string; + channel: Channel; + getHandler: (di: DiContainerForInjection) => MessageChannelHandler; + causesSideEffects?: boolean; +} + +export const getMessageChannelListenerInjectable = < + Channel extends MessageChannel, + Message, +>( + info: GetMessageChannelListenerInfo, +) => + getInjectable({ + id: `${info.channel.id}-message-listener-${info.id}`, + + instantiate: (di): MessageChannelListener => ({ + id: `${info.channel.id}-message-listener-${info.id}`, + channel: info.channel, + handler: info.getHandler(di), + }), + + injectionToken: messageChannelListenerInjectionToken, + causesSideEffects: info.causesSideEffects, + }); diff --git a/packages/core/src/common/utils/channel/message-to-channel-injection-token.ts b/packages/technical-features/messaging/agnostic/src/features/actual/message/message-to-channel-injection-token.ts similarity index 55% rename from packages/core/src/common/utils/channel/message-to-channel-injection-token.ts rename to packages/technical-features/messaging/agnostic/src/features/actual/message/message-to-channel-injection-token.ts index 3ffd75f4f7..9cb5df4986 100644 --- a/packages/core/src/common/utils/channel/message-to-channel-injection-token.ts +++ b/packages/technical-features/messaging/agnostic/src/features/actual/message/message-to-channel-injection-token.ts @@ -1,7 +1,3 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ import { getInjectionToken } from "@ogre-tools/injectable"; import type { MessageChannel } from "./message-channel-listener-injection-token"; @@ -10,12 +6,6 @@ export interface SendMessageToChannel { (channel: MessageChannel, message: Message): void; } -export type MessageChannelSender = Channel extends MessageChannel - ? () => void - : Channel extends MessageChannel - ? (message: Message) => void - : never; - export const sendMessageToChannelInjectionToken = getInjectionToken({ id: "send-message-to-message-channel", }); diff --git a/packages/technical-features/messaging/agnostic/src/features/actual/request/enlist-request-channel-listener-injection-token.ts b/packages/technical-features/messaging/agnostic/src/features/actual/request/enlist-request-channel-listener-injection-token.ts new file mode 100644 index 0000000000..cdb3ac97d5 --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/features/actual/request/enlist-request-channel-listener-injection-token.ts @@ -0,0 +1,15 @@ +import { getInjectionToken } from "@ogre-tools/injectable"; + +import type { + RequestChannel, + RequestChannelListener, +} from "./request-channel-listener-injection-token"; + +export type EnlistRequestChannelListener = ( + listener: RequestChannelListener>, +) => () => void; + +export const enlistRequestChannelListenerInjectionToken = + getInjectionToken({ + id: "listening-to-a-request-channel", + }); diff --git a/packages/technical-features/messaging/agnostic/src/features/actual/request/get-request-channel.ts b/packages/technical-features/messaging/agnostic/src/features/actual/request/get-request-channel.ts new file mode 100644 index 0000000000..c0ee40bcf4 --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/features/actual/request/get-request-channel.ts @@ -0,0 +1,7 @@ +import type { RequestChannel } from "./request-channel-listener-injection-token"; + +export const getRequestChannel = ( + id: string, +): RequestChannel => ({ + id, +}); diff --git a/packages/core/src/main/utils/channel/channel-listeners/listener-tokens.ts b/packages/technical-features/messaging/agnostic/src/features/actual/request/request-channel-listener-injection-token.ts similarity index 56% rename from packages/core/src/main/utils/channel/channel-listeners/listener-tokens.ts rename to packages/technical-features/messaging/agnostic/src/features/actual/request/request-channel-listener-injection-token.ts index a3cd5af4f4..2ec76ff546 100644 --- a/packages/core/src/main/utils/channel/channel-listeners/listener-tokens.ts +++ b/packages/technical-features/messaging/agnostic/src/features/actual/request/request-channel-listener-injection-token.ts @@ -1,23 +1,28 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - import type { DiContainerForInjection } from "@ogre-tools/injectable"; import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; -import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; -export type RequestChannelHandler = Channel extends RequestChannel +export interface RequestChannel { + id: string; + _requestSignature?: Request; + _responseSignature?: Response; +} + +export type RequestChannelHandler = Channel extends RequestChannel< + infer Request, + infer Response +> ? (req: Request) => Promise | Response : never; export interface RequestChannelListener { + id: string; channel: Channel; handler: RequestChannelHandler; } - -export const requestChannelListenerInjectionToken = getInjectionToken>>( { +export const requestChannelListenerInjectionToken = getInjectionToken< + RequestChannelListener> +>({ id: "request-channel-listener", }); @@ -26,21 +31,26 @@ export interface GetRequestChannelListenerInjectableInfo< Request, Response, > { + id: string; channel: Channel; - handler: (di: DiContainerForInjection) => RequestChannelHandler; + getHandler: (di: DiContainerForInjection) => RequestChannelHandler; } -export function getRequestChannelListenerInjectable< +export const getRequestChannelListenerInjectable = < Channel extends RequestChannel, Request, Response, ->(info: GetRequestChannelListenerInjectableInfo) { - return getInjectable({ - id: `${info.channel.id}-listener`, +>( + info: GetRequestChannelListenerInjectableInfo, +) => + getInjectable({ + id: `${info.channel.id}-request-listener-${info.id}`, + instantiate: (di) => ({ + id: `${info.channel.id}-request-listener-${info.id}`, channel: info.channel, - handler: info.handler(di), + handler: info.getHandler(di), }), + injectionToken: requestChannelListenerInjectionToken, }); -} diff --git a/packages/core/src/common/utils/channel/request-from-channel-injection-token.ts b/packages/technical-features/messaging/agnostic/src/features/actual/request/request-from-channel-injection-token.ts similarity index 53% rename from packages/core/src/common/utils/channel/request-from-channel-injection-token.ts rename to packages/technical-features/messaging/agnostic/src/features/actual/request/request-from-channel-injection-token.ts index 939cc23f9d..194091b588 100644 --- a/packages/core/src/common/utils/channel/request-from-channel-injection-token.ts +++ b/packages/technical-features/messaging/agnostic/src/features/actual/request/request-from-channel-injection-token.ts @@ -1,16 +1,18 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ import { getInjectionToken } from "@ogre-tools/injectable"; import type { RequestChannel } from "./request-channel-listener-injection-token"; export interface RequestFromChannel { - (channel: RequestChannel, request: Request): Promise>; - (channel: RequestChannel): Promise>; + ( + channel: RequestChannel, + request: Request, + ): Promise; + (channel: RequestChannel): Promise; } -export type ChannelRequester = Channel extends RequestChannel +export type ChannelRequester = Channel extends RequestChannel< + infer Request, + infer Response +> ? (req: Request) => Promise> : never; diff --git a/packages/technical-features/messaging/agnostic/src/features/unit-testing/feature.ts b/packages/technical-features/messaging/agnostic/src/features/unit-testing/feature.ts new file mode 100644 index 0000000000..ca6ddd2b2e --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/features/unit-testing/feature.ts @@ -0,0 +1,18 @@ +import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration"; +import { getFeature } from "@k8slens/feature-core"; +import { messagingFeature } from "../actual/feature"; + +export const messagingFeatureForUnitTesting = getFeature({ + id: "messaging-for-unit-testing", + + dependencies: [messagingFeature], + + register: (di) => { + autoRegister({ + di, + targetModule: module, + + getRequireContexts: () => [require.context("./", true, /\.injectable\.(ts|tsx)$/)], + }); + }, +}); diff --git a/packages/technical-features/messaging/agnostic/src/features/unit-testing/index.ts b/packages/technical-features/messaging/agnostic/src/features/unit-testing/index.ts new file mode 100644 index 0000000000..e0ee8eb25b --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/features/unit-testing/index.ts @@ -0,0 +1 @@ +export { messagingFeatureForUnitTesting } from "./feature"; diff --git a/packages/technical-features/messaging/agnostic/src/features/unit-testing/test-doubles.injectable.ts b/packages/technical-features/messaging/agnostic/src/features/unit-testing/test-doubles.injectable.ts new file mode 100644 index 0000000000..621e088a4b --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/features/unit-testing/test-doubles.injectable.ts @@ -0,0 +1,31 @@ +import { sendMessageToChannelInjectionToken } from "../actual/message/message-to-channel-injection-token"; +import { enlistMessageChannelListenerInjectionToken } from "../actual/message/enlist-message-channel-listener-injection-token"; +import { requestFromChannelInjectionToken } from "../actual/request/request-from-channel-injection-token"; +import { enlistRequestChannelListenerInjectionToken } from "../actual/request/enlist-request-channel-listener-injection-token"; +import { getInjectable } from "@ogre-tools/injectable"; + +export const sendMessageToChannelStubInjectable = getInjectable({ + id: "send-message-to-channel-stub", + /* c8 ignore next */ + instantiate: () => () => {}, + injectionToken: sendMessageToChannelInjectionToken, +}); + +export const enlistMessageChannelListenerStubInjectable = getInjectable({ + id: "enlist-message-channel-listener-stub", + instantiate: () => () => () => {}, + injectionToken: enlistMessageChannelListenerInjectionToken, +}); + +export const requestFromChannelStubInjectable = getInjectable({ + id: "request-from-channel-stub", + /* c8 ignore next */ + instantiate: () => () => Promise.resolve(), + injectionToken: requestFromChannelInjectionToken, +}); + +export const enlistRequestChannelListenerStubInjectable = getInjectable({ + id: "enlist-request-channel-listener-stub", + instantiate: () => () => () => {}, + injectionToken: enlistRequestChannelListenerInjectionToken, +}); diff --git a/packages/technical-features/messaging/agnostic/src/listening-of-messages.test.ts b/packages/technical-features/messaging/agnostic/src/listening-of-messages.test.ts new file mode 100644 index 0000000000..8dc25fa41e --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/listening-of-messages.test.ts @@ -0,0 +1,169 @@ +import { createContainer, DiContainer, Injectable } from "@ogre-tools/injectable"; + +import { registerFeature } from "@k8slens/feature-core"; +import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; +import { runInAction } from "mobx"; + +import { + EnlistMessageChannelListener, + enlistMessageChannelListenerInjectionToken, +} from "./features/actual/message/enlist-message-channel-listener-injection-token"; + +import { messagingFeatureForUnitTesting } from "./features/unit-testing"; + +import { + getMessageChannelListenerInjectable, + MessageChannel, + MessageChannelListener, +} from "./features/actual/message/message-channel-listener-injection-token"; + +import { listeningOfChannelsInjectionToken } from "./features/actual/listening-of-channels/listening-of-channels.injectable"; + +import { getMessageChannel } from "./features/actual/message/get-message-channel"; +import { applicationFeature, startApplicationInjectionToken } from "@k8slens/application"; + +describe("listening-of-messages", () => { + let di: DiContainer; + let enlistMessageChannelListenerMock: jest.MockedFunction; + let disposeSomeListenerMock: jest.Mock; + let disposeSomeUnrelatedListenerMock: jest.Mock; + + beforeEach(() => { + di = createContainer("irrelevant"); + + registerMobX(di); + + disposeSomeListenerMock = jest.fn(); + disposeSomeUnrelatedListenerMock = jest.fn(); + + enlistMessageChannelListenerMock = jest.fn((listener) => + listener.id === "some-channel-id-message-listener-some-listener" + ? disposeSomeListenerMock + : disposeSomeUnrelatedListenerMock, + ); + + runInAction(() => { + registerFeature(di, applicationFeature, messagingFeatureForUnitTesting); + }); + + di.override(enlistMessageChannelListenerInjectionToken, () => enlistMessageChannelListenerMock); + }); + + describe("given listening of channels has not started yet", () => { + describe("when a new listener gets registered", () => { + let someChannel: MessageChannel; + let someMessageHandler: () => void; + + let someListenerInjectable: Injectable< + MessageChannelListener>, + MessageChannelListener> + >; + + beforeEach(() => { + someChannel = getMessageChannel("some-channel-id"); + + someMessageHandler = () => {}; + + someListenerInjectable = getMessageChannelListenerInjectable({ + id: "some-listener", + channel: someChannel, + getHandler: () => someMessageHandler, + }); + + runInAction(() => { + di.register(someListenerInjectable); + }); + }); + + describe("when application is started", () => { + beforeEach(async () => { + const startApplication = di.inject(startApplicationInjectionToken); + + await startApplication(); + }); + + it("enlists a listener for the channel", () => { + expect(enlistMessageChannelListenerMock).toHaveBeenCalledWith({ + id: "some-channel-id-message-listener-some-listener", + channel: someChannel, + handler: someMessageHandler, + }); + }); + + describe("when another listener gets registered", () => { + let someOtherListenerInjectable: Injectable< + MessageChannelListener>, + MessageChannelListener>, + void + >; + + beforeEach(() => { + const handler = () => someMessageHandler; + + someOtherListenerInjectable = getMessageChannelListenerInjectable({ + id: "some-other-listener", + channel: someChannel, + getHandler: handler, + }); + + enlistMessageChannelListenerMock.mockClear(); + + runInAction(() => { + di.register(someOtherListenerInjectable); + }); + }); + + it("only enlists it as well", () => { + expect(enlistMessageChannelListenerMock.mock.calls).toEqual([ + [ + { + id: "some-channel-id-message-listener-some-other-listener", + channel: someChannel, + handler: someMessageHandler, + }, + ], + ]); + }); + + describe("when one of the listeners gets deregistered", () => { + beforeEach(() => { + runInAction(() => { + di.deregister(someListenerInjectable); + }); + }); + + it("the listener gets disposed", () => { + expect(disposeSomeListenerMock).toHaveBeenCalled(); + }); + + it("the unrelated listener does not get disposed", () => { + expect(disposeSomeUnrelatedListenerMock).not.toHaveBeenCalled(); + }); + + describe("when listening of channels stops", () => { + beforeEach(() => { + const listening = di.inject(listeningOfChannelsInjectionToken); + + listening.stop(); + }); + + it("remaining listeners get disposed", () => { + expect(disposeSomeUnrelatedListenerMock).toHaveBeenCalled(); + }); + + it("when yet another listener gets registered, does not enlist it", () => { + enlistMessageChannelListenerMock.mockClear(); + + runInAction(() => { + di.register(someListenerInjectable); + }); + + expect(enlistMessageChannelListenerMock).not.toHaveBeenCalled(); + }); + }); + }); + }); + }); + }); + }); +}); diff --git a/packages/technical-features/messaging/agnostic/src/listening-of-requests.test.ts b/packages/technical-features/messaging/agnostic/src/listening-of-requests.test.ts new file mode 100644 index 0000000000..e708c983c8 --- /dev/null +++ b/packages/technical-features/messaging/agnostic/src/listening-of-requests.test.ts @@ -0,0 +1,204 @@ +import { createContainer, DiContainer, Injectable } from "@ogre-tools/injectable"; + +import { registerFeature } from "@k8slens/feature-core"; +import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; +import { _resetGlobalState, configure, runInAction } from "mobx"; + +import { + EnlistRequestChannelListener, + enlistRequestChannelListenerInjectionToken, +} from "./features/actual/request/enlist-request-channel-listener-injection-token"; + +import { messagingFeatureForUnitTesting } from "./features/unit-testing"; + +import { + getRequestChannelListenerInjectable, + RequestChannel, + RequestChannelListener, +} from "./features/actual/request/request-channel-listener-injection-token"; + +import { listeningOfChannelsInjectionToken } from "./features/actual/listening-of-channels/listening-of-channels.injectable"; +import { noop } from "lodash/fp"; +import { getRequestChannel } from "./features/actual/request/get-request-channel"; +import { applicationFeature, startApplicationInjectionToken } from "@k8slens/application"; + +describe("listening-of-requests", () => { + let di: DiContainer; + let enlistRequestChannelListenerMock: jest.MockedFunction; + let disposeSomeListenerMock: jest.Mock; + let disposeSomeUnrelatedListenerMock: jest.Mock; + + beforeEach(() => { + configure({ + disableErrorBoundaries: false, + }); + + _resetGlobalState(); + + di = createContainer("irrelevant"); + + registerMobX(di); + + disposeSomeListenerMock = jest.fn(); + disposeSomeUnrelatedListenerMock = jest.fn(); + + enlistRequestChannelListenerMock = jest.fn((listener) => + listener.id === "some-channel-id-request-listener-some-listener" + ? disposeSomeListenerMock + : disposeSomeUnrelatedListenerMock, + ); + + runInAction(() => { + registerFeature(di, applicationFeature, messagingFeatureForUnitTesting); + }); + + di.override(enlistRequestChannelListenerInjectionToken, () => enlistRequestChannelListenerMock); + }); + + describe("given listening of channels has not started yet", () => { + describe("when a new listener gets registered", () => { + let someChannel: RequestChannel; + let someOtherChannel: RequestChannel; + let someRequestHandler: () => string; + + let someListenerInjectable: Injectable< + RequestChannelListener>, + RequestChannelListener> + >; + + beforeEach(() => { + someChannel = getRequestChannel("some-channel-id"); + someOtherChannel = getRequestChannel("some-other-channel-id"); + + someRequestHandler = () => "some-response"; + + someListenerInjectable = getRequestChannelListenerInjectable({ + id: "some-listener", + channel: someChannel, + getHandler: () => someRequestHandler, + }); + + runInAction(() => { + di.register(someListenerInjectable); + }); + }); + + describe("when application is started", () => { + beforeEach(async () => { + const startApplication = di.inject(startApplicationInjectionToken); + + await startApplication(); + }); + + it("enlists a listener for the channel", () => { + expect(enlistRequestChannelListenerMock).toHaveBeenCalledWith({ + id: "some-channel-id-request-listener-some-listener", + channel: someChannel, + handler: someRequestHandler, + }); + }); + + it("when another listener for same channel gets registered, throws", () => { + const originalConsoleWarn = console.warn; + + console.warn = noop; + + configure({ + disableErrorBoundaries: true, + }); + + console.warn = originalConsoleWarn; + + const handler = () => someRequestHandler; + + const someConflictingListenerInjectable = getRequestChannelListenerInjectable({ + id: "some-other-listener", + channel: someChannel, + getHandler: handler, + }); + + expect(() => { + runInAction(() => { + di.register(someConflictingListenerInjectable); + }); + }).toThrow( + 'Tried to add listener for channel "some-channel-id" but listener already exists.', + ); + }); + + describe("when another listener gets registered", () => { + let someOtherListenerInjectable: Injectable< + RequestChannelListener>, + RequestChannelListener> + >; + + beforeEach(() => { + const handler = () => someRequestHandler; + + someOtherListenerInjectable = getRequestChannelListenerInjectable({ + id: "some-other-listener", + channel: someOtherChannel, + getHandler: handler, + }); + + enlistRequestChannelListenerMock.mockClear(); + + runInAction(() => { + di.register(someOtherListenerInjectable); + }); + }); + + it("only enlists it as well", () => { + expect(enlistRequestChannelListenerMock.mock.calls).toEqual([ + [ + { + id: "some-other-channel-id-request-listener-some-other-listener", + channel: someOtherChannel, + handler: someRequestHandler, + }, + ], + ]); + }); + + describe("when one of the listeners gets deregistered", () => { + beforeEach(() => { + runInAction(() => { + di.deregister(someListenerInjectable); + }); + }); + + it("the listener gets disposed", () => { + expect(disposeSomeListenerMock).toHaveBeenCalled(); + }); + + it("the unrelated listener does not get disposed", () => { + expect(disposeSomeUnrelatedListenerMock).not.toHaveBeenCalled(); + }); + + describe("when listening of channels stops", () => { + beforeEach(() => { + const listening = di.inject(listeningOfChannelsInjectionToken); + + listening.stop(); + }); + + it("remaining listeners get disposed", () => { + expect(disposeSomeUnrelatedListenerMock).toHaveBeenCalled(); + }); + + it("when yet another listener gets registered, does not enlist it", () => { + enlistRequestChannelListenerMock.mockClear(); + + runInAction(() => { + di.register(someListenerInjectable); + }); + + expect(enlistRequestChannelListenerMock).not.toHaveBeenCalled(); + }); + }); + }); + }); + }); + }); + }); +}); diff --git a/packages/technical-features/messaging/agnostic/tsconfig.json b/packages/technical-features/messaging/agnostic/tsconfig.json new file mode 100644 index 0000000000..ec29a8f75f --- /dev/null +++ b/packages/technical-features/messaging/agnostic/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@k8slens/typescript/config/base.json", + "include": ["**/*.ts", "**/*.tsx"] +} diff --git a/packages/technical-features/messaging/agnostic/webpack.config.js b/packages/technical-features/messaging/agnostic/webpack.config.js new file mode 100644 index 0000000000..3183f30179 --- /dev/null +++ b/packages/technical-features/messaging/agnostic/webpack.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/webpack").configForNode; diff --git a/packages/technical-features/messaging/computed-channel/.eslintrc.json b/packages/technical-features/messaging/computed-channel/.eslintrc.json new file mode 100644 index 0000000000..b15115cb69 --- /dev/null +++ b/packages/technical-features/messaging/computed-channel/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "@k8slens/eslint-config/eslint", + "parserOptions": { + "project": "./tsconfig.json" + } +} diff --git a/packages/technical-features/messaging/computed-channel/.prettierrc b/packages/technical-features/messaging/computed-channel/.prettierrc new file mode 100644 index 0000000000..edd47b479e --- /dev/null +++ b/packages/technical-features/messaging/computed-channel/.prettierrc @@ -0,0 +1 @@ +"@k8slens/eslint-config/prettier" diff --git a/packages/technical-features/messaging/computed-channel/index.ts b/packages/technical-features/messaging/computed-channel/index.ts new file mode 100644 index 0000000000..4516e0b9a6 --- /dev/null +++ b/packages/technical-features/messaging/computed-channel/index.ts @@ -0,0 +1,12 @@ +export { + computedChannelInjectionToken, + computedChannelObserverInjectionToken, +} from "./src/computed-channel/computed-channel.injectable"; + +export type { + ChannelObserver, + ComputedChannelFactory, + JsonifiableObject, + JsonifiableArray, + Jsonifiable, +} from "./src/computed-channel/computed-channel.injectable"; diff --git a/packages/technical-features/messaging/computed-channel/jest.config.js b/packages/technical-features/messaging/computed-channel/jest.config.js new file mode 100644 index 0000000000..38d54ab7b6 --- /dev/null +++ b/packages/technical-features/messaging/computed-channel/jest.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact; diff --git a/packages/technical-features/messaging/computed-channel/package.json b/packages/technical-features/messaging/computed-channel/package.json new file mode 100644 index 0000000000..81c4f80013 --- /dev/null +++ b/packages/technical-features/messaging/computed-channel/package.json @@ -0,0 +1,51 @@ +{ + "name": "@k8slens/computed-channel", + "private": false, + "version": "1.0.0-alpha.1", + "description": "MobX-like computed between channels", + "type": "commonjs", + + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/lensapp/lens.git" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "author": { + "name": "OpenLens Authors", + "email": "info@k8slens.dev" + }, + "license": "MIT", + "homepage": "https://github.com/lensapp/lens", + "scripts": { + "build": "webpack", + "dev": "webpack --mode=development --watch", + "test:unit": "jest --coverage --runInBand", + "lint": "lens-lint", + "lint:fix": "lens-lint --fix" + }, + "peerDependencies": { + "@k8slens/application": "^6.5.0-alpha.0", + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@k8slens/messaging": "^1.0.0-alpha.1", + "@ogre-tools/fp": "^15.1.2", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", + "@ogre-tools/injectable-extension-for-mobx": "^15.1.2", + "lodash": "^4.17.21", + "mobx": "^6.8.0" + }, + "devDependencies": { + "@k8slens/eslint-config": "^6.5.0-alpha.1", + "@k8slens/messaging-fake-bridge": "^1.0.0-alpha.1", + "type-fest": "^2.14.0" + } +} diff --git a/packages/technical-features/messaging/computed-channel/src/computed-channel/computed-channel-administration-channel.injectable.ts b/packages/technical-features/messaging/computed-channel/src/computed-channel/computed-channel-administration-channel.injectable.ts new file mode 100644 index 0000000000..b0fdb3c59f --- /dev/null +++ b/packages/technical-features/messaging/computed-channel/src/computed-channel/computed-channel-administration-channel.injectable.ts @@ -0,0 +1,64 @@ +import { reaction } from "mobx"; + +import { getMessageChannelListenerInjectable } from "@k8slens/messaging"; +import { sendMessageToChannelInjectionToken } from "@k8slens/messaging"; +import type { JsonPrimitive } from "type-fest"; +import { computedChannelObserverInjectionToken } from "./computed-channel.injectable"; +import { getMessageChannel } from "@k8slens/messaging"; + +export type JsonifiableObject = { [Key in string]?: Jsonifiable } | { toJSON: () => Jsonifiable }; +export type JsonifiableArray = readonly Jsonifiable[]; +export type Jsonifiable = JsonPrimitive | JsonifiableObject | JsonifiableArray; + +export type ComputedChannelAdminMessage = { + channelId: string; + status: "became-observed" | "became-unobserved"; +}; + +export const computedChannelAdministrationChannel = getMessageChannel( + "computed-channel-administration-channel", +); + +export const computedChannelAdministrationListenerInjectable = getMessageChannelListenerInjectable({ + id: "computed-channel-administration", + channel: computedChannelAdministrationChannel, + + getHandler: (di) => { + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + + const disposersByChannelId = new Map void>(); + + return (message) => { + if (message.status === "became-observed") { + const result = di + .injectMany(computedChannelObserverInjectionToken) + .find((channelObserver) => channelObserver.channel.id === message.channelId); + + if (result === undefined) { + return; + } + + const disposer = reaction( + () => result.observer.get(), + (observed) => + sendMessageToChannel( + { + id: message.channelId, + }, + + observed, + ), + { + fireImmediately: true, + }, + ); + + disposersByChannelId.set(message.channelId, disposer); + } else { + const disposer = disposersByChannelId.get(message.channelId); + + disposer?.(); + } + }; + }, +}); diff --git a/packages/technical-features/messaging/computed-channel/src/computed-channel/computed-channel.injectable.ts b/packages/technical-features/messaging/computed-channel/src/computed-channel/computed-channel.injectable.ts new file mode 100644 index 0000000000..03f5a16aab --- /dev/null +++ b/packages/technical-features/messaging/computed-channel/src/computed-channel/computed-channel.injectable.ts @@ -0,0 +1,110 @@ +import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; + +import { + _getGlobalState, + computed, + IComputedValue, + observable, + onBecomeObserved, + onBecomeUnobserved, + runInAction, +} from "mobx"; + +import type { MessageChannel } from "@k8slens/messaging"; +import { getMessageChannelListenerInjectable } from "@k8slens/messaging"; +import { sendMessageToChannelInjectionToken } from "@k8slens/messaging"; +import type { JsonPrimitive } from "type-fest"; +import { computedChannelAdministrationChannel } from "./computed-channel-administration-channel.injectable"; + +export type JsonifiableObject = { [Key in string]?: Jsonifiable } | { toJSON: () => Jsonifiable }; +export type JsonifiableArray = readonly Jsonifiable[]; +export type Jsonifiable = JsonPrimitive | JsonifiableObject | JsonifiableArray; + +export type ComputedChannelFactory = ( + channel: MessageChannel, + pendingValue: T, +) => IComputedValue; + +export const computedChannelInjectionToken = getInjectionToken({ + id: "computed-channel-injection-token", +}); + +export type ChannelObserver = { + channel: MessageChannel; + observer: IComputedValue; +}; + +export const computedChannelObserverInjectionToken = getInjectionToken< + ChannelObserver +>({ + id: "computed-channel-observer", +}); + +const computedChannelInjectable = getInjectable({ + id: "computed-channel", + + instantiate: (di) => { + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + + return ((channel, pendingValue) => { + const observableValue = observable.box(pendingValue); + + const computedValue = computed(() => { + const { trackingDerivation } = _getGlobalState(); + + const contextIsReactive = !!trackingDerivation; + + if (!contextIsReactive) { + throw new Error( + `Tried to access value of computed channel "${channel.id}" outside of reactive context. This is not possible, as the value is acquired asynchronously sometime *after* being observed. Not respecting that, the value could be stale.`, + ); + } + + return observableValue.get(); + }); + + const valueReceiverInjectable = getMessageChannelListenerInjectable({ + id: `computed-channel-value-receiver-for-${channel.id}`, + channel, + + getHandler: () => (message) => { + runInAction(() => { + observableValue.set(message); + }); + }, + }); + + runInAction(() => { + di.register(valueReceiverInjectable); + }); + + onBecomeObserved(computedValue, () => { + runInAction(() => { + observableValue.set(pendingValue); + }); + + sendMessageToChannel(computedChannelAdministrationChannel, { + channelId: channel.id, + status: "became-observed", + }); + }); + + onBecomeUnobserved(computedValue, () => { + runInAction(() => { + observableValue.set(pendingValue); + }); + + sendMessageToChannel(computedChannelAdministrationChannel, { + channelId: channel.id, + status: "became-unobserved", + }); + }); + + return computedValue; + }) as ComputedChannelFactory; + }, + + injectionToken: computedChannelInjectionToken, +}); + +export default computedChannelInjectable; diff --git a/packages/technical-features/messaging/computed-channel/src/computed-channel/computed-channel.test.tsx b/packages/technical-features/messaging/computed-channel/src/computed-channel/computed-channel.test.tsx new file mode 100644 index 0000000000..4736fe22d4 --- /dev/null +++ b/packages/technical-features/messaging/computed-channel/src/computed-channel/computed-channel.test.tsx @@ -0,0 +1,571 @@ +import React from "react"; +import { act } from "@testing-library/react"; +import { createContainer, DiContainer, getInjectable } from "@ogre-tools/injectable"; +import { getMessageBridgeFake, MessageBridgeFake } from "@k8slens/messaging-fake-bridge"; +import { startApplicationInjectionToken } from "@k8slens/application"; +import { + computed, + IComputedValue, + IObservableValue, + observable, + reaction, + runInAction, +} from "mobx"; +import type { MessageChannel } from "@k8slens/messaging"; +import { getMessageChannelListenerInjectable } from "@k8slens/messaging"; +import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; +import { registerFeature } from "@k8slens/feature-core"; +import { testUtils } from "@k8slens/messaging"; +import { + computedChannelInjectionToken, + computedChannelObserverInjectionToken, +} from "./computed-channel.injectable"; +import { runWithThrownMobxReactions, renderFor } from "@k8slens/test-utils"; +import { observer } from "mobx-react"; +import { + computedChannelAdministrationChannel, + ComputedChannelAdminMessage, +} from "./computed-channel-administration-channel.injectable"; +import { computedChannelFeature } from "../feature"; + +const testChannel: MessageChannel = { id: "some-channel-id" }; +const testChannel2: MessageChannel = { id: "some-other-channel-id" }; + +const TestComponent = observer(({ someComputed }: { someComputed: IComputedValue }) => ( +
{someComputed.get()}
+)); + +[{ scenarioIsAsync: true }, { scenarioIsAsync: false }].forEach(({ scenarioIsAsync }) => + describe(`computed-channel, given running message bridge fake as ${ + scenarioIsAsync ? "async" : "sync" + }`, () => { + describe("given multiple dis and a message channel and a channel observer and application has started", () => { + let di1: DiContainer; + let di2: DiContainer; + let latestAdminMessage: ComputedChannelAdminMessage | undefined; + let latestValueMessage: string | undefined; + let messageBridgeFake: MessageBridgeFake; + + beforeEach(async () => { + latestAdminMessage = undefined; + latestValueMessage = undefined; + + di1 = createContainer("some-container-1"); + di2 = createContainer("some-container-2"); + registerMobX(di1); + registerMobX(di2); + + const administrationChannelTestListenerInjectable = getMessageChannelListenerInjectable({ + id: "administration-channel-test-listener", + channel: computedChannelAdministrationChannel, + + getHandler: () => (adminMessage) => { + latestAdminMessage = adminMessage; + }, + }); + + const channelValueTestListenerInjectable = getMessageChannelListenerInjectable({ + id: "test-channel-value-listener", + channel: testChannel, + + getHandler: () => (message) => { + latestValueMessage = message; + }, + }); + + runInAction(() => { + const messagingFeature = testUtils.messagingFeatureForUnitTesting; + + registerFeature(di1, messagingFeature, computedChannelFeature); + registerFeature(di2, messagingFeature, computedChannelFeature); + + di1.register(channelValueTestListenerInjectable); + di2.register(administrationChannelTestListenerInjectable); + }); + + messageBridgeFake = getMessageBridgeFake(); + messageBridgeFake.setAsync(scenarioIsAsync); + messageBridgeFake.involve(di1, di2); + + await Promise.all([ + di1.inject(startApplicationInjectionToken)(), + di2.inject(startApplicationInjectionToken)(), + ]); + }); + + describe("given a channel observer and matching computed channel for the channel in di-2", () => { + let someObservable: IObservableValue; + let computedTestChannel: IComputedValue; + + beforeEach(() => { + someObservable = observable.box("some-initial-value"); + + const channelObserverInjectable = getInjectable({ + id: "some-channel-observer", + + instantiate: () => ({ + channel: testChannel, + observer: computed(() => someObservable.get()), + }), + + injectionToken: computedChannelObserverInjectionToken, + }); + + runInAction(() => { + di2.register(channelObserverInjectable); + }); + + const computedChannel = di1.inject(computedChannelInjectionToken); + + computedTestChannel = computedChannel(testChannel, "some-pending-value"); + }); + + it("there is no admin message yet", () => { + expect(latestAdminMessage).toBeUndefined(); + }); + + describe("when observing the computed value in a component in di-1", () => { + let rendered: any; + + beforeEach(() => { + const render = renderFor(di2); + + rendered = render(); + }); + + const scenarioName = scenarioIsAsync ? "when all messages are propagated" : "immediately"; + + // eslint-disable-next-line jest/valid-title + describe(scenarioName, () => { + beforeEach((done) => { + if (scenarioIsAsync) { + messageBridgeFake.messagePropagationRecursive(act).then(done); + } else { + done(); + } + }); + + it("renders", () => { + expect(rendered.container).toHaveTextContent("some-initial-value"); + }); + }); + }); + + describe("when observing the computed channel in di-1", () => { + let observedValue: string | undefined; + let stopObserving: () => void; + + beforeEach(() => { + observedValue = undefined; + + stopObserving = reaction( + () => computedTestChannel.get(), + (value) => { + observedValue = value; + }, + + { + fireImmediately: true, + }, + ); + }); + + scenarioIsAsync && + it("computed test channel value is observed as the pending value", () => { + expect(observedValue).toBe("some-pending-value"); + }); + + const scenarioName = scenarioIsAsync + ? "when admin messages are propagated" + : "immediately"; + + // eslint-disable-next-line jest/valid-title + describe(scenarioName, () => { + beforeEach((done) => { + if (scenarioIsAsync) { + void messageBridgeFake.messagePropagation().then(done); + } else { + done(); + } + }); + + it("administration-message to start observing gets listened", () => { + expect(latestAdminMessage).toEqual({ + channelId: "some-channel-id", + status: "became-observed", + }); + }); + + const scenarioName = scenarioIsAsync + ? "when returning value-messages propagate" + : "immediately"; + + // eslint-disable-next-line jest/valid-title + describe(scenarioName, () => { + beforeEach((done) => { + if (scenarioIsAsync) { + void messageBridgeFake.messagePropagation().then(done); + } else { + done(); + } + }); + + it("the computed channel value in di-1 matches the value in di-2", () => { + expect(observedValue).toBe("some-initial-value"); + }); + + it("the value gets listened in di-1", () => { + expect(latestValueMessage).toBe("some-initial-value"); + }); + + describe("when the observed value changes", () => { + beforeEach(async () => { + latestValueMessage = undefined; + + runInAction(() => { + someObservable.set("some-new-value"); + }); + }); + + const scenarioName = scenarioIsAsync + ? "when value-messages propagate" + : "immediately"; + + // eslint-disable-next-line jest/valid-title + describe(scenarioName, () => { + beforeEach((done) => { + if (scenarioIsAsync) { + void messageBridgeFake.messagePropagation().then(done); + } else { + done(); + } + }); + + it("the computed channel value in di-1 changes", () => { + expect(observedValue).toBe("some-new-value"); + }); + + it("the new value gets listened in di-1", () => { + expect(latestValueMessage).toBe("some-new-value"); + }); + }); + }); + + describe("when stopping observation for the channel in di-1", () => { + beforeEach(async () => { + latestValueMessage = undefined; + + stopObserving(); + }); + + const scenarioName = scenarioIsAsync + ? "when admin-messages propagate" + : "immediately"; + + // eslint-disable-next-line jest/valid-title + describe(scenarioName, () => { + beforeEach((done) => { + if (scenarioIsAsync) { + void messageBridgeFake.messagePropagation().then(done); + } else { + done(); + } + }); + + it("messages administration channel to stop observing", () => { + expect(latestAdminMessage).toEqual({ + channelId: "some-channel-id", + status: "became-unobserved", + }); + }); + + it("no value gets listened in di-1 anymore", () => { + expect(latestValueMessage).toBeUndefined(); + }); + + describe("when the observed value changes", () => { + beforeEach(async () => { + latestValueMessage = undefined; + + runInAction(() => { + someObservable.set("some-new-value-2"); + }); + }); + + it("when accessing the computed value outside of reactive context, throws", () => { + expect(() => { + computedTestChannel.get(); + }).toThrow( + 'Tried to access value of computed channel "some-channel-id" outside of reactive context. This is not possible, as the value is acquired asynchronously sometime *after* being observed. Not respecting that, the value could be stale.', + ); + }); + + it("no value gets listened in di-1 anymore", () => { + expect(latestValueMessage).toBeUndefined(); + }); + + describe("when observing the computed channel again", () => { + beforeEach(() => { + observedValue = undefined; + + reaction( + () => computedTestChannel.get(), + (value) => { + observedValue = value; + }, + + { + fireImmediately: true, + }, + ); + }); + + scenarioIsAsync && + it("computed test channel value is observed as the pending value again", () => { + expect(observedValue).toBe("some-pending-value"); + }); + + const scenarioName = scenarioIsAsync + ? "when admin messages propagate" + : "immediately"; + + // eslint-disable-next-line jest/valid-title + describe(scenarioName, () => { + beforeEach((done) => { + if (scenarioIsAsync) { + latestAdminMessage = undefined; + + void messageBridgeFake.messagePropagation().then(done); + } else { + done(); + } + }); + + it("administration-message to start observing gets listened again", () => { + expect(latestAdminMessage).toEqual({ + channelId: "some-channel-id", + status: "became-observed", + }); + }); + + scenarioIsAsync && + it("computed test channel value is still observed as the pending value", () => { + expect(observedValue).toBe("some-pending-value"); + }); + + const scenarioTitle = scenarioIsAsync + ? "when value-messages propagate back" + : "immediately"; + + // eslint-disable-next-line jest/valid-title + describe(scenarioTitle, () => { + beforeEach((done) => { + if (scenarioIsAsync) { + latestValueMessage = undefined; + + void messageBridgeFake.messagePropagation().then(done); + } else { + done(); + } + }); + + it("the computed channel value changes", () => { + expect(observedValue).toBe("some-new-value-2"); + }); + + it("the current value gets listened", () => { + expect(latestValueMessage).toBe("some-new-value-2"); + }); + }); + }); + }); + }); + }); + + it("when accessing the computed value outside of reactive context, throws", () => { + expect(() => { + computedTestChannel.get(); + }).toThrow( + 'Tried to access value of computed channel "some-channel-id" outside of reactive context. This is not possible, as the value is acquired asynchronously sometime *after* being observed. Not respecting that, the value could be stale.', + ); + }); + }); + + it("given observation of unrelated computed channel is stopped, observation of other computed channel still works", async () => { + const someOtherObservable = observable.box(""); + + const channelObserver2Injectable = getInjectable({ + id: "some-channel-observer-2", + + instantiate: () => ({ + channel: testChannel2, + observer: computed(() => someOtherObservable.get()), + }), + + injectionToken: computedChannelObserverInjectionToken, + }); + + runInAction(() => { + di2.register(channelObserver2Injectable); + }); + + const computedChannel = di1.inject(computedChannelInjectionToken); + + computedTestChannel = computedChannel(testChannel2, "some-pending-value"); + + reaction( + () => computedTestChannel.get(), + (value) => { + observedValue = value; + }, + + { + fireImmediately: true, + }, + ); + + scenarioIsAsync && (await messageBridgeFake.messagePropagation()); + + stopObserving(); + + scenarioIsAsync && (await messageBridgeFake.messagePropagation()); + + runInAction(() => { + someOtherObservable.set("some-value"); + }); + + scenarioIsAsync && (await messageBridgeFake.messagePropagation()); + + expect(observedValue).toBe("some-value"); + }); + + describe("when observing the computed channel again", () => { + beforeEach(() => { + latestAdminMessage = undefined; + + reaction( + () => computedTestChannel.get(), + (value) => { + observedValue = value; + }, + + { + fireImmediately: true, + }, + ); + }); + + it("doesn't send second administration message", () => { + expect(latestAdminMessage).toBeUndefined(); + }); + + it("when one of the observations stops, doesn't send administration message to stop observing", async () => { + latestAdminMessage = undefined; + + stopObserving(); + + expect(latestAdminMessage).toBeUndefined(); + }); + }); + + describe("when accessing the computed value outside of reactive context", () => { + let nonReactiveValue: string; + + beforeEach(() => { + latestValueMessage = undefined; + latestAdminMessage = undefined; + + nonReactiveValue = computedTestChannel.get(); + }); + + it("the non reactive value is what ever happens to be the current value from di-2", () => { + expect(nonReactiveValue).toBe("some-initial-value"); + }); + + const scenarioName = scenarioIsAsync + ? "when messages would be propagated" + : "immediately"; + + // eslint-disable-next-line jest/valid-title + describe(scenarioName, () => { + beforeEach((done) => { + if (scenarioIsAsync) { + void messageBridgeFake.messagePropagation().then(done); + } else { + done(); + } + }); + + it("does not send new value message", () => { + expect(latestValueMessage).toBeUndefined(); + }); + + it("does not send new admin message", () => { + expect(latestAdminMessage).toBeUndefined(); + }); + }); + }); + }); + }); + }); + + it("when accessing the computed value outside of reactive context, throws", () => { + expect(() => { + computedTestChannel.get(); + }).toThrow( + 'Tried to access value of computed channel "some-channel-id" outside of reactive context. This is not possible, as the value is acquired asynchronously sometime *after* being observed. Not respecting that, the value could be stale.', + ); + }); + + it("given duplicate channel observer for the channel is registered, when the computed channel is observer, throws", () => { + const duplicateChannelObserverInjectable = getInjectable({ + id: "some-duplicate-channel-observer", + + instantiate: () => ({ + channel: testChannel, + observer: computed(() => "irrelevant"), + }), + + injectionToken: computedChannelObserverInjectionToken, + }); + + expect(() => { + runWithThrownMobxReactions(() => { + runInAction(() => { + di2.register(duplicateChannelObserverInjectable); + }); + }); + }).toThrow('Tried to register duplicate channel observer for channels "some-channel-id"'); + }); + }); + + describe("given no channel observer but still a computed channel", () => { + let computedTestChannel: IComputedValue; + + beforeEach(() => { + const computedChannel = di1.inject(computedChannelInjectionToken); + + computedTestChannel = computedChannel(testChannel, "some-pending-value"); + }); + + it("when the computed channel is observed, observes as undefined", () => { + let observedValue = "some-value-to-never-be-seen-in-unit-test"; + + reaction( + () => computedTestChannel.get(), + + (value) => { + observedValue = value; + }, + + { + fireImmediately: true, + }, + ); + + expect(observedValue).toBe("some-pending-value"); + }); + }); + }); + }), +); diff --git a/packages/technical-features/messaging/computed-channel/src/computed-channel/duplicate-channel-observer-guard.injectable.ts b/packages/technical-features/messaging/computed-channel/src/computed-channel/duplicate-channel-observer-guard.injectable.ts new file mode 100644 index 0000000000..42f313ec36 --- /dev/null +++ b/packages/technical-features/messaging/computed-channel/src/computed-channel/duplicate-channel-observer-guard.injectable.ts @@ -0,0 +1,42 @@ +import { onLoadOfApplicationInjectionToken } from "@k8slens/application"; +import { pipeline } from "@ogre-tools/fp"; +import { getInjectable } from "@ogre-tools/injectable"; +import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; +import { filter, groupBy, nth, map, toPairs } from "lodash/fp"; +import { reaction } from "mobx"; +import { computedChannelObserverInjectionToken } from "./computed-channel.injectable"; + +export const duplicateChannelObserverGuardInjectable = getInjectable({ + id: "duplicate-channel-observer-guard", + + instantiate: (di) => { + const computedInjectMany = di.inject(computedInjectManyInjectable); + + return { + run: () => { + reaction( + () => computedInjectMany(computedChannelObserverInjectionToken).get(), + (observers) => { + const duplicateObserverChannelIds = pipeline( + observers, + groupBy((observer) => observer.channel.id), + toPairs, + filter(([, channelObservers]) => channelObservers.length > 1), + map(nth(0)), + ); + + if (duplicateObserverChannelIds.length) { + throw new Error( + `Tried to register duplicate channel observer for channels "${duplicateObserverChannelIds.join( + '", "', + )}"`, + ); + } + }, + ); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); diff --git a/packages/technical-features/messaging/computed-channel/src/feature.ts b/packages/technical-features/messaging/computed-channel/src/feature.ts new file mode 100644 index 0000000000..179700edcc --- /dev/null +++ b/packages/technical-features/messaging/computed-channel/src/feature.ts @@ -0,0 +1,18 @@ +import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration"; +import { getFeature } from "@k8slens/feature-core"; +import { messagingFeature } from "@k8slens/messaging"; + +export const computedChannelFeature = getFeature({ + id: "computed-channel", + + dependencies: [messagingFeature], + + register: (di) => { + autoRegister({ + di, + targetModule: module, + + getRequireContexts: () => [require.context("./", true, /\.injectable\.(ts|tsx)$/)], + }); + }, +}); diff --git a/packages/technical-features/messaging/computed-channel/tsconfig.json b/packages/technical-features/messaging/computed-channel/tsconfig.json new file mode 100644 index 0000000000..ec29a8f75f --- /dev/null +++ b/packages/technical-features/messaging/computed-channel/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@k8slens/typescript/config/base.json", + "include": ["**/*.ts", "**/*.tsx"] +} diff --git a/packages/technical-features/messaging/computed-channel/webpack.config.js b/packages/technical-features/messaging/computed-channel/webpack.config.js new file mode 100644 index 0000000000..3183f30179 --- /dev/null +++ b/packages/technical-features/messaging/computed-channel/webpack.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/webpack").configForNode; diff --git a/packages/technical-features/messaging/electron/main/.eslintrc.json b/packages/technical-features/messaging/electron/main/.eslintrc.json new file mode 100644 index 0000000000..b15115cb69 --- /dev/null +++ b/packages/technical-features/messaging/electron/main/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "@k8slens/eslint-config/eslint", + "parserOptions": { + "project": "./tsconfig.json" + } +} diff --git a/packages/technical-features/messaging/electron/main/.prettierrc b/packages/technical-features/messaging/electron/main/.prettierrc new file mode 100644 index 0000000000..edd47b479e --- /dev/null +++ b/packages/technical-features/messaging/electron/main/.prettierrc @@ -0,0 +1 @@ +"@k8slens/eslint-config/prettier" diff --git a/packages/technical-features/messaging/electron/main/index.ts b/packages/technical-features/messaging/electron/main/index.ts new file mode 100644 index 0000000000..1ff9afff0d --- /dev/null +++ b/packages/technical-features/messaging/electron/main/index.ts @@ -0,0 +1 @@ +export { messagingFeatureForMain } from "./src/feature"; diff --git a/packages/technical-features/messaging/electron/main/jest.config.js b/packages/technical-features/messaging/electron/main/jest.config.js new file mode 100644 index 0000000000..c6074967eb --- /dev/null +++ b/packages/technical-features/messaging/electron/main/jest.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode; diff --git a/packages/technical-features/messaging/electron/main/package.json b/packages/technical-features/messaging/electron/main/package.json new file mode 100644 index 0000000000..ff1a76a7d3 --- /dev/null +++ b/packages/technical-features/messaging/electron/main/package.json @@ -0,0 +1,47 @@ +{ + "name": "@k8slens/messaging-for-main", + "private": false, + "version": "1.0.0-alpha.1", + "description": "Implementations for 'messaging' in Electron main", + "type": "commonjs", + + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/lensapp/lens.git" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "author": { + "name": "OpenLens Authors", + "email": "info@k8slens.dev" + }, + "license": "MIT", + "homepage": "https://github.com/lensapp/lens", + "scripts": { + "build": "webpack", + "dev": "webpack --mode=development --watch", + "test:unit": "jest --coverage --runInBand", + "lint": "lens-lint", + "lint:fix": "lens-lint --fix" + }, + "peerDependencies": { + "@k8slens/application": "^6.5.0-alpha.0", + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@k8slens/messaging": "^1.0.0-alpha.1", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", + "electron": "^22.3.3", + "lodash": "^4.17.21" + }, + "devDependencies": { + "@k8slens/eslint-config": "^6.5.0-alpha.1" + } +} diff --git a/packages/technical-features/messaging/electron/main/src/channel-listeners/enlist-message-channel-listener.injectable.ts b/packages/technical-features/messaging/electron/main/src/channel-listeners/enlist-message-channel-listener.injectable.ts new file mode 100644 index 0000000000..5a33ed39d9 --- /dev/null +++ b/packages/technical-features/messaging/electron/main/src/channel-listeners/enlist-message-channel-listener.injectable.ts @@ -0,0 +1,28 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import type { IpcMainEvent } from "electron"; +import ipcMainInjectable from "../ipc-main/ipc-main.injectable"; +import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; + +const enlistMessageChannelListenerInjectable = getInjectable({ + id: "enlist-message-channel-listener", + + instantiate: (di) => { + const ipcMain = di.inject(ipcMainInjectable); + + return ({ channel, handler }) => { + const nativeOnCallback = (nativeEvent: IpcMainEvent, message: unknown) => { + handler(message, { frameId: nativeEvent.frameId, processId: nativeEvent.processId }); + }; + + ipcMain.on(channel.id, nativeOnCallback); + + return () => { + ipcMain.off(channel.id, nativeOnCallback); + }; + }; + }, + + injectionToken: enlistMessageChannelListenerInjectionToken, +}); + +export default enlistMessageChannelListenerInjectable; diff --git a/packages/core/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts b/packages/technical-features/messaging/electron/main/src/channel-listeners/enlist-message-channel-listener.test.ts similarity index 59% rename from packages/core/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts rename to packages/technical-features/messaging/electron/main/src/channel-listeners/enlist-message-channel-listener.test.ts index 2bb5f7b10f..c4385246cd 100644 --- a/packages/core/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts +++ b/packages/technical-features/messaging/electron/main/src/channel-listeners/enlist-message-channel-listener.test.ts @@ -1,12 +1,12 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import ipcMainInjectable from "../ipc-main/ipc-main.injectable"; -import type { EnlistMessageChannelListener } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token"; -import { enlistMessageChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token"; import type { IpcMain, IpcMainEvent } from "electron"; +import { + EnlistMessageChannelListener, + enlistMessageChannelListenerInjectionToken, +} from "@k8slens/messaging"; +import { createContainer } from "@ogre-tools/injectable"; +import { registerFeature } from "@k8slens/feature-core"; +import { messagingFeatureForMain } from "../feature"; describe("enlist message channel listener in main", () => { let enlistMessageChannelListener: EnlistMessageChannelListener; @@ -15,7 +15,9 @@ describe("enlist message channel listener in main", () => { let offMock: jest.Mock; beforeEach(() => { - const di = getDiForUnitTesting(); + const di = createContainer("irrelevant"); + + registerFeature(di, messagingFeatureForMain); onMock = jest.fn(); offMock = jest.fn(); @@ -27,9 +29,7 @@ describe("enlist message channel listener in main", () => { di.override(ipcMainInjectable, () => ipcMainStub); - enlistMessageChannelListener = di.inject( - enlistMessageChannelListenerInjectionToken, - ); + enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken); }); describe("when called", () => { @@ -40,6 +40,7 @@ describe("enlist message channel listener in main", () => { handlerMock = jest.fn(); disposer = enlistMessageChannelListener({ + id: "some-listener", channel: { id: "some-channel-id" }, handler: handlerMock, }); @@ -50,10 +51,7 @@ describe("enlist message channel listener in main", () => { }); it("registers the listener", () => { - expect(onMock).toHaveBeenCalledWith( - "some-channel-id", - expect.any(Function), - ); + expect(onMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function)); }); it("does not de-register the listener yet", () => { @@ -62,11 +60,11 @@ describe("enlist message channel listener in main", () => { describe("when message arrives", () => { beforeEach(() => { - onMock.mock.calls[0][1]({} as IpcMainEvent, "some-message"); + onMock.mock.calls[0][1]({ frameId: 42, processId: 84 } as IpcMainEvent, "some-message"); }); it("calls the handler with the message", () => { - expect(handlerMock).toHaveBeenCalledWith("some-message"); + expect(handlerMock).toHaveBeenCalledWith("some-message", { frameId: 42, processId: 84 }); }); it("when disposing the listener, de-registers the listener", () => { @@ -77,21 +75,21 @@ describe("enlist message channel listener in main", () => { }); it("given number as message, when message arrives, calls the handler with the message", () => { - onMock.mock.calls[0][1]({} as IpcMainEvent, 42); + onMock.mock.calls[0][1]({ frameId: 42, processId: 84 } as IpcMainEvent, 42); - expect(handlerMock).toHaveBeenCalledWith(42); + expect(handlerMock).toHaveBeenCalledWith(42, { frameId: 42, processId: 84 }); }); it("given boolean as message, when message arrives, calls the handler with the message", () => { - onMock.mock.calls[0][1]({} as IpcMainEvent, true); + onMock.mock.calls[0][1]({ frameId: 42, processId: 84 } as IpcMainEvent, true); - expect(handlerMock).toHaveBeenCalledWith(true); + expect(handlerMock).toHaveBeenCalledWith(true, { frameId: 42, processId: 84 }); }); it("given object as message, when message arrives, calls the handler with the message", () => { - onMock.mock.calls[0][1]({} as IpcMainEvent, { some: "object" }); + onMock.mock.calls[0][1]({ frameId: 42, processId: 84 } as IpcMainEvent, { some: "object" }); - expect(handlerMock).toHaveBeenCalledWith({ some: "object" }); + expect(handlerMock).toHaveBeenCalledWith({ some: "object" }, { frameId: 42, processId: 84 }); }); }); }); diff --git a/packages/core/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.injectable.ts b/packages/technical-features/messaging/electron/main/src/channel-listeners/enlist-request-channel-listener.injectable.ts similarity index 53% rename from packages/core/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.injectable.ts rename to packages/technical-features/messaging/electron/main/src/channel-listeners/enlist-request-channel-listener.injectable.ts index 08a600297c..f46976a3f1 100644 --- a/packages/core/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.injectable.ts +++ b/packages/technical-features/messaging/electron/main/src/channel-listeners/enlist-request-channel-listener.injectable.ts @@ -1,21 +1,18 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ import { getInjectable } from "@ogre-tools/injectable"; import type { IpcMainInvokeEvent } from "electron"; -import type { Disposer } from "@k8slens/utilities"; -import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; -import type { RequestChannelListener } from "./listener-tokens"; -import ipcMainInjectionToken from "../../../../common/ipc/ipc-main-injection-token"; +import ipcMainInjectable from "../ipc-main/ipc-main.injectable"; +import type { RequestChannel, RequestChannelListener } from "@k8slens/messaging"; +import { enlistRequestChannelListenerInjectionToken } from "@k8slens/messaging"; -export type EnlistRequestChannelListener = >(listener: RequestChannelListener) => Disposer; +export type EnlistRequestChannelListener = >( + listener: RequestChannelListener, +) => () => void; const enlistRequestChannelListenerInjectable = getInjectable({ id: "enlist-request-channel-listener-for-main", instantiate: (di): EnlistRequestChannelListener => { - const ipcMain = di.inject(ipcMainInjectionToken); + const ipcMain = di.inject(ipcMainInjectable); return ({ channel, handler }) => { const nativeHandleCallback = (_: IpcMainInvokeEvent, request: unknown) => handler(request); @@ -27,6 +24,8 @@ const enlistRequestChannelListenerInjectable = getInjectable({ }; }; }, + + injectionToken: enlistRequestChannelListenerInjectionToken, }); export default enlistRequestChannelListenerInjectable; diff --git a/packages/core/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.test.ts b/packages/technical-features/messaging/electron/main/src/channel-listeners/enlist-request-channel-listener.test.ts similarity index 86% rename from packages/core/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.test.ts rename to packages/technical-features/messaging/electron/main/src/channel-listeners/enlist-request-channel-listener.test.ts index 188a11866b..68672f4e95 100644 --- a/packages/core/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.test.ts +++ b/packages/technical-features/messaging/electron/main/src/channel-listeners/enlist-request-channel-listener.test.ts @@ -1,17 +1,14 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import ipcMainInjectable from "../ipc-main/ipc-main.injectable"; import type { IpcMain, IpcMainInvokeEvent } from "electron"; -import { getPromiseStatus } from "@k8slens/test-utils"; import type { AsyncFnMock } from "@async-fn/jest"; import asyncFn from "@async-fn/jest"; -import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; import type { EnlistRequestChannelListener } from "./enlist-request-channel-listener.injectable"; import enlistRequestChannelListenerInjectable from "./enlist-request-channel-listener.injectable"; -import type { RequestChannelHandler } from "./listener-tokens"; +import type { RequestChannel, RequestChannelHandler } from "@k8slens/messaging"; +import { getPromiseStatus } from "@k8slens/test-utils"; +import { createContainer } from "@ogre-tools/injectable"; +import { registerFeature } from "@k8slens/feature-core"; +import { messagingFeatureForMain } from "../feature"; type TestRequestChannel = RequestChannel; @@ -26,7 +23,9 @@ describe("enlist request channel listener in main", () => { let offMock: jest.Mock; beforeEach(() => { - const di = getDiForUnitTesting(); + const di = createContainer("irrelevant"); + + registerFeature(di, messagingFeatureForMain); handleMock = jest.fn(); offMock = jest.fn(); @@ -49,6 +48,7 @@ describe("enlist request channel listener in main", () => { handlerMock = asyncFn(); disposer = enlistRequestChannelListener({ + id: "some-listener", channel: testRequestChannel, handler: handlerMock, }); @@ -59,10 +59,7 @@ describe("enlist request channel listener in main", () => { }); it("registers the listener", () => { - expect(handleMock).toHaveBeenCalledWith( - "some-channel-id", - expect.any(Function), - ); + expect(handleMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function)); }); it("does not de-register the listener yet", () => { @@ -73,10 +70,7 @@ describe("enlist request channel listener in main", () => { let actualPromise: Promise; beforeEach(() => { - actualPromise = handleMock.mock.calls[0][1]( - {} as IpcMainInvokeEvent, - "some-request", - ); + actualPromise = handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, "some-request"); }); it("calls the handler with the request", () => { diff --git a/packages/technical-features/messaging/electron/main/src/feature.ts b/packages/technical-features/messaging/electron/main/src/feature.ts new file mode 100644 index 0000000000..250f5ed104 --- /dev/null +++ b/packages/technical-features/messaging/electron/main/src/feature.ts @@ -0,0 +1,18 @@ +import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration"; +import { getFeature } from "@k8slens/feature-core"; +import { messagingFeature } from "@k8slens/messaging"; + +export const messagingFeatureForMain = getFeature({ + id: "messaging-for-main", + + register: (di) => { + autoRegister({ + di, + targetModule: module, + + getRequireContexts: () => [require.context("./", true, /\.injectable\.(ts|tsx)$/)], + }); + }, + + dependencies: [messagingFeature], +}); diff --git a/packages/technical-features/messaging/electron/main/src/ipc-main/ipc-main.injectable.ts b/packages/technical-features/messaging/electron/main/src/ipc-main/ipc-main.injectable.ts new file mode 100644 index 0000000000..fc55a6b414 --- /dev/null +++ b/packages/technical-features/messaging/electron/main/src/ipc-main/ipc-main.injectable.ts @@ -0,0 +1,10 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import { ipcMain } from "electron"; + +const ipcMainInjectable = getInjectable({ + id: "ipc-main", + instantiate: () => ipcMain, + causesSideEffects: true, +}); + +export default ipcMainInjectable; diff --git a/packages/technical-features/messaging/electron/main/src/ipc-main/ipc-main.test.ts b/packages/technical-features/messaging/electron/main/src/ipc-main/ipc-main.test.ts new file mode 100644 index 0000000000..9a3e92c02e --- /dev/null +++ b/packages/technical-features/messaging/electron/main/src/ipc-main/ipc-main.test.ts @@ -0,0 +1,21 @@ +import { createContainer, DiContainer } from "@ogre-tools/injectable"; +import { registerFeature } from "@k8slens/feature-core"; +import ipcMainInjectable from "./ipc-main.injectable"; +import { ipcMain } from "electron"; +import { messagingFeatureForMain } from "../feature"; + +describe("ipc-main", () => { + let di: DiContainer; + + beforeEach(() => { + di = createContainer("irrelevant"); + + registerFeature(di, messagingFeatureForMain); + }); + + it("is the actual IPC-main of Electron", () => { + const actual = di.inject(ipcMainInjectable); + + expect(actual).toBe(ipcMain); + }); +}); diff --git a/packages/technical-features/messaging/electron/main/src/request-from-channel/request-from-channel.injectable.ts b/packages/technical-features/messaging/electron/main/src/request-from-channel/request-from-channel.injectable.ts new file mode 100644 index 0000000000..220daf9fc7 --- /dev/null +++ b/packages/technical-features/messaging/electron/main/src/request-from-channel/request-from-channel.injectable.ts @@ -0,0 +1,23 @@ +/* c8 ignore start */ +import { getInjectable } from "@ogre-tools/injectable"; +import { + RequestChannel, + RequestFromChannel, + requestFromChannelInjectionToken, +} from "@k8slens/messaging"; + +const requestFromChannelInjectable = getInjectable({ + id: "request-from-channel", + + instantiate: () => + ((channel: RequestChannel) => { + throw new Error( + `Tried to request from channel "${channel.id}" but requesting in "main" it's not supported yet`, + ); + }) as unknown as RequestFromChannel, + + injectionToken: requestFromChannelInjectionToken, +}); + +export default requestFromChannelInjectable; +/* c8 ignore stop */ diff --git a/packages/technical-features/messaging/electron/main/src/send-message-to-channel/allow-communication-listener.injectable.ts b/packages/technical-features/messaging/electron/main/src/send-message-to-channel/allow-communication-listener.injectable.ts new file mode 100644 index 0000000000..af74d7a810 --- /dev/null +++ b/packages/technical-features/messaging/electron/main/src/send-message-to-channel/allow-communication-listener.injectable.ts @@ -0,0 +1,21 @@ +import { getMessageChannel, getMessageChannelListenerInjectable } from "@k8slens/messaging"; +import frameIdsInjectable from "./frameIds.injectable"; + +const frameCommunicationAdminChannel = getMessageChannel( + "frame-communication-admin-channel", +); + +const allowCommunicationListenerInjectable = getMessageChannelListenerInjectable({ + id: "allow-communication", + channel: frameCommunicationAdminChannel, + + getHandler: (di) => { + const frameIds = di.inject(frameIdsInjectable); + + return (_, { frameId, processId }) => { + frameIds.add({ frameId, processId }); + }; + }, +}); + +export default allowCommunicationListenerInjectable; diff --git a/packages/technical-features/messaging/electron/main/src/send-message-to-channel/frameIds.injectable.ts b/packages/technical-features/messaging/electron/main/src/send-message-to-channel/frameIds.injectable.ts new file mode 100644 index 0000000000..6dff45afc2 --- /dev/null +++ b/packages/technical-features/messaging/electron/main/src/send-message-to-channel/frameIds.injectable.ts @@ -0,0 +1,8 @@ +import { getInjectable } from "@ogre-tools/injectable"; + +const frameIdsInjectable = getInjectable({ + id: "frame-ids", + instantiate: () => new Set<{ frameId: number; processId: number }>(), +}); + +export default frameIdsInjectable; diff --git a/packages/technical-features/messaging/electron/main/src/send-message-to-channel/get-web-contents.injectable.ts b/packages/technical-features/messaging/electron/main/src/send-message-to-channel/get-web-contents.injectable.ts new file mode 100644 index 0000000000..701c976621 --- /dev/null +++ b/packages/technical-features/messaging/electron/main/src/send-message-to-channel/get-web-contents.injectable.ts @@ -0,0 +1,12 @@ +/* c8 ignore start */ +import { getInjectable } from "@ogre-tools/injectable"; +import { webContents } from "electron"; + +const getWebContentsInjectable = getInjectable({ + id: "web-contents", + instantiate: () => () => webContents.getAllWebContents(), + causesSideEffects: true, +}); + +export default getWebContentsInjectable; +/* c8 ignore stop */ diff --git a/packages/technical-features/messaging/electron/main/src/send-message-to-channel/send-message-to-channel.injectable.test.ts b/packages/technical-features/messaging/electron/main/src/send-message-to-channel/send-message-to-channel.injectable.test.ts new file mode 100644 index 0000000000..5c5a486658 --- /dev/null +++ b/packages/technical-features/messaging/electron/main/src/send-message-to-channel/send-message-to-channel.injectable.test.ts @@ -0,0 +1,121 @@ +import { registerFeature } from "@k8slens/feature-core"; +import { createContainer, DiContainer } from "@ogre-tools/injectable"; +import { messagingFeatureForMain } from "../feature"; +import { getMessageChannel, sendMessageToChannelInjectionToken } from "@k8slens/messaging"; +import getWebContentsInjectable from "./get-web-contents.injectable"; +import type { WebContents } from "electron"; +import allowCommunicationListenerInjectable from "./allow-communication-listener.injectable"; + +const someChannel = getMessageChannel("some-channel"); + +describe("send-message-to-channel", () => { + let di: DiContainer; + + beforeEach(() => { + di = createContainer("irrelevant"); + + registerFeature(di, messagingFeatureForMain); + }); + + it("given no web contents, when sending a message, does not do anything", () => { + di.override(getWebContentsInjectable, () => () => []); + + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + + expect(() => sendMessageToChannel(someChannel, "some-message")).not.toThrow(); + }); + + describe("given web content that is alive", () => { + let sendToFrameMock: jest.Mock; + let sendMessageMock: jest.Mock; + + beforeEach(() => { + sendToFrameMock = jest.fn(); + sendMessageMock = jest.fn(); + + di.override(getWebContentsInjectable, () => () => [ + { + send: (...args: any[]) => sendMessageMock("first", ...args), + sendToFrame: (...args: any[]) => sendToFrameMock("first", ...args), + isDestroyed: () => false, + isCrashed: () => false, + } as unknown as WebContents, + { + send: (...args: any[]) => sendMessageMock("second", ...args), + sendToFrame: (...args: any[]) => sendToFrameMock("second", ...args), + isDestroyed: () => false, + isCrashed: () => false, + } as unknown as WebContents, + ]); + }); + + it("when sending message, sends the message to webcontents", () => { + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + + sendMessageToChannel(someChannel, "some-message"); + + expect(sendMessageMock.mock.calls).toEqual([ + ["first", "some-channel", "some-message"], + ["second", "some-channel", "some-message"], + ]); + }); + + describe("when multiple renderers inform that they are ready to listen messages", () => { + beforeEach(() => { + const allowCommunicationListener = di.inject(allowCommunicationListenerInjectable); + + allowCommunicationListener.handler(undefined, { frameId: 42, processId: 126 }); + allowCommunicationListener.handler(undefined, { frameId: 84, processId: 168 }); + }); + + describe("when sending a message", () => { + beforeEach(() => { + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + + sendMessageToChannel(someChannel, "some-message"); + }); + + it("sends the message to webcontents", () => { + expect(sendMessageMock.mock.calls).toEqual([ + ["first", "some-channel", "some-message"], + ["second", "some-channel", "some-message"], + ]); + }); + + it("sends the message to individual frames in webcontents", () => { + expect(sendToFrameMock.mock.calls).toEqual([ + ["first", [42, 126], "some-channel", "some-message"], + ["first", [84, 168], "some-channel", "some-message"], + + ["second", [42, 126], "some-channel", "some-message"], + ["second", [84, 168], "some-channel", "some-message"], + ]); + }); + }); + }); + }); + + it("given non alive web contents, when sending a message, does not send messages", () => { + const sendToWebContentsMock = jest.fn(); + + di.override(getWebContentsInjectable, () => () => [ + { + send: sendToWebContentsMock, + isDestroyed: () => true, + isCrashed: () => false, + } as unknown as WebContents, + + { + send: sendToWebContentsMock, + isDestroyed: () => false, + isCrashed: () => true, + } as unknown as WebContents, + ]); + + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + + sendMessageToChannel(someChannel, "irrelevant"); + + expect(sendToWebContentsMock).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/technical-features/messaging/electron/main/src/send-message-to-channel/send-message-to-channel.injectable.ts b/packages/technical-features/messaging/electron/main/src/send-message-to-channel/send-message-to-channel.injectable.ts new file mode 100644 index 0000000000..e68d8e1577 --- /dev/null +++ b/packages/technical-features/messaging/electron/main/src/send-message-to-channel/send-message-to-channel.injectable.ts @@ -0,0 +1,48 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import { pipeline } from "@ogre-tools/fp"; +import { SendMessageToChannel, sendMessageToChannelInjectionToken } from "@k8slens/messaging"; +import getWebContentsInjectable from "./get-web-contents.injectable"; +import { flatMap, reject } from "lodash/fp"; +import type { WebContents } from "electron"; +import frameIdsInjectable from "./frameIds.injectable"; + +const isDestroyed = (webContent: WebContents) => webContent.isDestroyed(); +const isCrashed = (webContent: WebContents) => webContent.isCrashed(); + +const forEach = + (predicate: (item: T) => void) => + (items: T[]) => + items.forEach(predicate); + +const sendMessageToChannelInjectable = getInjectable({ + id: "send-message-to-channel", + + instantiate: (di) => { + const getWebContents = di.inject(getWebContentsInjectable); + const frameIds = di.inject(frameIdsInjectable); + + return ((channel, message) => { + pipeline( + getWebContents(), + reject(isDestroyed), + reject(isCrashed), + + flatMap((webContent) => [ + (channelId: string, ...args: any[]) => webContent.send(channelId, ...args), + + ...[...frameIds].map(({ frameId, processId }) => (channelId: string, ...args: any[]) => { + webContent.sendToFrame([frameId, processId], channelId, ...args); + }), + ]), + + forEach((send) => { + send(channel.id, message); + }), + ); + }) as SendMessageToChannel; + }, + + injectionToken: sendMessageToChannelInjectionToken, +}); + +export default sendMessageToChannelInjectable; diff --git a/packages/technical-features/messaging/electron/main/tsconfig.json b/packages/technical-features/messaging/electron/main/tsconfig.json new file mode 100644 index 0000000000..1819203dc1 --- /dev/null +++ b/packages/technical-features/messaging/electron/main/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@k8slens/typescript/config/base.json", + "include": ["**/*.ts"] +} diff --git a/packages/technical-features/messaging/electron/main/webpack.config.js b/packages/technical-features/messaging/electron/main/webpack.config.js new file mode 100644 index 0000000000..3183f30179 --- /dev/null +++ b/packages/technical-features/messaging/electron/main/webpack.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/webpack").configForNode; diff --git a/packages/technical-features/messaging/electron/renderer/.eslintrc.json b/packages/technical-features/messaging/electron/renderer/.eslintrc.json new file mode 100644 index 0000000000..b15115cb69 --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "@k8slens/eslint-config/eslint", + "parserOptions": { + "project": "./tsconfig.json" + } +} diff --git a/packages/technical-features/messaging/electron/renderer/.prettierrc b/packages/technical-features/messaging/electron/renderer/.prettierrc new file mode 100644 index 0000000000..edd47b479e --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/.prettierrc @@ -0,0 +1 @@ +"@k8slens/eslint-config/prettier" diff --git a/packages/technical-features/messaging/electron/renderer/index.ts b/packages/technical-features/messaging/electron/renderer/index.ts new file mode 100644 index 0000000000..337d473d1f --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/index.ts @@ -0,0 +1 @@ +export { messagingFeatureForRenderer } from "./src/feature"; diff --git a/packages/technical-features/messaging/electron/renderer/jest.config.js b/packages/technical-features/messaging/electron/renderer/jest.config.js new file mode 100644 index 0000000000..c6074967eb --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/jest.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode; diff --git a/packages/technical-features/messaging/electron/renderer/package.json b/packages/technical-features/messaging/electron/renderer/package.json new file mode 100644 index 0000000000..75fcb8e38f --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/package.json @@ -0,0 +1,48 @@ +{ + "name": "@k8slens/messaging-for-renderer", + "private": false, + "version": "1.0.0-alpha.1", + "description": "Implementations for 'messaging' in Electron renderer", + "type": "commonjs", + + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/lensapp/lens.git" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "author": { + "name": "OpenLens Authors", + "email": "info@k8slens.dev" + }, + "license": "MIT", + "homepage": "https://github.com/lensapp/lens", + "scripts": { + "build": "webpack", + "dev": "webpack --mode=development --watch", + "test:unit": "jest --coverage --runInBand", + "lint": "lens-lint", + "lint:fix": "lens-lint --fix" + }, + "peerDependencies": { + "@k8slens/application": "^6.5.0-alpha.0", + "@k8slens/messaging": "^1.0.0-alpha.1", + "@k8slens/run-many": "^1.0.0-alpha.1", + "@k8slens/startable-stoppable": "^1.0.0-alpha.1", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", + "electron": "^22.3.3", + "lodash": "^4.17.21" + }, + "devDependencies": { + "@k8slens/eslint-config": "^6.5.0-alpha.1" + } +} diff --git a/packages/technical-features/messaging/electron/renderer/src/allow-communication-to-iframe.injectable.ts b/packages/technical-features/messaging/electron/renderer/src/allow-communication-to-iframe.injectable.ts new file mode 100644 index 0000000000..59abe7f931 --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/allow-communication-to-iframe.injectable.ts @@ -0,0 +1,25 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import { onLoadOfApplicationInjectionToken } from "@k8slens/application"; +import { getMessageChannel, sendMessageToChannelInjectionToken } from "@k8slens/messaging"; + +export const frameCommunicationAdminChannel = getMessageChannel( + "frame-communication-admin-channel", +); + +const allowCommunicationToIframeInjectable = getInjectable({ + id: "allow-communication-to-iframe-injectable", + + instantiate: (di) => { + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + + return { + run: () => { + sendMessageToChannel(frameCommunicationAdminChannel); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default allowCommunicationToIframeInjectable; diff --git a/packages/technical-features/messaging/electron/renderer/src/allow-communication-to-iframe.test.ts b/packages/technical-features/messaging/electron/renderer/src/allow-communication-to-iframe.test.ts new file mode 100644 index 0000000000..990c6601d9 --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/allow-communication-to-iframe.test.ts @@ -0,0 +1,35 @@ +import { createContainer, DiContainer } from "@ogre-tools/injectable"; +import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; +import { startApplicationInjectionToken } from "@k8slens/application"; +import { registerFeature } from "@k8slens/feature-core"; +import { messagingFeatureForRenderer } from "./feature"; +import { runInAction } from "mobx"; +import ipcRendererInjectable from "./ipc/ipc-renderer.injectable"; +import { sendMessageToChannelInjectionToken } from "@k8slens/messaging"; +import { frameCommunicationAdminChannel } from "./allow-communication-to-iframe.injectable"; + +describe("allow communication to iframe", () => { + let di: DiContainer; + let sendMessageToChannelMock: jest.Mock; + + beforeEach(() => { + di = createContainer("irrelevant"); + + registerMobX(di); + + runInAction(() => { + registerFeature(di, messagingFeatureForRenderer); + }); + + di.override(ipcRendererInjectable, () => ({ on: () => {} } as unknown)); + + sendMessageToChannelMock = jest.fn(); + di.override(sendMessageToChannelInjectionToken, () => sendMessageToChannelMock); + }); + + it("when application starts, sends message to communication channel to register the frame ID and process ID for further usage", async () => { + await di.inject(startApplicationInjectionToken)(); + + expect(sendMessageToChannelMock).toHaveBeenCalledWith(frameCommunicationAdminChannel); + }); +}); diff --git a/packages/technical-features/messaging/electron/renderer/src/feature.ts b/packages/technical-features/messaging/electron/renderer/src/feature.ts new file mode 100644 index 0000000000..c63092f2e5 --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/feature.ts @@ -0,0 +1,18 @@ +import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration"; +import { getFeature } from "@k8slens/feature-core"; +import { messagingFeature } from "@k8slens/messaging"; + +export const messagingFeatureForRenderer = getFeature({ + id: "messaging-for-renderer", + + register: (di) => { + autoRegister({ + di, + targetModule: module, + + getRequireContexts: () => [require.context("./", true, /\.injectable\.(ts|tsx)$/)], + }); + }, + + dependencies: [messagingFeature], +}); diff --git a/packages/technical-features/messaging/electron/renderer/src/ipc/ipc-renderer.injectable.ts b/packages/technical-features/messaging/electron/renderer/src/ipc/ipc-renderer.injectable.ts new file mode 100644 index 0000000000..a2ecffac15 --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/ipc/ipc-renderer.injectable.ts @@ -0,0 +1,10 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import { ipcRenderer } from "electron"; + +const ipcRendererInjectable = getInjectable({ + id: "ipc-renderer", + instantiate: () => ipcRenderer, + causesSideEffects: true, +}); + +export default ipcRendererInjectable; diff --git a/packages/technical-features/messaging/electron/renderer/src/ipc/ipc-renderer.test.ts b/packages/technical-features/messaging/electron/renderer/src/ipc/ipc-renderer.test.ts new file mode 100644 index 0000000000..e882b5f5aa --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/ipc/ipc-renderer.test.ts @@ -0,0 +1,27 @@ +import { createContainer, DiContainer } from "@ogre-tools/injectable"; +import { registerFeature } from "@k8slens/feature-core"; +import ipcRendererInjectable from "./ipc-renderer.injectable"; +import { messagingFeatureForRenderer } from "../feature"; +import { ipcRenderer } from "electron"; + +describe("ipc-renderer", () => { + let di: DiContainer; + + beforeEach(() => { + di = createContainer("irrelevant"); + + registerFeature(di, messagingFeatureForRenderer); + }); + + it("is not undefined", () => { + const actual = di.inject(ipcRendererInjectable); + + expect(actual).not.toBeUndefined(); + }); + + it("is IPC-renderer of Electron", () => { + const actual = di.inject(ipcRendererInjectable); + + expect(actual).toBe(ipcRenderer); + }); +}); diff --git a/packages/core/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts b/packages/technical-features/messaging/electron/renderer/src/listening-of-messages/enlist-message-channel-listener.injectable.ts similarity index 60% rename from packages/core/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts rename to packages/technical-features/messaging/electron/renderer/src/listening-of-messages/enlist-message-channel-listener.injectable.ts index 1e5b14bb40..6948e51073 100644 --- a/packages/core/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts +++ b/packages/technical-features/messaging/electron/renderer/src/listening-of-messages/enlist-message-channel-listener.injectable.ts @@ -1,11 +1,7 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import ipcRendererInjectable from "../ipc-renderer.injectable"; +import ipcRendererInjectable from "../ipc/ipc-renderer.injectable"; import { getInjectable } from "@ogre-tools/injectable"; import type { IpcRendererEvent } from "electron"; -import { enlistMessageChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token"; +import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; const enlistMessageChannelListenerInjectable = getInjectable({ id: "enlist-message-channel-listener-for-renderer", @@ -14,7 +10,7 @@ const enlistMessageChannelListenerInjectable = getInjectable({ const ipcRenderer = di.inject(ipcRendererInjectable); return ({ channel, handler }) => { - const nativeCallback = (_: IpcRendererEvent, message: unknown) => { + const nativeCallback = (event: IpcRendererEvent, message: unknown) => { handler(message); }; diff --git a/packages/core/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts b/packages/technical-features/messaging/electron/renderer/src/listening-of-messages/enlist-message-channel-listener.test.ts similarity index 73% rename from packages/core/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts rename to packages/technical-features/messaging/electron/renderer/src/listening-of-messages/enlist-message-channel-listener.test.ts index ac416a568e..dc89c7b3e7 100644 --- a/packages/core/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts +++ b/packages/technical-features/messaging/electron/renderer/src/listening-of-messages/enlist-message-channel-listener.test.ts @@ -1,12 +1,12 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; -import type { EnlistMessageChannelListener } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token"; -import { enlistMessageChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token"; import type { IpcRendererEvent, IpcRenderer } from "electron"; -import ipcRendererInjectable from "../ipc-renderer.injectable"; +import ipcRendererInjectable from "../ipc/ipc-renderer.injectable"; +import { + EnlistMessageChannelListener, + enlistMessageChannelListenerInjectionToken, +} from "@k8slens/messaging"; +import { createContainer } from "@ogre-tools/injectable"; +import { registerFeature } from "@k8slens/feature-core"; +import { messagingFeatureForRenderer } from "../feature"; describe("enlist message channel listener in renderer", () => { let enlistMessageChannelListener: EnlistMessageChannelListener; @@ -15,7 +15,9 @@ describe("enlist message channel listener in renderer", () => { let offMock: jest.Mock; beforeEach(() => { - const di = getDiForUnitTesting(); + const di = createContainer("irrelevant"); + + registerFeature(di, messagingFeatureForRenderer); onMock = jest.fn(); offMock = jest.fn(); @@ -27,9 +29,7 @@ describe("enlist message channel listener in renderer", () => { di.override(ipcRendererInjectable, () => ipcRendererStub); - enlistMessageChannelListener = di.inject( - enlistMessageChannelListenerInjectionToken, - ); + enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken); }); describe("when called", () => { @@ -40,6 +40,7 @@ describe("enlist message channel listener in renderer", () => { handlerMock = jest.fn(); disposer = enlistMessageChannelListener({ + id: "some-listener", channel: { id: "some-channel-id" }, handler: handlerMock, }); @@ -50,10 +51,7 @@ describe("enlist message channel listener in renderer", () => { }); it("registers the listener", () => { - expect(onMock).toHaveBeenCalledWith( - "some-channel-id", - expect.any(Function), - ); + expect(onMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function)); }); it("does not de-register the listener yet", () => { diff --git a/packages/technical-features/messaging/electron/renderer/src/listening-of-requests/enlist-request-channel-listener.injectable.ts b/packages/technical-features/messaging/electron/renderer/src/listening-of-requests/enlist-request-channel-listener.injectable.ts new file mode 100644 index 0000000000..92d6c978fe --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/listening-of-requests/enlist-request-channel-listener.injectable.ts @@ -0,0 +1,18 @@ +/* c8 ignore start */ +import { getInjectable } from "@ogre-tools/injectable"; +import { enlistRequestChannelListenerInjectionToken } from "@k8slens/messaging"; + +const enlistRequestChannelListenerInjectable = getInjectable({ + id: "enlist-request-channel-listener-for-renderer", + + instantiate: () => (listener) => { + throw new Error( + `Tried to enlist request channel "${listener.channel.id}" in "renderer", but requesting it's not supported yet.`, + ); + }, + + injectionToken: enlistRequestChannelListenerInjectionToken, +}); + +export default enlistRequestChannelListenerInjectable; +/* c8 ignore end */ diff --git a/packages/technical-features/messaging/electron/renderer/src/requesting-of-requests/invoke-ipc.injectable.ts b/packages/technical-features/messaging/electron/renderer/src/requesting-of-requests/invoke-ipc.injectable.ts new file mode 100644 index 0000000000..03329d0c92 --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/requesting-of-requests/invoke-ipc.injectable.ts @@ -0,0 +1,10 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import ipcRendererInjectable from "../ipc/ipc-renderer.injectable"; + +const invokeIpcInjectable = getInjectable({ + id: "invoke-ipc", + + instantiate: (di) => di.inject(ipcRendererInjectable).invoke, +}); + +export default invokeIpcInjectable; diff --git a/packages/technical-features/messaging/electron/renderer/src/requesting-of-requests/invoke-ipc.test.ts b/packages/technical-features/messaging/electron/renderer/src/requesting-of-requests/invoke-ipc.test.ts new file mode 100644 index 0000000000..e54c98616b --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/requesting-of-requests/invoke-ipc.test.ts @@ -0,0 +1,21 @@ +import { createContainer, DiContainer } from "@ogre-tools/injectable"; +import { registerFeature } from "@k8slens/feature-core"; +import { messagingFeatureForRenderer } from "../feature"; +import { ipcRenderer } from "electron"; +import invokeIpcInjectable from "./invoke-ipc.injectable"; + +describe("ipc-renderer", () => { + let di: DiContainer; + + beforeEach(() => { + di = createContainer("irrelevant"); + + registerFeature(di, messagingFeatureForRenderer); + }); + + it("is IPC-renderer invoke of Electron", () => { + const actual = di.inject(invokeIpcInjectable); + + expect(actual).toBe(ipcRenderer.invoke); + }); +}); diff --git a/packages/technical-features/messaging/electron/renderer/src/requesting-of-requests/request-from-channel.injectable.ts b/packages/technical-features/messaging/electron/renderer/src/requesting-of-requests/request-from-channel.injectable.ts new file mode 100644 index 0000000000..032042198a --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/requesting-of-requests/request-from-channel.injectable.ts @@ -0,0 +1,18 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import type { RequestFromChannel } from "@k8slens/messaging"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; +import invokeIpcInjectable from "./invoke-ipc.injectable"; + +const requestFromChannelInjectable = getInjectable({ + id: "request-from-channel", + + instantiate: (di) => { + const invokeIpc = di.inject(invokeIpcInjectable); + + return ((channel, request) => invokeIpc(channel.id, request)) as RequestFromChannel; + }, + + injectionToken: requestFromChannelInjectionToken, +}); + +export default requestFromChannelInjectable; diff --git a/packages/technical-features/messaging/electron/renderer/src/requesting-of-requests/request-from-channel.test.ts b/packages/technical-features/messaging/electron/renderer/src/requesting-of-requests/request-from-channel.test.ts new file mode 100644 index 0000000000..930040f117 --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/requesting-of-requests/request-from-channel.test.ts @@ -0,0 +1,46 @@ +import { createContainer, DiContainer } from "@ogre-tools/injectable"; +import { registerFeature } from "@k8slens/feature-core"; +import { requestFromChannelInjectionToken } from "@k8slens/messaging"; +import { messagingFeatureForRenderer } from "../feature"; +import type { RequestChannel } from "@k8slens/messaging"; +import invokeIpcInjectable from "./invoke-ipc.injectable"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; + +describe("request-from-channel", () => { + let di: DiContainer; + let invokeIpcMock: AsyncFnMock<() => Promise>; + + beforeEach(() => { + di = createContainer("irrelevant"); + + registerFeature(di, messagingFeatureForRenderer); + + invokeIpcMock = asyncFn(); + di.override(invokeIpcInjectable, () => invokeIpcMock); + }); + + describe("when called", () => { + let actualPromise: Promise; + + beforeEach(() => { + const requestFromChannel = di.inject(requestFromChannelInjectionToken); + + const someChannel: RequestChannel = { + id: "some-channel-id", + }; + + actualPromise = requestFromChannel(someChannel, "some-request-payload"); + }); + + it("invokes ipcRenderer of Electron", () => { + expect(invokeIpcMock).toHaveBeenCalledWith("some-channel-id", "some-request-payload"); + }); + + it("when invoke resolves with response, resolves with said response", async () => { + await invokeIpcMock.resolve(42); + + expect(await actualPromise).toBe(42); + }); + }); +}); diff --git a/packages/technical-features/messaging/electron/renderer/src/sending-of-messages/message-to-channel.injectable.ts b/packages/technical-features/messaging/electron/renderer/src/sending-of-messages/message-to-channel.injectable.ts new file mode 100644 index 0000000000..469a1548ab --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/sending-of-messages/message-to-channel.injectable.ts @@ -0,0 +1,19 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import sendToIpcInjectable from "./send-to-ipc.injectable"; +import { SendMessageToChannel, sendMessageToChannelInjectionToken } from "@k8slens/messaging"; + +const messageToChannelInjectable = getInjectable({ + id: "message-to-channel", + + instantiate: (di) => { + const sendToIpc = di.inject(sendToIpcInjectable); + + return ((channel, message) => { + sendToIpc(channel.id, message); + }) as SendMessageToChannel; + }, + + injectionToken: sendMessageToChannelInjectionToken, +}); + +export default messageToChannelInjectable; diff --git a/packages/technical-features/messaging/electron/renderer/src/sending-of-messages/message-to-channel.test.ts b/packages/technical-features/messaging/electron/renderer/src/sending-of-messages/message-to-channel.test.ts new file mode 100644 index 0000000000..07989cf16e --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/sending-of-messages/message-to-channel.test.ts @@ -0,0 +1,38 @@ +import { createContainer, DiContainer } from "@ogre-tools/injectable"; +import { registerFeature } from "@k8slens/feature-core"; +import { sendMessageToChannelInjectionToken } from "@k8slens/messaging"; +import { messagingFeatureForRenderer } from "../feature"; +import type { MessageChannel } from "@k8slens/messaging"; +import sendToIpcInjectable from "./send-to-ipc.injectable"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; + +describe("message-from-channel", () => { + let di: DiContainer; + let sendToIpcMock: AsyncFnMock<() => Promise>; + + beforeEach(() => { + di = createContainer("irrelevant"); + + registerFeature(di, messagingFeatureForRenderer); + + sendToIpcMock = asyncFn(); + di.override(sendToIpcInjectable, () => sendToIpcMock); + }); + + describe("when called", () => { + beforeEach(() => { + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + + const someChannel: MessageChannel = { + id: "some-channel-id", + }; + + sendMessageToChannel(someChannel, 42); + }); + + it("sends to ipcRenderer of Electron", () => { + expect(sendToIpcMock).toHaveBeenCalledWith("some-channel-id", 42); + }); + }); +}); diff --git a/packages/technical-features/messaging/electron/renderer/src/sending-of-messages/send-to-ipc.injectable.ts b/packages/technical-features/messaging/electron/renderer/src/sending-of-messages/send-to-ipc.injectable.ts new file mode 100644 index 0000000000..059dcbe8c2 --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/sending-of-messages/send-to-ipc.injectable.ts @@ -0,0 +1,10 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import ipcRendererInjectable from "../ipc/ipc-renderer.injectable"; + +const sendToIpcInjectable = getInjectable({ + id: "send-to-ipc", + + instantiate: (di) => di.inject(ipcRendererInjectable).send, +}); + +export default sendToIpcInjectable; diff --git a/packages/technical-features/messaging/electron/renderer/src/sending-of-messages/send-to-ipc.test.ts b/packages/technical-features/messaging/electron/renderer/src/sending-of-messages/send-to-ipc.test.ts new file mode 100644 index 0000000000..9d1b2303f5 --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/src/sending-of-messages/send-to-ipc.test.ts @@ -0,0 +1,21 @@ +import { createContainer, DiContainer } from "@ogre-tools/injectable"; +import { registerFeature } from "@k8slens/feature-core"; +import { messagingFeatureForRenderer } from "../feature"; +import { ipcRenderer } from "electron"; +import sendToIpcInjectable from "./send-to-ipc.injectable"; + +describe("ipc-renderer", () => { + let di: DiContainer; + + beforeEach(() => { + di = createContainer("irrelevant"); + + registerFeature(di, messagingFeatureForRenderer); + }); + + it("is IPC-renderer send of Electron", () => { + const actual = di.inject(sendToIpcInjectable); + + expect(actual).toBe(ipcRenderer.send); + }); +}); diff --git a/packages/technical-features/messaging/electron/renderer/tsconfig.json b/packages/technical-features/messaging/electron/renderer/tsconfig.json new file mode 100644 index 0000000000..1819203dc1 --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@k8slens/typescript/config/base.json", + "include": ["**/*.ts"] +} diff --git a/packages/technical-features/messaging/electron/renderer/webpack.config.js b/packages/technical-features/messaging/electron/renderer/webpack.config.js new file mode 100644 index 0000000000..3183f30179 --- /dev/null +++ b/packages/technical-features/messaging/electron/renderer/webpack.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/webpack").configForNode; diff --git a/packages/technical-features/messaging/message-bridge-fake/.eslintrc.json b/packages/technical-features/messaging/message-bridge-fake/.eslintrc.json new file mode 100644 index 0000000000..b15115cb69 --- /dev/null +++ b/packages/technical-features/messaging/message-bridge-fake/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "@k8slens/eslint-config/eslint", + "parserOptions": { + "project": "./tsconfig.json" + } +} diff --git a/packages/technical-features/messaging/message-bridge-fake/.prettierrc b/packages/technical-features/messaging/message-bridge-fake/.prettierrc new file mode 100644 index 0000000000..edd47b479e --- /dev/null +++ b/packages/technical-features/messaging/message-bridge-fake/.prettierrc @@ -0,0 +1 @@ +"@k8slens/eslint-config/prettier" diff --git a/packages/technical-features/messaging/message-bridge-fake/index.ts b/packages/technical-features/messaging/message-bridge-fake/index.ts new file mode 100644 index 0000000000..6b85a40628 --- /dev/null +++ b/packages/technical-features/messaging/message-bridge-fake/index.ts @@ -0,0 +1,3 @@ +export type { MessageBridgeFake } from "./src/get-message-bridge-fake/get-message-bridge-fake"; + +export { getMessageBridgeFake } from "./src/get-message-bridge-fake/get-message-bridge-fake"; diff --git a/packages/technical-features/messaging/message-bridge-fake/jest.config.js b/packages/technical-features/messaging/message-bridge-fake/jest.config.js new file mode 100644 index 0000000000..38d54ab7b6 --- /dev/null +++ b/packages/technical-features/messaging/message-bridge-fake/jest.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact; diff --git a/packages/technical-features/messaging/message-bridge-fake/package.json b/packages/technical-features/messaging/message-bridge-fake/package.json new file mode 100644 index 0000000000..6837504a0e --- /dev/null +++ b/packages/technical-features/messaging/message-bridge-fake/package.json @@ -0,0 +1,49 @@ +{ + "name": "@k8slens/messaging-fake-bridge", + "private": false, + "version": "1.0.0-alpha.1", + "description": "Fake implementation to bridge multiple dependency injection containers.", + "type": "commonjs", + + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/lensapp/lens.git" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "author": { + "name": "OpenLens Authors", + "email": "info@k8slens.dev" + }, + "license": "MIT", + "homepage": "https://github.com/lensapp/lens", + "scripts": { + "build": "webpack", + "dev": "webpack --mode=development --watch", + "test:unit": "jest --coverage --runInBand", + "lint:fix": "lens-lint --fix", + "lint": "lens-lint" + }, + "peerDependencies": { + "@k8slens/messaging": "^1.0.0-alpha.1", + "@ogre-tools/fp": "^15.1.2", + "@ogre-tools/injectable": "^15.1.2", + "lodash": "^4.17.21" + }, + + "devDependencies": { + "@async-fn/jest": "^1.6.4", + "@k8slens/feature-core": "6.5.0-alpha.1", + "@k8slens/eslint-config": "^6.5.0-alpha.1", + "@ogre-tools/injectable-extension-for-mobx": "^15.1.2", + "mobx": "^6.7.0" + } +} diff --git a/packages/technical-features/messaging/message-bridge-fake/src/get-message-bridge-fake/get-message-bridge-fake.test.ts b/packages/technical-features/messaging/message-bridge-fake/src/get-message-bridge-fake/get-message-bridge-fake.test.ts new file mode 100644 index 0000000000..e91109df10 --- /dev/null +++ b/packages/technical-features/messaging/message-bridge-fake/src/get-message-bridge-fake/get-message-bridge-fake.test.ts @@ -0,0 +1,397 @@ +import { createContainer, DiContainer, Injectable } from "@ogre-tools/injectable"; +import asyncFn, { AsyncFnMock } from "@async-fn/jest"; +import { registerFeature } from "@k8slens/feature-core"; +import { + getMessageChannel, + getMessageChannelListenerInjectable, + getRequestChannel, + getRequestChannelListenerInjectable, + MessageChannel, + testUtils, + RequestChannel, + requestFromChannelInjectionToken, + sendMessageToChannelInjectionToken, +} from "@k8slens/messaging"; + +import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; +import { runInAction } from "mobx"; +import { getPromiseStatus } from "@k8slens/test-utils"; +import { getMessageBridgeFake } from "./get-message-bridge-fake"; +import { startApplicationInjectionToken } from "@k8slens/application"; + +type SomeMessageChannel = MessageChannel; +type SomeRequestChannel = RequestChannel; + +const someMessageChannel: SomeMessageChannel = getMessageChannel("some-message-channel"); +const someRequestChannel: SomeRequestChannel = getRequestChannel("some-request-channel"); +const someOtherRequestChannel: SomeRequestChannel = { + id: "some-other-request-channel", +}; +const someRequestChannelWithoutListeners: SomeRequestChannel = { + id: "some-request-channel-without-listeners", +}; + +[{ scenarioIsAsync: true }, { scenarioIsAsync: false }].forEach(({ scenarioIsAsync }) => + describe(`get-message-bridge-fake, given running as ${ + scenarioIsAsync ? "async" : "sync" + }`, () => { + let messageBridgeFake: any; + + beforeEach(() => { + messageBridgeFake = getMessageBridgeFake(); + }); + + describe("given multiple DIs are involved", () => { + let someDi1: DiContainer; + let someDi2: DiContainer; + let someDiWithoutListeners: DiContainer; + + beforeEach(async () => { + someDi1 = createContainer("some-di-1"); + someDi2 = createContainer("some-di-2"); + + someDiWithoutListeners = createContainer("some-di-3"); + + registerMobX(someDi1); + registerMobX(someDi2); + registerMobX(someDiWithoutListeners); + + runInAction(() => { + const feature = testUtils.messagingFeatureForUnitTesting; + + registerFeature(someDi1, feature); + registerFeature(someDi2, feature); + registerFeature(someDiWithoutListeners, feature); + }); + + messageBridgeFake.involve(someDi1, someDi2, someDiWithoutListeners); + + if (scenarioIsAsync) { + messageBridgeFake.setAsync(scenarioIsAsync); + } + + await Promise.all([ + someDi1.inject(startApplicationInjectionToken)(), + someDi2.inject(startApplicationInjectionToken)(), + someDiWithoutListeners.inject(startApplicationInjectionToken)(), + ]); + }); + + describe("given there are message listeners", () => { + let someHandler1MockInDi1: jest.Mock; + let someHandler1MockInDi2: jest.Mock; + let someHandler2MockInDi2: jest.Mock; + let someListener1InDi2: Injectable; + + beforeEach(() => { + someHandler1MockInDi1 = jest.fn(); + someHandler1MockInDi2 = jest.fn(); + someHandler2MockInDi2 = jest.fn(); + + const someListener1InDi1 = getMessageChannelListenerInjectable({ + id: "some-listener-in-di-1", + channel: someMessageChannel, + getHandler: () => someHandler1MockInDi1, + }); + + someListener1InDi2 = getMessageChannelListenerInjectable({ + id: "some-listener-in-di-2", + channel: someMessageChannel, + getHandler: () => someHandler1MockInDi2, + }); + + const someListener2InDi2 = getMessageChannelListenerInjectable({ + id: "some-listener-2-in-di-2", + channel: someMessageChannel, + getHandler: () => someHandler2MockInDi2, + }); + + runInAction(() => { + someDi1.register(someListener1InDi1); + someDi2.register(someListener1InDi2); + someDi2.register(someListener2InDi2); + }); + }); + + describe("given there is a listener in di-2 that responds to a message with a message", () => { + beforeEach(() => { + const someResponder = getMessageChannelListenerInjectable({ + id: "some-responder-di-2", + channel: someMessageChannel, + + getHandler: (di) => { + const sendMessage = di.inject(sendMessageToChannelInjectionToken); + + return (message) => { + sendMessage(someMessageChannel, `some-response-to: ${message}`); + }; + }, + }); + + runInAction(() => { + someDi2.register(someResponder); + }); + }); + + describe("given a message is sent in di-1", () => { + beforeEach(() => { + const sendMessageToChannelFromDi1 = someDi1.inject( + sendMessageToChannelInjectionToken, + ); + + sendMessageToChannelFromDi1(someMessageChannel, "some-message"); + }); + + const scenarioTitle = scenarioIsAsync + ? "when all message steps are propagated using a wrapper" + : "immediately"; + + // eslint-disable-next-line jest/valid-title + describe(scenarioTitle, () => { + let someWrapper: jest.Mock; + + beforeEach((done) => { + someWrapper = jest.fn((propagation) => propagation()); + + if (scenarioIsAsync) { + messageBridgeFake.messagePropagationRecursive(someWrapper).then(done); + } else { + done(); + } + }); + + it("the response gets handled in di-1", () => { + expect(someHandler1MockInDi1).toHaveBeenCalledWith( + "some-response-to: some-message", + { + frameId: 42, + processId: 42, + }, + ); + }); + + scenarioIsAsync && + it("the wrapper gets called with the both propagations", () => { + expect(someWrapper).toHaveBeenCalledTimes(2); + }); + }); + + const scenarioName: string = scenarioIsAsync + ? "when all message steps are propagated not using a wrapper" + : "immediately"; + + // eslint-disable-next-line jest/valid-title + describe(scenarioName, () => { + beforeEach((done) => { + if (scenarioIsAsync) { + messageBridgeFake.messagePropagationRecursive().then(done); + } else { + done(); + } + }); + + it("the response gets handled in di-1", () => { + expect(someHandler1MockInDi1).toHaveBeenCalledWith( + "some-response-to: some-message", + { + frameId: 42, + processId: 42, + }, + ); + }); + }); + }); + }); + + describe("when sending message in a DI", () => { + beforeEach(() => { + const sendMessageToChannelFromDi1 = someDi1.inject(sendMessageToChannelInjectionToken); + + sendMessageToChannelFromDi1(someMessageChannel, "some-message"); + }); + + it("listener in sending DI does not handle the message", () => { + expect(someHandler1MockInDi1).not.toHaveBeenCalled(); + }); + + scenarioIsAsync && + it("listeners in other than sending DIs do not handle the message yet", () => { + expect(someHandler1MockInDi2).not.toHaveBeenCalled(); + expect(someHandler2MockInDi2).not.toHaveBeenCalled(); + }); + + const scenarioName = scenarioIsAsync ? "when messages are propagated" : "immediately"; + + // eslint-disable-next-line jest/valid-title + describe(scenarioName, () => { + beforeEach((done) => { + if (scenarioIsAsync) { + messageBridgeFake.messagePropagation().then(done); + } else { + done(); + } + }); + + it("listeners in other than sending DIs handle the message", () => { + expect(someHandler1MockInDi2).toHaveBeenCalledWith("some-message", { + frameId: 42, + processId: 42, + }); + + expect(someHandler2MockInDi2).toHaveBeenCalledWith("some-message", { + frameId: 42, + processId: 42, + }); + }); + }); + + scenarioIsAsync && + describe("when messages are propagated using a wrapper, such as act() in react testing lib", () => { + let someWrapper: jest.Mock; + + beforeEach(async () => { + someWrapper = jest.fn((observation) => observation()); + + await messageBridgeFake.messagePropagation(someWrapper); + }); + + it("the wrapper gets called with the related propagation", async () => { + expect(someWrapper).toHaveBeenCalledTimes(1); + }); + + it("listeners still handle the message", () => { + expect(someHandler1MockInDi2).toHaveBeenCalledWith("some-message", { + frameId: 42, + processId: 42, + }); + + expect(someHandler2MockInDi2).toHaveBeenCalledWith("some-message", { + frameId: 42, + processId: 42, + }); + }); + }); + }); + + it("given a listener is deregistered, when sending message, deregistered listener does not handle the message", () => { + runInAction(() => { + someDi2.deregister(someListener1InDi2); + }); + + const sendMessageToChannelFromDi1 = someDi1.inject(sendMessageToChannelInjectionToken); + + someHandler1MockInDi2.mockClear(); + + sendMessageToChannelFromDi1(someMessageChannel, "irrelevant"); + + expect(someHandler1MockInDi2).not.toHaveBeenCalled(); + }); + }); + + describe("given there are request listeners", () => { + let someHandler1MockInDi1: AsyncFnMock<(message: string) => Promise>; + + let someHandler1MockInDi2: AsyncFnMock<(message: string) => Promise>; + + let someListener1InDi2: Injectable; + let actualPromise: Promise; + + beforeEach(() => { + someHandler1MockInDi1 = asyncFn(); + someHandler1MockInDi2 = asyncFn(); + + const someListener1InDi1 = getRequestChannelListenerInjectable({ + id: "some-request-listener-in-di-1", + channel: someOtherRequestChannel, + getHandler: () => someHandler1MockInDi1, + }); + + someListener1InDi2 = getRequestChannelListenerInjectable({ + id: "some-request-listener-in-di-2", + channel: someRequestChannel, + getHandler: () => someHandler1MockInDi2, + }); + + runInAction(() => { + someDi1.register(someListener1InDi1); + someDi2.register(someListener1InDi2); + }); + }); + + describe("when requesting from a channel in a DI", () => { + beforeEach(() => { + const requestFromChannelFromDi1 = someDi1.inject(requestFromChannelInjectionToken); + + actualPromise = requestFromChannelFromDi1(someRequestChannel, "some-request"); + }); + + it("listener in requesting DI does not handle the request", () => { + expect(someHandler1MockInDi1).not.toHaveBeenCalled(); + }); + + it("the listener in other than requesting DIs handle the request", () => { + expect(someHandler1MockInDi2).toHaveBeenCalledWith("some-request"); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("when handle resolves, resolves with response", async () => { + await someHandler1MockInDi2.resolve(42); + + const actual = await actualPromise; + + expect(actual).toBe(42); + }); + }); + + it("given a listener is deregistered, when requesting, deregistered listener does not handle the request", () => { + runInAction(() => { + someDi2.deregister(someListener1InDi2); + }); + + const sendMessageToChannelFromDi1 = someDi1.inject(sendMessageToChannelInjectionToken); + + someHandler1MockInDi2.mockClear(); + + sendMessageToChannelFromDi1(someMessageChannel, "irrelevant"); + + expect(someHandler1MockInDi2).not.toHaveBeenCalled(); + }); + + it("given there are multiple listeners between different DIs for same channel, when requesting, throws", () => { + const someConflictingListenerInjectable = getRequestChannelListenerInjectable({ + id: "conflicting-listener", + channel: someRequestChannel, + getHandler: () => () => 84, + }); + + runInAction(() => { + someDi1.register(someConflictingListenerInjectable); + }); + + const requestFromChannelFromDi2 = someDi2.inject(requestFromChannelInjectionToken); + + return expect(() => + requestFromChannelFromDi2(someRequestChannel, "irrelevant"), + ).rejects.toThrow( + 'Tried to make a request but multiple listeners were discovered for channel "some-request-channel" in multiple DIs.', + ); + }); + + it("when requesting from channel without listener, throws", () => { + const requestFromChannel = someDi1.inject(requestFromChannelInjectionToken); + + return expect(() => + requestFromChannel(someRequestChannelWithoutListeners, "irrelevant"), + ).rejects.toThrow( + 'Tried to make a request but no listeners for channel "some-request-channel-without-listeners" was discovered in any DIs', + ); + }); + }); + }); + }), +); diff --git a/packages/technical-features/messaging/message-bridge-fake/src/get-message-bridge-fake/get-message-bridge-fake.ts b/packages/technical-features/messaging/message-bridge-fake/src/get-message-bridge-fake/get-message-bridge-fake.ts new file mode 100644 index 0000000000..ef8eed6461 --- /dev/null +++ b/packages/technical-features/messaging/message-bridge-fake/src/get-message-bridge-fake/get-message-bridge-fake.ts @@ -0,0 +1,196 @@ +import type { DiContainer } from "@ogre-tools/injectable"; +import type { Channel, MessageChannelHandler, RequestChannelHandler } from "@k8slens/messaging"; + +import { + enlistMessageChannelListenerInjectionToken, + enlistRequestChannelListenerInjectionToken, + RequestFromChannel, + requestFromChannelInjectionToken, + sendMessageToChannelInjectionToken, +} from "@k8slens/messaging"; + +import { pipeline } from "@ogre-tools/fp"; +import { filter, map } from "lodash/fp"; +import asyncFn, { AsyncFnMock } from "@async-fn/jest"; + +export type MessageBridgeFake = { + involve: (...dis: DiContainer[]) => void; + messagePropagation: () => Promise; + messagePropagationRecursive: (callback: any) => any; + setAsync: (value: boolean) => void; +}; + +const overrideMessaging = ({ + di, + messageListenersByDi, + messagePropagationBuffer, + getAsyncModeStatus, +}: { + di: DiContainer; + + messageListenersByDi: Map>>>; + + messagePropagationBuffer: Set<{ resolve: () => Promise }>; + + getAsyncModeStatus: () => boolean; +}) => { + const messageHandlersByChannel = new Map>>(); + + messageListenersByDi.set(di, messageHandlersByChannel); + + di.override(sendMessageToChannelInjectionToken, () => (channel, message) => { + const allOtherDis = [...messageListenersByDi.keys()].filter((x) => x !== di); + + allOtherDis.forEach((otherDi) => { + const listeners = messageListenersByDi.get(otherDi); + + const handlersForChannel = listeners?.get(channel.id); + + if (!handlersForChannel) { + return; + } + + if (getAsyncModeStatus()) { + const resolvableHandlePromise = asyncFn(); + + resolvableHandlePromise().then(() => { + handlersForChannel.forEach((handler) => handler(message, { frameId: 42, processId: 42 })); + }); + + messagePropagationBuffer.add(resolvableHandlePromise); + } else { + handlersForChannel.forEach((handler) => handler(message, { frameId: 42, processId: 42 })); + } + }); + }); + + di.override(enlistMessageChannelListenerInjectionToken, () => (listener) => { + if (!messageHandlersByChannel.has(listener.channel.id)) { + messageHandlersByChannel.set(listener.channel.id, new Set()); + } + + const handlerSet = messageHandlersByChannel.get(listener.channel.id); + + handlerSet?.add(listener.handler); + + return () => { + handlerSet?.delete(listener.handler); + }; + }); +}; + +const overrideRequesting = ({ + di, + requestListenersByDi, +}: { + di: DiContainer; + + requestListenersByDi: Map>>>; +}) => { + const requestHandlersByChannel = new Map>>(); + + requestListenersByDi.set(di, requestHandlersByChannel); + + di.override( + requestFromChannelInjectionToken, + () => + (async (channel, request) => + pipeline( + [...requestListenersByDi.values()], + map((listenersByChannel) => listenersByChannel?.get(channel.id)), + filter((x) => !!x), + + (channelSpecificListeners) => { + if (channelSpecificListeners.length === 0) { + throw new Error( + `Tried to make a request but no listeners for channel "${channel.id}" was discovered in any DIs`, + ); + } + + if (channelSpecificListeners.length > 1) { + throw new Error( + `Tried to make a request but multiple listeners were discovered for channel "${channel.id}" in multiple DIs.`, + ); + } + + const listeners = channelSpecificListeners[0]; + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const [handler] = listeners!; + + return handler; + }, + + async (handler) => handler(request), + )) as RequestFromChannel, + ); + + di.override(enlistRequestChannelListenerInjectionToken, () => (listener) => { + if (!requestHandlersByChannel.has(listener.channel.id)) { + requestHandlersByChannel.set(listener.channel.id, new Set()); + } + + const handlerSet = requestHandlersByChannel.get(listener.channel.id); + + handlerSet?.add(listener.handler); + + return () => { + handlerSet?.delete(listener.handler); + }; + }); +}; + +export const getMessageBridgeFake = (): MessageBridgeFake => { + const messageListenersByDi = new Map< + DiContainer, + Map>> + >(); + + const requestListenersByDi = new Map< + DiContainer, + Map>> + >(); + + const messagePropagationBuffer = new Set void>>(); + + const messagePropagation = async (wrapper: (callback: any) => any = (callback) => callback()) => { + const oldMessages = [...messagePropagationBuffer.values()]; + + messagePropagationBuffer.clear(); + await Promise.all(oldMessages.map((x) => wrapper(x.resolve))); + }; + + const messagePropagationRecursive = async ( + wrapper: (callback: any) => any = (callback) => callback(), + ) => { + while (messagePropagationBuffer.size) { + await messagePropagation(wrapper); + } + }; + + let asyncModeStatus = false; + const getAsyncModeStatus = () => asyncModeStatus; + + return { + involve: (...dis: DiContainer[]) => { + dis.forEach((di) => { + overrideRequesting({ di, requestListenersByDi }); + + overrideMessaging({ + di, + messageListenersByDi, + messagePropagationBuffer, + getAsyncModeStatus, + }); + }); + }, + + messagePropagation, + + messagePropagationRecursive, + + setAsync: (value) => { + asyncModeStatus = value; + }, + }; +}; diff --git a/packages/technical-features/messaging/message-bridge-fake/tsconfig.json b/packages/technical-features/messaging/message-bridge-fake/tsconfig.json new file mode 100644 index 0000000000..ec29a8f75f --- /dev/null +++ b/packages/technical-features/messaging/message-bridge-fake/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@k8slens/typescript/config/base.json", + "include": ["**/*.ts", "**/*.tsx"] +} diff --git a/packages/technical-features/messaging/message-bridge-fake/webpack.config.js b/packages/technical-features/messaging/message-bridge-fake/webpack.config.js new file mode 100644 index 0000000000..3183f30179 --- /dev/null +++ b/packages/technical-features/messaging/message-bridge-fake/webpack.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/webpack").configForNode; diff --git a/packages/utility-features/run-many/jest.config.js b/packages/utility-features/run-many/jest.config.js index 23be80353b..05dbeacf60 100644 --- a/packages/utility-features/run-many/jest.config.js +++ b/packages/utility-features/run-many/jest.config.js @@ -1,2 +1,3 @@ -module.exports = - require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact; +const config = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact; + +module.exports = { ...config, coverageThreshold: undefined }; diff --git a/packages/utility-features/run-many/package.json b/packages/utility-features/run-many/package.json index 52410ecb5b..1fa8af2ba1 100644 --- a/packages/utility-features/run-many/package.json +++ b/packages/utility-features/run-many/package.json @@ -22,12 +22,18 @@ "scripts": { "build": "webpack", "dev": "webpack --mode=development --watch", - "test": "jest --coverage --runInBand" + "test:unit": "jest --coverage --runInBand" }, "peerDependencies": { "@k8slens/test-utils": "^1.0.0-alpha.1", "@k8slens/utilities": "^1.0.0-alpha.1", - "@ogre-tools/fp": "^15.1.1", - "@ogre-tools/injectable": "^15.1.1" + "@ogre-tools/fp": "^15.1.2", + "@ogre-tools/injectable": "^15.1.2", + "type-fest": "^2.19.0", + "typed-emitter": "^1.4.0", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@types/uuid": "^9.0.1" } } diff --git a/packages/utility-features/startable-stoppable/.eslintrc.json b/packages/utility-features/startable-stoppable/.eslintrc.json new file mode 100644 index 0000000000..b15115cb69 --- /dev/null +++ b/packages/utility-features/startable-stoppable/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "@k8slens/eslint-config/eslint", + "parserOptions": { + "project": "./tsconfig.json" + } +} diff --git a/packages/utility-features/startable-stoppable/.prettierrc b/packages/utility-features/startable-stoppable/.prettierrc new file mode 100644 index 0000000000..edd47b479e --- /dev/null +++ b/packages/utility-features/startable-stoppable/.prettierrc @@ -0,0 +1 @@ +"@k8slens/eslint-config/prettier" diff --git a/packages/utility-features/startable-stoppable/index.ts b/packages/utility-features/startable-stoppable/index.ts new file mode 100644 index 0000000000..7f5c07cf47 --- /dev/null +++ b/packages/utility-features/startable-stoppable/index.ts @@ -0,0 +1,3 @@ +export type { StartableStoppable, Starter, Stopper } from "./src/get-startable-stoppable"; + +export { getStartableStoppable } from "./src/get-startable-stoppable"; diff --git a/packages/utility-features/startable-stoppable/jest.config.js b/packages/utility-features/startable-stoppable/jest.config.js new file mode 100644 index 0000000000..38d54ab7b6 --- /dev/null +++ b/packages/utility-features/startable-stoppable/jest.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact; diff --git a/packages/utility-features/startable-stoppable/package.json b/packages/utility-features/startable-stoppable/package.json new file mode 100644 index 0000000000..940e1e91b2 --- /dev/null +++ b/packages/utility-features/startable-stoppable/package.json @@ -0,0 +1,38 @@ +{ + "name": "@k8slens/startable-stoppable", + "private": false, + "version": "1.0.0-alpha.1", + "description": "TBD", + "type": "commonjs", + + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + + "files": [ + "build" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/lensapp/lens.git" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "author": { + "name": "OpenLens Authors", + "email": "info@k8slens.dev" + }, + "license": "MIT", + "homepage": "https://github.com/lensapp/lens", + "scripts": { + "build": "webpack", + "dev": "webpack --mode=development --watch", + "test:unit": "jest --coverage --runInBand", + "lint": "lens-lint", + "lint:fix": "lens-lint --fix" + }, + "devDependencies": { + "@k8slens/eslint-config": "^6.5.0-alpha.1" + } +} diff --git a/packages/core/src/common/utils/get-startable-stoppable.test.ts b/packages/utility-features/startable-stoppable/src/get-startable-stoppable.test.ts similarity index 84% rename from packages/core/src/common/utils/get-startable-stoppable.test.ts rename to packages/utility-features/startable-stoppable/src/get-startable-stoppable.test.ts index dc8b24dd43..2ec64eeb95 100644 --- a/packages/core/src/common/utils/get-startable-stoppable.test.ts +++ b/packages/utility-features/startable-stoppable/src/get-startable-stoppable.test.ts @@ -1,7 +1,3 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ import type { StartableStoppable } from "./get-startable-stoppable"; import { getStartableStoppable } from "./get-startable-stoppable"; @@ -25,7 +21,7 @@ describe("getStartableStoppable", () => { }); it("when stopping before ever starting, throws", () => { - expect(() => actual.stop()).toThrow("Tried to stop \"some-id\", but it is already stopped."); + expect(() => actual.stop()).toThrow('Tried to stop "some-id", but it is already stopped.'); }); it("is not started", () => { @@ -45,6 +41,10 @@ describe("getStartableStoppable", () => { expect(actual.started).toBe(true); }); + it("when started again, throws", () => { + expect(() => actual.start()).toThrow('Tried to start "some-id", but it is already started.'); + }); + describe("when stopped", () => { beforeEach(() => { actual.stop(); diff --git a/packages/core/src/common/utils/get-startable-stoppable.ts b/packages/utility-features/startable-stoppable/src/get-startable-stoppable.ts similarity index 86% rename from packages/core/src/common/utils/get-startable-stoppable.ts rename to packages/utility-features/startable-stoppable/src/get-startable-stoppable.ts index 05d8b4d9af..85f0a6336d 100644 --- a/packages/core/src/common/utils/get-startable-stoppable.ts +++ b/packages/utility-features/startable-stoppable/src/get-startable-stoppable.ts @@ -1,8 +1,3 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - export type Stopper = () => void; export type Starter = () => Stopper; diff --git a/packages/utility-features/startable-stoppable/tsconfig.json b/packages/utility-features/startable-stoppable/tsconfig.json new file mode 100644 index 0000000000..1819203dc1 --- /dev/null +++ b/packages/utility-features/startable-stoppable/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@k8slens/typescript/config/base.json", + "include": ["**/*.ts"] +} diff --git a/packages/utility-features/startable-stoppable/webpack.config.js b/packages/utility-features/startable-stoppable/webpack.config.js new file mode 100644 index 0000000000..3183f30179 --- /dev/null +++ b/packages/utility-features/startable-stoppable/webpack.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/webpack").configForNode; diff --git a/packages/utility-features/test-utils/index.ts b/packages/utility-features/test-utils/index.ts index ed5afc6535..aca8fd5f9a 100644 --- a/packages/utility-features/test-utils/index.ts +++ b/packages/utility-features/test-utils/index.ts @@ -2,3 +2,5 @@ export * from "./src/flush-promises"; export * from "./src/get-global-override-for-function"; export * from "./src/get-global-override"; export * from "./src/get-promise-status"; +export * from "./src/render-for"; +export * from "./src/run-with-thrown-mobx-reactions"; diff --git a/packages/utility-features/test-utils/package.json b/packages/utility-features/test-utils/package.json index 3adf142d0c..4336c0682b 100644 --- a/packages/utility-features/test-utils/package.json +++ b/packages/utility-features/test-utils/package.json @@ -21,7 +21,16 @@ "homepage": "https://github.com/lensapp/lens", "scripts": { "build": "webpack", - "dev": "webpack --mode=development --watch", - "test": "jest --coverage --runInBand" + "dev": "webpack --mode=development --watch" + }, + "peerDependencies": { + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-react": "^15.1.2", + "@testing-library/react": "^12.1.5", + "lodash": "^4.17.21", + "react": "^17.0.2" + }, + "devDependencies": { + "@types/lodash": "^4.14.191" } } diff --git a/packages/utility-features/test-utils/src/flush-promises.ts b/packages/utility-features/test-utils/src/flush-promises.ts index c2fdeff99e..fa3271654a 100644 --- a/packages/utility-features/test-utils/src/flush-promises.ts +++ b/packages/utility-features/test-utils/src/flush-promises.ts @@ -2,6 +2,9 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { setImmediate } from "timers"; +import { setImmediate, setTimeout } from "timers/promises"; -export const flushPromises = () => new Promise(setImmediate); +export const flushPromises = async () => { + await setImmediate(); + await setTimeout(5); +}; diff --git a/packages/utility-features/test-utils/src/render-for.tsx b/packages/utility-features/test-utils/src/render-for.tsx new file mode 100644 index 0000000000..2508d70d69 --- /dev/null +++ b/packages/utility-features/test-utils/src/render-for.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import type { RenderResult } from "@testing-library/react"; +import { render as testingLibraryRender } from "@testing-library/react"; +import type { DiContainer } from "@ogre-tools/injectable"; +import { DiContextProvider } from "@ogre-tools/injectable-react"; + +export type DiRender = (ui: React.ReactElement) => RenderResult; + +type DiRenderFor = (di: DiContainer) => DiRender; + +export const renderFor: DiRenderFor = (di) => (ui) => { + const result = testingLibraryRender( + {ui} + ); + + return { + ...result, + + rerender: (ui: React.ReactElement) => + result.rerender( + {ui} + ), + }; +}; diff --git a/packages/utility-features/test-utils/src/run-with-thrown-mobx-reactions.ts b/packages/utility-features/test-utils/src/run-with-thrown-mobx-reactions.ts new file mode 100644 index 0000000000..cc3f7ada23 --- /dev/null +++ b/packages/utility-features/test-utils/src/run-with-thrown-mobx-reactions.ts @@ -0,0 +1,38 @@ +import { noop } from "lodash/fp"; +import { _resetGlobalState, configure } from "mobx"; + +export const runWithThrownMobxReactions = (callback: () => void) => { + const originalConsoleWarn = console.warn; + + console.warn = noop; + + configure({ + disableErrorBoundaries: true, + }); + + console.warn = originalConsoleWarn; + + let error: any; + + try { + callback(); + } catch (e) { + error = e; + } finally { + configure({ + disableErrorBoundaries: false, + }); + + // This is because when disableErrorBoundaries is true, MobX doesn't recover from the thrown + // errors, and its global state starts bleeding between tests making. + _resetGlobalState(); + + if (!error) { + throw new Error( + "Tried to run with thrown MobX reactions but nothing was thrown" + ); + } else { + throw error; + } + } +}; diff --git a/packages/utility-features/utilities/jest.config.js b/packages/utility-features/utilities/jest.config.js index 23be80353b..05dbeacf60 100644 --- a/packages/utility-features/utilities/jest.config.js +++ b/packages/utility-features/utilities/jest.config.js @@ -1,2 +1,3 @@ -module.exports = - require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact; +const config = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact; + +module.exports = { ...config, coverageThreshold: undefined }; diff --git a/packages/utility-features/utilities/package.json b/packages/utility-features/utilities/package.json index ba22c430aa..777907d851 100644 --- a/packages/utility-features/utilities/package.json +++ b/packages/utility-features/utilities/package.json @@ -22,10 +22,32 @@ "scripts": { "build": "webpack", "dev": "webpack --mode=development --watch", - "test": "jest --coverage --runInBand" + "test:unit": "jest --coverage --runInBand" }, "peerDependencies": { + "@astronautlabs/jsonpath": "^1.1.0", + "crypto-js": "^4.1.1", + "lodash": "^4.17.21", "mobx": "^6.8.0", - "type-fest": "^2.19.0" + "moment": "^2.29.4", + "p-limit": "^3.1.0", + "path-to-regexp": "^6.2.1", + "react": "^17.0.2", + "react-router": "^5.3.4", + "readable-stream": "^3.6.2", + "semver": "^7.3.8", + "tar": "^6.1.13", + "type-fest": "^2.19.0", + "typed-regex": "^0.0.8" + }, + "devDependencies": { + "@types/crypto-js": "^4.1.1", + "@types/lodash": "^4.14.191", + "@types/react": "^17.0.2", + "@types/react-router": "^5.1.20", + "@types/readable-stream": "^2.3.15", + "@types/semver": "^7.3.13", + "@types/tar": "^6.1.4", + "type-fest": "^2.14.0" } } diff --git a/packages/utility-features/utilities/src/abort-controller.ts b/packages/utility-features/utilities/src/abort-controller.ts index b062fce487..784b495a9e 100644 --- a/packages/utility-features/utilities/src/abort-controller.ts +++ b/packages/utility-features/utilities/src/abort-controller.ts @@ -3,8 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import AbortController from "abort-controller"; - /** * This is like an `AbortController` but will also abort if the parent aborts, * but won't make the parent abort if this aborts (single direction) diff --git a/packages/utility-features/utilities/src/delay.ts b/packages/utility-features/utilities/src/delay.ts index 96171f6535..d86395026b 100644 --- a/packages/utility-features/utilities/src/delay.ts +++ b/packages/utility-features/utilities/src/delay.ts @@ -3,8 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type AbortController from "abort-controller"; - /** * Return a promise that will be resolved after at least `timeout` ms have * passed. If `failFast` is provided then the promise is also resolved if it has diff --git a/packages/utility-features/utilities/src/reject-promise.ts b/packages/utility-features/utilities/src/reject-promise.ts index 8212bacd3f..0211586783 100644 --- a/packages/utility-features/utilities/src/reject-promise.ts +++ b/packages/utility-features/utilities/src/reject-promise.ts @@ -3,8 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { AbortSignal } from "abort-controller"; - /** * Creates a new promise that will be rejected when the signal rejects. * diff --git a/packages/utility-features/utilities/src/splitArray.test.ts b/packages/utility-features/utilities/src/splitArray.test.ts index 038d4731d8..9a1a2aac59 100644 --- a/packages/utility-features/utilities/src/splitArray.test.ts +++ b/packages/utility-features/utilities/src/splitArray.test.ts @@ -3,62 +3,62 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { bifurcateArray, splitArray } from "../splitArray"; +import { array } from "./array"; describe("split array on element tests", () => { it("empty array", () => { - expect(splitArray([], 10)).toStrictEqual([[], [], false]); + expect(array.split([], 10)).toStrictEqual([[], [], false]); }); it("one element, not in array", () => { - expect(splitArray([1], 10)).toStrictEqual([[1], [], false]); + expect(array.split([1], 10)).toStrictEqual([[1], [], false]); }); it("ten elements, not in array", () => { - expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [], false]); + expect(array.split([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [], false]); }); it("one elements, in array", () => { - expect(splitArray([1], 1)).toStrictEqual([[], [], true]); + expect(array.split([1], 1)).toStrictEqual([[], [], true]); }); it("ten elements, in front array", () => { - expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0)).toStrictEqual([[], [1, 2, 3, 4, 5, 6, 7, 8, 9], true]); + expect(array.split([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0)).toStrictEqual([[], [1, 2, 3, 4, 5, 6, 7, 8, 9], true]); }); it("ten elements, in middle array", () => { - expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)).toStrictEqual([[0, 1, 2, 3], [5, 6, 7, 8, 9], true]); + expect(array.split([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)).toStrictEqual([[0, 1, 2, 3], [5, 6, 7, 8, 9], true]); }); it("ten elements, in end array", () => { - expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]); + expect(array.split([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]); }); }); describe("bifurcateArray", () => { it("should return tuple of empty arrays from empty array", () => { - const [left, right] = bifurcateArray([], () => true); + const [left, right] = array.bifurcate([], () => true); expect(left).toStrictEqual([]); expect(right).toStrictEqual([]); }); it("should return all true condition returning items in the right array", () => { - const [left, right] = bifurcateArray([1, 2, 3], () => true); + const [left, right] = array.bifurcate([1, 2, 3], () => true); expect(left).toStrictEqual([]); expect(right).toStrictEqual([1, 2, 3]); }); it("should return all false condition returning items in the right array", () => { - const [left, right] = bifurcateArray([1, 2, 3], () => false); + const [left, right] = array.bifurcate([1, 2, 3], () => false); expect(left).toStrictEqual([1, 2, 3]); expect(right).toStrictEqual([]); }); it("should split array as specified", () => { - const [left, right] = bifurcateArray([1, 2, 3], (i) => Boolean(i % 2)); + const [left, right] = array.bifurcate([1, 2, 3], (i) => Boolean(i % 2)); expect(left).toStrictEqual([2]); expect(right).toStrictEqual([1, 3]); diff --git a/packages/utility-features/utilities/src/union-env-path.test.ts b/packages/utility-features/utilities/src/union-env-path.test.ts index ff8ca916d2..10afb9d1d6 100644 --- a/packages/utility-features/utilities/src/union-env-path.test.ts +++ b/packages/utility-features/utilities/src/union-env-path.test.ts @@ -4,7 +4,7 @@ */ import path from "path"; -import { unionPATHs } from "../union-env-path"; +import { unionPATHs } from "./union-env-path"; describe("unionPATHs", () => { it("return the same path if given only one with no double delimiters", () => {