From a61a455fad8f8325ede54b12ec61619addeec31e Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 24 May 2022 09:28:17 -0700 Subject: [PATCH 01/43] Fix Catalog displaying wrong number of items per category (#5427) Signed-off-by: Sebastian Malton --- .../__tests__/catalog-entity-store.test.ts | 163 ++++++++++++++++++ .../catalog-entity.store.tsx | 9 +- 2 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 src/renderer/components/+catalog/__tests__/catalog-entity-store.test.ts diff --git a/src/renderer/components/+catalog/__tests__/catalog-entity-store.test.ts b/src/renderer/components/+catalog/__tests__/catalog-entity-store.test.ts new file mode 100644 index 0000000000..6794fed899 --- /dev/null +++ b/src/renderer/components/+catalog/__tests__/catalog-entity-store.test.ts @@ -0,0 +1,163 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { CatalogCategoryMetadata, CatalogCategorySpec } from "../../../../common/catalog"; +import { CatalogEntity, categoryVersion } from "../../../../common/catalog"; +import { CatalogCategory } from "../../../api/catalog-entity"; +import { noop } from "../../../utils"; +import type { CatalogEntityStore } from "../catalog-entity-store/catalog-entity.store"; +import { catalogEntityStore } from "../catalog-entity-store/catalog-entity.store"; + +class TestEntityOne extends CatalogEntity { + public static readonly apiVersion: string = "entity.k8slens.dev/v1alpha1"; + public static readonly kind: string = "TestEntityOne"; + + public readonly apiVersion = TestEntityOne.apiVersion; + public readonly kind = TestEntityOne.kind; +} + +class TestEntityTwo extends CatalogEntity { + public static readonly apiVersion: string = "entity.k8slens.dev/v1alpha1"; + public static readonly kind: string = "TestEntityTwo"; + + public readonly apiVersion = TestEntityTwo.apiVersion; + public readonly kind = TestEntityTwo.kind; +} + +class TestCategoryOne extends CatalogCategory { + apiVersion = "catalog.k8slens.dev/v1alpha1"; + kind = "CatalogCategory"; + metadata: CatalogCategoryMetadata = { + icon: "dash", + name: "test-one", + }; + spec: CatalogCategorySpec = { + group: "entity.k8slens.dev", + versions: [ + categoryVersion("v1alpha1", TestEntityOne), + ], + names: { + kind: "KubernetesCluster", + }, + }; +} + +class TestCategoryTwo extends CatalogCategory { + apiVersion = "catalog.k8slens.dev/v1alpha1"; + kind = "CatalogCategory"; + metadata: CatalogCategoryMetadata = { + icon: "dash", + name: "test-two", + }; + spec: CatalogCategorySpec = { + group: "entity.k8slens.dev", + versions: [ + categoryVersion("v1alpha1", TestEntityTwo), + ], + names: { + kind: "KubernetesCluster", + }, + }; +} + +describe("CatalogEntityStore", () => { + describe("getTotalCount", () => { + let store: CatalogEntityStore; + let testCategoryOne: TestCategoryOne; + let testCategoryTwo: TestCategoryTwo; + + beforeEach(() => { + const entityItems = [ + new TestEntityOne({ + metadata: { + labels: {}, + name: "my-test-one", + uid: "1", + }, + spec: {}, + status: { + phase: "unknown", + }, + }), + new TestEntityOne({ + metadata: { + labels: {}, + name: "my-test-two", + uid: "2", + }, + spec: {}, + status: { + phase: "unknown", + }, + }), + new TestEntityTwo({ + metadata: { + labels: {}, + name: "my-test-three", + uid: "3", + }, + spec: {}, + status: { + phase: "unknown", + }, + }), + new TestEntityTwo({ + metadata: { + labels: {}, + name: "my-test-four", + uid: "4", + }, + spec: {}, + status: { + phase: "unknown", + }, + }), + new TestEntityTwo({ + metadata: { + labels: {}, + name: "my-test-five", + uid: "5", + }, + spec: {}, + status: { + phase: "unknown", + }, + }), + ]; + + testCategoryOne = new TestCategoryOne(); + testCategoryTwo = new TestCategoryTwo(); + store = catalogEntityStore({ + catalogRegistry: { + items: [ + testCategoryOne, + testCategoryTwo, + ], + }, + entityRegistry: { + onRun: noop, + filteredItems: entityItems, + getItemsForCategory: (category: CatalogCategory): T[] => { + return entityItems.filter(item => category.spec.versions.some(version => item instanceof version.entityClass)) as T[]; + }, + }, + }); + }); + + it("given no active category, returns count of all kinds", () => { + expect(store.getTotalCount()).toBe(5); + }); + + it("given active category is TestCategoryOne, only returns count for those declared kinds", () => { + store.activeCategory.set(testCategoryOne); + expect(store.getTotalCount()).toBe(2); + }); + + it("given active category is TestCategoryTwo, only returns count for those declared kinds", () => { + store.activeCategory.set(testCategoryTwo); + expect(store.getTotalCount()).toBe(3); + }); + }); +}); diff --git a/src/renderer/components/+catalog/catalog-entity-store/catalog-entity.store.tsx b/src/renderer/components/+catalog/catalog-entity-store/catalog-entity.store.tsx index f0562fd736..3209428cba 100644 --- a/src/renderer/components/+catalog/catalog-entity-store/catalog-entity.store.tsx +++ b/src/renderer/components/+catalog/catalog-entity-store/catalog-entity.store.tsx @@ -12,9 +12,12 @@ import type { Disposer } from "../../../../common/utils"; import { disposer } from "../../../../common/utils"; import type { ItemListStore } from "../../item-object-list"; +type EntityRegistry = Pick; +type CatalogRegistry = Pick; + interface Dependencies { - entityRegistry: CatalogEntityRegistry; - catalogRegistry: CatalogCategoryRegistry; + entityRegistry: EntityRegistry; + catalogRegistry: CatalogRegistry; } export type CatalogEntityStore = ItemListStore & { @@ -71,7 +74,7 @@ export function catalogEntityStore({ ), onRun: entity => entityRegistry.onRun(entity), failedLoading: false, - getTotalCount: () => entityRegistry.filteredItems.length, + getTotalCount: () => entities.get().length, isLoaded: true, isSelected: (item) => item.getId() === selectedItemId.get(), isSelectedAll: () => false, From d09816aacf61bef6b0795d60a5ca76ed034d27dd Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 25 May 2022 06:00:37 -0700 Subject: [PATCH 02/43] Cherry Pick bug fixes from v5.5.0-beta.2 (#5429) --- .../__tests__/{nodes.test.ts => node.test.ts} | 56 ++++++++++++++++++- src/common/k8s-api/endpoints/node.api.ts | 35 +++++++++--- src/main/helm/exec.ts | 2 +- src/main/kubectl/kubectl.ts | 2 +- src/renderer/components/+nodes/store.ts | 4 +- 5 files changed, 86 insertions(+), 13 deletions(-) rename src/common/k8s-api/__tests__/{nodes.test.ts => node.test.ts} (72%) diff --git a/src/common/k8s-api/__tests__/nodes.test.ts b/src/common/k8s-api/__tests__/node.test.ts similarity index 72% rename from src/common/k8s-api/__tests__/nodes.test.ts rename to src/common/k8s-api/__tests__/node.test.ts index e2527cd9c6..53ffc59d79 100644 --- a/src/common/k8s-api/__tests__/nodes.test.ts +++ b/src/common/k8s-api/__tests__/node.test.ts @@ -8,7 +8,61 @@ import { Node } from "../endpoints"; * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -describe("Nodes tests", () => { +describe("Node tests", () => { + describe("isMasterNode()", () => { + it("given a master node labelled before kubernetes 1.20, should return true", () => { + const node = new Node({ + apiVersion: "foo", + kind: "Node", + metadata: { + name: "bar", + resourceVersion: "1", + uid: "bat", + labels: { + "node-role.kubernetes.io/master": "NoSchedule", + }, + selfLink: "/api/v1/nodes/bar", + }, + }); + + expect(node.isMasterNode()).toBe(true); + }); + + it("given a master node labelled after kubernetes 1.20, should return true", () => { + const node = new Node({ + apiVersion: "foo", + kind: "Node", + metadata: { + name: "bar", + resourceVersion: "1", + uid: "bat", + labels: { + "node-role.kubernetes.io/control-plane": "NoSchedule", + }, + selfLink: "/api/v1/nodes/bar", + }, + }); + + expect(node.isMasterNode()).toBe(true); + }); + + it("given a non master node, should return false", () => { + const node = new Node({ + apiVersion: "foo", + kind: "Node", + metadata: { + name: "bar", + resourceVersion: "1", + uid: "bat", + labels: {}, + selfLink: "/api/v1/nodes/bar", + }, + }); + + expect(node.isMasterNode()).toBe(false); + }); + }); + describe("getRoleLabels()", () => { it("should return empty string if labels is not present", () => { const node = new Node({ diff --git a/src/common/k8s-api/endpoints/node.api.ts b/src/common/k8s-api/endpoints/node.api.ts index 3aab828621..a51db59fa7 100644 --- a/src/common/k8s-api/endpoints/node.api.ts +++ b/src/common/k8s-api/endpoints/node.api.ts @@ -5,7 +5,7 @@ import type { BaseKubeObjectCondition, KubeObjectScope } from "../kube-object"; import { KubeObject } from "../kube-object"; -import { cpuUnitsToNumber, unitsToBytes } from "../../../renderer/utils"; +import { cpuUnitsToNumber, unitsToBytes, isObject } from "../../../renderer/utils"; import type { MetricData } from "./metrics.api"; import { metricsApi } from "./metrics.api"; import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api"; @@ -69,6 +69,17 @@ export interface NodeCondition extends BaseKubeObjectCondition { lastHeartbeatTime?: string; } +/** + * These role label prefixs are the ones that are for master nodes + * + * The `master` label has been deprecated in Kubernetes 1.20, and will be removed in 1.25 so we + * have to also use the newer `control-plane` label + */ +const masterNodeLabels = [ + "master", + "control-plane", +]; + /** * This regex is used in the `getRoleLabels()` method bellow, but placed here * as factoring out regexes is best practice. @@ -189,15 +200,19 @@ export class Node extends KubeObject masterNodeLabels.includes(roleLabel)); + } + + getRoleLabelItems(): string[] { const { labels } = this.metadata; - - if (!labels || typeof labels !== "object") { - return ""; - } - const roleLabels: string[] = []; + if (!isObject(labels)) { + return roleLabels; + } + for (const labelKey of Object.keys(labels)) { const match = nodeRoleLabelKeyMatcher.match(labelKey); @@ -214,7 +229,11 @@ export class Node extends KubeObject { } @computed get masterNodes() { - return this.items.filter(node => node.getRoleLabels().includes("master")); + return this.items.filter(node => node.isMasterNode()); } @computed get workerNodes() { - return this.items.filter(node => !node.getRoleLabels().includes("master")); + return this.items.filter(node => !node.isMasterNode()); } getWarningsCount(): number { From b616eee6002ac8acb4de3240932a5e1b002a74ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 May 2022 10:57:17 -0400 Subject: [PATCH 03/43] Bump @typescript-eslint/eslint-plugin from 5.23.0 to 5.26.0 (#5437) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 86 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index a2b4408cfe..b31a5a8f44 100644 --- a/package.json +++ b/package.json @@ -340,7 +340,7 @@ "@types/webpack-dev-server": "^4.7.2", "@types/webpack-env": "^1.16.4", "@types/webpack-node-externals": "^2.5.3", - "@typescript-eslint/eslint-plugin": "^5.21.0", + "@typescript-eslint/eslint-plugin": "^5.26.0", "@typescript-eslint/parser": "^5.17.0", "ansi_up": "^5.1.0", "chart.js": "^2.9.4", diff --git a/yarn.lock b/yarn.lock index 21beb4bb50..492cff8d19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2196,19 +2196,19 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^5.21.0": - version "5.23.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz#bc4cbcf91fbbcc2e47e534774781b82ae25cc3d8" - integrity sha512-hEcSmG4XodSLiAp1uxv/OQSGsDY6QN3TcRU32gANp+19wGE1QQZLRS8/GV58VRUoXhnkuJ3ZxNQ3T6Z6zM59DA== +"@typescript-eslint/eslint-plugin@^5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.26.0.tgz#c1f98ccba9d345e38992975d3ca56ed6260643c2" + integrity sha512-oGCmo0PqnRZZndr+KwvvAUvD3kNE4AfyoGCwOZpoCncSh4MVD06JTE8XQa2u9u+NX5CsyZMBTEc2C72zx38eYA== dependencies: - "@typescript-eslint/scope-manager" "5.23.0" - "@typescript-eslint/type-utils" "5.23.0" - "@typescript-eslint/utils" "5.23.0" - debug "^4.3.2" + "@typescript-eslint/scope-manager" "5.26.0" + "@typescript-eslint/type-utils" "5.26.0" + "@typescript-eslint/utils" "5.26.0" + debug "^4.3.4" functional-red-black-tree "^1.0.1" - ignore "^5.1.8" + ignore "^5.2.0" regexpp "^3.2.0" - semver "^7.3.5" + semver "^7.3.7" tsutils "^3.21.0" "@typescript-eslint/parser@^5.17.0": @@ -2229,13 +2229,21 @@ "@typescript-eslint/types" "5.23.0" "@typescript-eslint/visitor-keys" "5.23.0" -"@typescript-eslint/type-utils@5.23.0": - version "5.23.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.23.0.tgz#f852252f2fc27620d5bb279d8fed2a13d2e3685e" - integrity sha512-iuI05JsJl/SUnOTXA9f4oI+/4qS/Zcgk+s2ir+lRmXI+80D8GaGwoUqs4p+X+4AxDolPpEpVUdlEH4ADxFy4gw== +"@typescript-eslint/scope-manager@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.26.0.tgz#44209c7f649d1a120f0717e0e82da856e9871339" + integrity sha512-gVzTJUESuTwiju/7NiTb4c5oqod8xt5GhMbExKsCTp6adU3mya6AGJ4Pl9xC7x2DX9UYFsjImC0mA62BCY22Iw== dependencies: - "@typescript-eslint/utils" "5.23.0" - debug "^4.3.2" + "@typescript-eslint/types" "5.26.0" + "@typescript-eslint/visitor-keys" "5.26.0" + +"@typescript-eslint/type-utils@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.26.0.tgz#937dee97702361744a3815c58991acf078230013" + integrity sha512-7ccbUVWGLmcRDSA1+ADkDBl5fP87EJt0fnijsMFTVHXKGduYMgienC/i3QwoVhDADUAPoytgjbZbCOMj4TY55A== + dependencies: + "@typescript-eslint/utils" "5.26.0" + debug "^4.3.4" tsutils "^3.21.0" "@typescript-eslint/types@5.23.0": @@ -2243,6 +2251,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.23.0.tgz#8733de0f58ae0ed318dbdd8f09868cdbf9f9ad09" integrity sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw== +"@typescript-eslint/types@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.26.0.tgz#cb204bb154d3c103d9cc4d225f311b08219469f3" + integrity sha512-8794JZFE1RN4XaExLWLI2oSXsVImNkl79PzTOOWt9h0UHROwJedNOD2IJyfL0NbddFllcktGIO2aOu10avQQyA== + "@typescript-eslint/typescript-estree@5.23.0": version "5.23.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz#dca5f10a0a85226db0796e8ad86addc9aee52065" @@ -2256,15 +2269,28 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/utils@5.23.0": - version "5.23.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.23.0.tgz#4691c3d1b414da2c53d8943310df36ab1c50648a" - integrity sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA== +"@typescript-eslint/typescript-estree@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.26.0.tgz#16cbceedb0011c2ed4f607255f3ee1e6e43b88c3" + integrity sha512-EyGpw6eQDsfD6jIqmXP3rU5oHScZ51tL/cZgFbFBvWuCwrIptl+oueUZzSmLtxFuSOQ9vDcJIs+279gnJkfd1w== + dependencies: + "@typescript-eslint/types" "5.26.0" + "@typescript-eslint/visitor-keys" "5.26.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.26.0.tgz#896b8480eb124096e99c8b240460bb4298afcfb4" + integrity sha512-PJFwcTq2Pt4AMOKfe3zQOdez6InIDOjUJJD3v3LyEtxHGVVRK3Vo7Dd923t/4M9hSH2q2CLvcTdxlLPjcIk3eg== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.23.0" - "@typescript-eslint/types" "5.23.0" - "@typescript-eslint/typescript-estree" "5.23.0" + "@typescript-eslint/scope-manager" "5.26.0" + "@typescript-eslint/types" "5.26.0" + "@typescript-eslint/typescript-estree" "5.26.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" @@ -2276,6 +2302,14 @@ "@typescript-eslint/types" "5.23.0" eslint-visitor-keys "^3.0.0" +"@typescript-eslint/visitor-keys@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.26.0.tgz#7195f756e367f789c0e83035297c45b417b57f57" + integrity sha512-wei+ffqHanYDOQgg/fS6Hcar6wAWv0CUPQ3TZzOWd2BLfgP539rb49bwua8WRAs7R6kOSLn82rfEu2ro6Llt8Q== + dependencies: + "@typescript-eslint/types" "5.26.0" + eslint-visitor-keys "^3.3.0" + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" @@ -4340,7 +4374,7 @@ debug@3.1.0, debug@~3.1.0: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -6339,7 +6373,7 @@ globalthis@^1.0.1: dependencies: define-properties "^1.1.3" -globby@^11.0.1, globby@^11.0.4: +globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -6878,7 +6912,7 @@ ignore-walk@^3.0.1: dependencies: minimatch "^3.0.4" -ignore@^5.1.8, ignore@^5.2.0: +ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== From 58ffb38d74837dc5d7ce31cb986150ea6bfa86f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 May 2022 11:16:48 -0400 Subject: [PATCH 04/43] Bump @pmmmwh/react-refresh-webpack-plugin from 0.5.5 to 0.5.7 (#5436) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b31a5a8f44..0743efbc38 100644 --- a/package.json +++ b/package.json @@ -282,7 +282,7 @@ "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", "@sentry/types": "^6.19.7", "@testing-library/dom": "^7.31.2", "@testing-library/jest-dom": "^5.16.4", diff --git a/yarn.lock b/yarn.lock index 492cff8d19..f2aff02221 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1027,10 +1027,10 @@ resolved "https://registry.yarnpkg.com/@panva/asn1.js/-/asn1.js-1.0.0.tgz#dd55ae7b8129e02049f009408b97c61ccf9032f6" integrity sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw== -"@pmmmwh/react-refresh-webpack-plugin@^0.5.5": - version "0.5.5" - resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.5.tgz#e77aac783bd079f548daa0a7f080ab5b5a9741ca" - integrity sha512-RbG7h6TuP6nFFYKJwbcToA1rjC1FyPg25NR2noAZ0vKI+la01KTSRPkuVPE+U88jXv7javx2JHglUcL1MHcshQ== +"@pmmmwh/react-refresh-webpack-plugin@^0.5.7": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz#58f8217ba70069cc6a73f5d7e05e85b458c150e2" + integrity sha512-bcKCAzF0DV2IIROp9ZHkRJa6O4jy7NlnHdWL3GmcUxYWNjLXkK5kfELELwEfSP5hXPfVL/qOGMAROuMQb9GG8Q== dependencies: ansi-html-community "^0.0.8" common-path-prefix "^3.0.0" From 199cd5766108a03e845d068e85799b470a903a70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 May 2022 15:55:28 -0400 Subject: [PATCH 05/43] Bump immer from 9.0.12 to 9.0.14 (#5377) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0743efbc38..a5d29c0c43 100644 --- a/package.json +++ b/package.json @@ -229,7 +229,7 @@ "handlebars": "^4.7.7", "history": "^4.10.1", "http-proxy": "^1.18.1", - "immer": "^9.0.12", + "immer": "^9.0.14", "joi": "^17.6.0", "js-yaml": "^4.1.0", "jsdom": "^16.7.0", diff --git a/yarn.lock b/yarn.lock index f2aff02221..2a6369b083 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6927,10 +6927,10 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= -immer@^9.0.12: - version "9.0.12" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20" - integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA== +immer@^9.0.14: + version "9.0.14" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.14.tgz#e05b83b63999d26382bb71676c9d827831248a48" + integrity sha512-ubBeqQutOSLIFCUBN03jGeOS6a3DoYlSYwYJTa+gSKEZKU5redJIqkIdZ3JVv/4RZpfcXdAWH5zCNLWPRv2WDw== immutable@^4.0.0: version "4.0.0" From 6e358d752ffaf6ed7e81eb8a19749f4895be5c25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 May 2022 15:58:45 -0400 Subject: [PATCH 06/43] Bump eslint from 8.15.0 to 8.16.0 (#5446) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index a5d29c0c43..4a3335f211 100644 --- a/package.json +++ b/package.json @@ -357,7 +357,7 @@ "electron-notarize": "^0.3.0", "esbuild": "^0.14.38", "esbuild-loader": "^2.18.0", - "eslint": "^8.14.0", + "eslint": "^8.16.0", "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "^2.26.0", "eslint-plugin-react": "^7.29.4", diff --git a/yarn.lock b/yarn.lock index 2a6369b083..751fea7c43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -481,15 +481,15 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== -"@eslint/eslintrc@^1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.3.tgz#fcaa2bcef39e13d6e9e7f6271f4cc7cae1174886" - integrity sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA== +"@eslint/eslintrc@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f" + integrity sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw== dependencies: ajv "^6.12.4" debug "^4.3.2" espree "^9.3.2" - globals "^13.9.0" + globals "^13.15.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" @@ -5413,12 +5413,12 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@^8.14.0: - version "8.15.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.15.0.tgz#fea1d55a7062da48d82600d2e0974c55612a11e9" - integrity sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA== +eslint@^8.16.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.16.0.tgz#6d936e2d524599f2a86c708483b4c372c5d3bbae" + integrity sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA== dependencies: - "@eslint/eslintrc" "^1.2.3" + "@eslint/eslintrc" "^1.3.0" "@humanwhocodes/config-array" "^0.9.2" ajv "^6.10.0" chalk "^4.0.0" @@ -5436,7 +5436,7 @@ eslint@^8.14.0: file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" glob-parent "^6.0.1" - globals "^13.6.0" + globals "^13.15.0" ignore "^5.2.0" import-fresh "^3.0.0" imurmurhash "^0.1.4" @@ -6359,10 +6359,10 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.6.0, globals@^13.9.0: - version "13.14.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.14.0.tgz#daf3ff9b4336527cf56e98330b6f64bea9aff9df" - integrity sha512-ERO68sOYwm5UuLvSJTY7w7NP2c8S4UcXs3X1GBX8cwOr+ShOcDBbCY5mH4zxz0jsYCdJ8ve8Mv9n2YGJMB1aeg== +globals@^13.15.0: + version "13.15.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac" + integrity sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog== dependencies: type-fest "^0.20.2" From 7381533ded1c17956e48f3aec60d3632054f399b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 May 2022 08:43:20 -0400 Subject: [PATCH 07/43] Bump @types/node from 14.18.17 to 14.18.18 (#5454) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 4a3335f211..aaa99f1df4 100644 --- a/package.json +++ b/package.json @@ -310,7 +310,7 @@ "@types/md5-file": "^4.0.2", "@types/mini-css-extract-plugin": "^2.4.0", "@types/mock-fs": "^4.13.1", - "@types/node": "14.18.17", + "@types/node": "14.18.18", "@types/node-fetch": "^2.6.1", "@types/npm": "^2.0.32", "@types/proper-lockfile": "^4.1.2", diff --git a/yarn.lock b/yarn.lock index 751fea7c43..27f02e9df2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1734,21 +1734,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.31.tgz#a5bb84ecfa27eec5e1c802c6bbf8139bdb163a5d" integrity sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q== -"@types/node@14.18.17": - version "14.18.17" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.17.tgz#37d3c01043fd09f3f17ffa8c17062bbb580f9558" - integrity sha512-oajWz4kOajqpKJMPgnCvBajPq8QAvl2xIWoFjlAJPKGu6n7pjov5SxGE45a+0RxHDoo4ycOMoZw1SCOWtDERbw== +"@types/node@14.18.18", "@types/node@^14.6.2": + version "14.18.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.18.tgz#5c9503030df484ccffcbb935ea9a9e1d6fad1a20" + integrity sha512-B9EoJFjhqcQ9OmQrNorItO+OwEOORNn3S31WuiHvZY/dm9ajkB7AKD/8toessEtHHNL+58jofbq7hMMY9v4yig== "@types/node@^10.12.0": version "10.17.60" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== -"@types/node@^14.6.2": - version "14.18.16" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.16.tgz#878f670ba3f00482bf859b6550b6010610fc54b5" - integrity sha512-X3bUMdK/VmvrWdoTkz+VCn6nwKwrKCFTHtqwBIaQJNx4RUIBBUFXM00bqPz/DsDd+Icjmzm6/tyYZzeGVqb6/Q== - "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" From f690898dc8a1820551de89f9a3159ed165733e67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 May 2022 08:46:24 -0400 Subject: [PATCH 08/43] Bump react-router-dom from 5.3.1 to 5.3.3 (#5452) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index aaa99f1df4..b6f82ea80e 100644 --- a/package.json +++ b/package.json @@ -387,7 +387,7 @@ "react-beautiful-dnd": "^13.1.0", "react-refresh": "^0.12.0", "react-refresh-typescript": "^2.0.4", - "react-router-dom": "^5.3.1", + "react-router-dom": "^5.3.3", "react-select": "^5.3.2", "react-select-event": "^5.5.0", "react-table": "^7.7.0", diff --git a/yarn.lock b/yarn.lock index 27f02e9df2..291629c515 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10943,23 +10943,23 @@ react-refresh@^0.12.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.12.0.tgz#28ac0a2c30ef2bb3433d5fd0621e69a6d774c3a4" integrity sha512-suLIhrU2IHKL5JEKR/fAwJv7bbeq4kJ+pJopf77jHwuR+HmJS/HbrPIGsTBUVfw7tXPOmYv7UJ7PCaN49e8x4A== -react-router-dom@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.1.tgz#0151baf2365c5fcd8493f6ec9b9b31f34d0f8ae1" - integrity sha512-f0pj/gMAbv9e8gahTmCEY20oFhxhrmHwYeIwH5EO5xu0qme+wXtsdB8YfUOAZzUz4VaXmb58m3ceiLtjMhqYmQ== +react-router-dom@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.3.tgz#8779fc28e6691d07afcaf98406d3812fe6f11199" + integrity sha512-Ov0tGPMBgqmbu5CDmN++tv2HQ9HlWDuWIIqn4b88gjlAN5IHI+4ZUZRcpz9Hl0azFIwihbLDYw1OiHGRo7ZIng== dependencies: "@babel/runtime" "^7.12.13" history "^4.9.0" loose-envify "^1.3.1" prop-types "^15.6.2" - react-router "5.3.1" + react-router "5.3.3" tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@5.3.1, react-router@^5.2.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.1.tgz#b13e84a016c79b9e80dde123ca4112c4f117e3cf" - integrity sha512-v+zwjqb7bakqgF+wMVKlAPTca/cEmPOvQ9zt7gpSNyPXau1+0qvuYZ5BWzzNDP1y6s15zDwgb9rPN63+SIniRQ== +react-router@5.3.3, react-router@^5.2.0: + version "5.3.3" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.3.tgz#8e3841f4089e728cf82a429d92cdcaa5e4a3a288" + integrity sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w== dependencies: "@babel/runtime" "^7.12.13" history "^4.9.0" From 34e62c71c04c3cca5504d0e32546d61d951ba067 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 May 2022 08:46:41 -0400 Subject: [PATCH 09/43] Bump fork-ts-checker-webpack-plugin from 6.5.0 to 6.5.2 (#5372) Bumps [fork-ts-checker-webpack-plugin](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin) from 6.5.0 to 6.5.2. - [Release notes](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/releases) - [Changelog](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/compare/v6.5.0...v6.5.2) --- updated-dependencies: - dependency-name: fork-ts-checker-webpack-plugin dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 127 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 121 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index b6f82ea80e..5b1ae54d4a 100644 --- a/package.json +++ b/package.json @@ -364,7 +364,7 @@ "eslint-plugin-react-hooks": "^4.5.0", "eslint-plugin-unused-imports": "^2.0.0", "flex.box": "^3.4.4", - "fork-ts-checker-webpack-plugin": "^6.5.0", + "fork-ts-checker-webpack-plugin": "^6.5.2", "gunzip-maybe": "^1.4.2", "html-webpack-plugin": "^5.5.0", "identity-obj-proxy": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 291629c515..04455e65b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31,7 +31,14 @@ resolved "https://registry.yarnpkg.com/@async-fn/jest/-/jest-1.6.0.tgz#48980e6f07c4d0d72b468b8b57a1b3be8473a746" integrity sha512-Jm4kf9qQSzcOZIyiI13C4EM4euSLORA8O4JTOWwy7SwaUr8lhVOn0nVbNLx9jnP35JTYeLsLZHfAyZLhYDIl2g== -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.8.3": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" + integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA== + dependencies: + "@babel/highlight" "^7.16.0" + +"@babel/code-frame@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== @@ -43,7 +50,29 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.10.tgz#711dc726a492dfc8be8220028b1b92482362baab" integrity sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw== -"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.5": +"@babel/core@^7.1.0", "@babel/core@^7.7.5": + version "7.10.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.2.tgz#bd6786046668a925ac2bd2fd95b579b92a23b36a" + integrity sha512-KQmV9yguEjQsXqyOUGKjS4+3K8/DlOCE2pZcq4augdQmtTy5iv5EHtmMSJ7V4c1BIPjuwtZYqYLCq9Ga+hGBRQ== + dependencies: + "@babel/code-frame" "^7.10.1" + "@babel/generator" "^7.10.2" + "@babel/helper-module-transforms" "^7.10.1" + "@babel/helpers" "^7.10.1" + "@babel/parser" "^7.10.2" + "@babel/template" "^7.10.1" + "@babel/traverse" "^7.10.1" + "@babel/types" "^7.10.2" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/core@^7.12.3": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.10.tgz#74ef0fbf56b7dfc3f198fc2d927f4f03e12f4b05" integrity sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA== @@ -64,6 +93,15 @@ json5 "^2.2.1" semver "^6.3.0" +"@babel/generator@^7.10.2", "@babel/generator@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.2.tgz#33873d6f89b21efe2da63fe554460f3df1c5880d" + integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== + dependencies: + "@babel/types" "^7.18.2" + "@jridgewell/gen-mapping" "^0.3.0" + jsesc "^2.5.1" + "@babel/generator@^7.17.10": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.10.tgz#c281fa35b0c349bbe9d02916f4ae08fc85ed7189" @@ -90,6 +128,11 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-environment-visitor@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz#8a6d2dedb53f6bf248e31b4baf38739ee4a637bd" + integrity sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ== + "@babel/helper-function-name@^7.17.9": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" @@ -112,6 +155,20 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-module-transforms@^7.10.1": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.0.tgz#baf05dec7a5875fb9235bd34ca18bad4e21221cd" + integrity sha512-kclUYSUBIjlvnzN2++K9f2qzYKFgjmnmjwL4zlmU5f8ZtzgWe8s0rUPSTGy2HmK4P8T52MQsS+HTQAgZd3dMEA== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.0" + "@babel/types" "^7.18.0" + "@babel/helper-module-transforms@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz#3943c7f777139e7954a5355c815263741a9c1cbd" @@ -155,6 +212,15 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== +"@babel/helpers@^7.10.1": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.2.tgz#970d74f0deadc3f5a938bfa250738eb4ac889384" + integrity sha512-j+d+u5xT5utcQSzrh9p+PaJX94h++KN+ng9b9WEJq7pkUPAd61FGqhjuUEdfknb3E/uDBb7ruwEeKkIxNJPIrg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.2" + "@babel/types" "^7.18.2" + "@babel/helpers@^7.17.9": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.9.tgz#b2af120821bfbe44f9907b1826e168e819375a1a" @@ -164,6 +230,15 @@ "@babel/traverse" "^7.17.9" "@babel/types" "^7.17.0" +"@babel/highlight@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a" + integrity sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g== + dependencies: + "@babel/helper-validator-identifier" "^7.15.7" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/highlight@^7.16.7": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.9.tgz#61b2ee7f32ea0454612def4fccdae0de232b73e3" @@ -178,6 +253,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.10.tgz#873b16db82a8909e0fbd7f115772f4b739f6ce78" integrity sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ== +"@babel/parser@^7.10.2", "@babel/parser@^7.18.0": + version "7.18.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.3.tgz#39e99c7b0c4c56cef4d1eed8de9f506411c2ebc2" + integrity sha512-rL50YcEuHbbauAFAysNsJA4/f89fGTOBRNs9P81sniKnKAr4xULe5AecolcsKbi88xu0ByWYDj/S1AJ3FSFuSQ== + "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -284,7 +364,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.16.7", "@babel/template@^7.3.3": +"@babel/template@^7.10.1", "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== @@ -309,6 +389,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.10.1", "@babel/traverse@^7.18.0", "@babel/traverse@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.2.tgz#b77a52604b5cc836a9e1e08dca01cba67a12d2e8" + integrity sha512-9eNwoeovJ6KH9zcCNnENY7DMFwTU9JdGCFtqNLfUAqtUHRCOsTOqWoffosP8vKmNYeSBUv3yVJXjfd8ucwOjUA== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.2" + "@babel/helper-environment-visitor" "^7.18.2" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.18.0" + "@babel/types" "^7.18.2" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.17.10", "@babel/types@^7.3.0", "@babel/types@^7.3.3": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.10.tgz#d35d7b4467e439fcf06d195f8100e0fea7fc82c4" @@ -317,6 +413,14 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@babel/types@^7.10.1", "@babel/types@^7.10.2", "@babel/types@^7.18.0", "@babel/types@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.2.tgz#191abfed79ebe6f4242f643a9a5cbaa36b10b091" + integrity sha512-0On6B8A4/+mFUto5WERt3EEuG1NznDirvwca1O8UwXQHVY8g3R7OzYgxXdOfMwLO08UrpUD/2+3Bclyq+/C94Q== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -813,6 +917,15 @@ "@jridgewell/set-array" "^1.0.0" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9" + integrity sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/resolve-uri@^3.0.3": version "3.0.7" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" @@ -1647,9 +1760,9 @@ integrity sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ== "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.11" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" - integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" + integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== "@types/json5@^0.0.29": version "0.0.29" @@ -5967,7 +6080,7 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= -fork-ts-checker-webpack-plugin@^6.5.0: +fork-ts-checker-webpack-plugin@^6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz#4f67183f2f9eb8ba7df7177ce3cf3e75cdafb340" integrity sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA== From c8cf300e9b0ca20e66510cdadd599ed96a3899da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 May 2022 09:21:30 -0400 Subject: [PATCH 10/43] Bump concurrently from 7.1.0 to 7.2.1 (#5464) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 51 ++++++++++++++++++++++----------------------------- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 5b1ae54d4a..610ee7d886 100644 --- a/package.json +++ b/package.json @@ -348,7 +348,7 @@ "cli-progress": "^3.11.0", "color": "^3.2.1", "command-line-args": "^5.2.1", - "concurrently": "^7.1.0", + "concurrently": "^7.2.1", "css-loader": "^6.7.1", "deepdash": "^5.3.9", "dompurify": "^2.3.6", diff --git a/yarn.lock b/yarn.lock index 04455e65b1..817aa4542b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -202,7 +202,7 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-validator-identifier@^7.16.7": +"@babel/helper-validator-identifier@^7.15.7", "@babel/helper-validator-identifier@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== @@ -413,7 +413,7 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" -"@babel/types@^7.10.1", "@babel/types@^7.10.2", "@babel/types@^7.18.0", "@babel/types@^7.18.2": +"@babel/types@^7.10.2", "@babel/types@^7.18.0", "@babel/types@^7.18.2": version "7.18.2" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.2.tgz#191abfed79ebe6f4242f643a9a5cbaa36b10b091" integrity sha512-0On6B8A4/+mFUto5WERt3EEuG1NznDirvwca1O8UwXQHVY8g3R7OzYgxXdOfMwLO08UrpUD/2+3Bclyq+/C94Q== @@ -4070,19 +4070,20 @@ concat-stream@^1.5.0, concat-stream@^1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" -concurrently@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-7.1.0.tgz#477b49b8cfc630bb491f9b02e9ed7fb7bff02942" - integrity sha512-Bz0tMlYKZRUDqJlNiF/OImojMB9ruKUz6GCfmhFnSapXgPe+3xzY4byqoKG9tUZ7L2PGEUjfLPOLfIX3labnmw== +concurrently@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-7.2.1.tgz#88b144060443403060aad46f837dd17451f7e55e" + integrity sha512-7cab/QyqipqghrVr9qZmoWbidu0nHsmxrpNqQ7r/67vfl1DWJElexehQnTH1p+87tDkihaAjM79xTZyBQh7HLw== dependencies: chalk "^4.1.0" date-fns "^2.16.1" lodash "^4.17.21" rxjs "^6.6.3" + shell-quote "^1.7.3" spawn-command "^0.0.2-1" supports-color "^8.1.0" tree-kill "^1.2.2" - yargs "^16.2.0" + yargs "^17.3.1" conf@^7.1.2: version "7.1.2" @@ -6300,7 +6301,7 @@ genfun@^5.0.0: resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA== -gensync@^1.0.0-beta.2: +gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== @@ -8843,7 +8844,7 @@ lodash.without@~4.4.0: resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac" integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw= -lodash@4.x, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: +lodash@4.x, lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -11509,7 +11510,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.18.1, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.9.0: +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.18.1, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.3.2, resolve@^1.9.0: version "1.22.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== @@ -11936,6 +11937,11 @@ shell-env@^3.0.1: execa "^1.0.0" strip-ansi "^5.2.0" +shell-quote@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" + integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== + shelljs@^0.8.5: version "0.8.5" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" @@ -12150,7 +12156,7 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== -source-map@^0.5.6, source-map@^0.5.7: +source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -14019,7 +14025,7 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yargs-parser@20.x, yargs-parser@^20.2.2: +yargs-parser@20.x: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== @@ -14086,23 +14092,10 @@ yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yargs@^17.0.1: - version "17.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.1.tgz#ebe23284207bb75cee7c408c33e722bfb27b5284" - integrity sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g== +yargs@^17.0.1, yargs@^17.3.1: + version "17.5.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" + integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== dependencies: cliui "^7.0.2" escalade "^3.1.1" From 41d4daded7b569f39c768de893f6383d6b7b8b33 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 26 May 2022 06:46:50 -0700 Subject: [PATCH 11/43] Fix crash when using an inline svg Icon (#5450) --- src/renderer/components/icon/icon.tsx | 94 ++++++++++++++++++++++++--- types/mocks.d.ts | 6 +- webpack/renderer.ts | 2 +- 3 files changed, 90 insertions(+), 12 deletions(-) diff --git a/src/renderer/components/icon/icon.tsx b/src/renderer/components/icon/icon.tsx index 60cf06f9a6..084b75d804 100644 --- a/src/renderer/components/icon/icon.tsx +++ b/src/renderer/components/icon/icon.tsx @@ -12,7 +12,57 @@ import type { LocationDescriptor } from "history"; import { cssNames } from "../../utils"; import { withTooltip } from "../tooltip"; import isNumber from "lodash/isNumber"; -import { decode } from "../../../common/utils/base64"; +import Configuration from "./configuration.svg"; +import Crane from "./crane.svg"; +import Group from "./group.svg"; +import Helm from "./helm.svg"; +import Install from "./install.svg"; +import Kube from "./kube.svg"; +import LensLogo from "./lens-logo.svg"; +import License from "./license.svg"; +import LogoLens from "./logo-lens.svg"; +import Logout from "./logout.svg"; +import Nodes from "./nodes.svg"; +import PushOff from "./push_off.svg"; +import PushPin from "./push_pin.svg"; +import Spinner from "./spinner.svg"; +import Ssh from "./ssh.svg"; +import Storage from "./storage.svg"; +import Terminal from "./terminal.svg"; +import User from "./user.svg"; +import Users from "./users.svg"; +import Wheel from "./wheel.svg"; +import Workloads from "./workloads.svg"; + +/** + * Mapping between the local file names and the svgs + * + * Because we only really want a fixed list of bundled icons, this is safer so that consumers of + * `` cannot pass in a `../some/path`. + */ +const localSvgIcons = new Map([ + ["configuration", Configuration], + ["crane", Crane], + ["group", Group], + ["helm", Helm], + ["install", Install], + ["kube", Kube], + ["lens-logo", LensLogo], + ["license", License], + ["logo-lens", LogoLens], + ["logout", Logout], + ["nodes", Nodes], + ["push_off", PushOff], + ["push_pin", PushPin], + ["spinner", Spinner], + ["ssh", Ssh], + ["storage", Storage], + ["terminal", Terminal], + ["user", User], + ["users", Users], + ["wheel", Wheel], + ["workloads", Workloads], +]); export interface BaseIconProps { /** @@ -21,7 +71,28 @@ export interface BaseIconProps { material?: string; /** - * Either an SVG data URL or one of the following strings + * Either an SVG XML or one of the following names + * - configuration + * - crane + * - group + * - helm + * - install + * - kube + * - lens-logo + * - license + * - logo-lens + * - logout + * - nodes + * - push_off + * - push_pin + * - spinner + * - ssh + * - storage + * - terminal + * - user + * - users + * - wheel + * - workloads */ svg?: string; @@ -78,8 +149,8 @@ export interface BaseIconProps { export interface IconProps extends React.HTMLAttributes, BaseIconProps {} export function isSvg(content: string): boolean { - // data-url for raw svg-icon - return String(content).includes("svg+xml"); + // source code of the asset + return String(content).includes(" { @@ -131,13 +202,16 @@ const RawIcon = withTooltip((props: IconProps) => { // render as inline svg-icon if (typeof svg === "string") { - const dataUrlPrefix = "data:image/svg+xml;base64,"; - const svgIconDataUrl = svg.startsWith(dataUrlPrefix) ? svg : require(`./${svg}.svg`); - const svgIconText = typeof svgIconDataUrl == "string" // decode xml from data-url - ? decode(svgIconDataUrl.replace(dataUrlPrefix, "")) - : ""; + const svgIconText = isSvg(svg) + ? svg + : localSvgIcons.get(svg) ?? ""; - iconContent = ; + iconContent = ( + + ); } // render as material-icon diff --git a/types/mocks.d.ts b/types/mocks.d.ts index 17dba848b7..4d3e378937 100644 --- a/types/mocks.d.ts +++ b/types/mocks.d.ts @@ -25,7 +25,11 @@ declare module "*.scss" { // Declare everything what's bundled as webpack's type="asset/resource" // Should be mocked for tests support in jestConfig.moduleNameMapper (currently in "/package.json") -declare module "*.svg"; +declare module "*.svg" { + const content: string; + export = content; +} + declare module "*.jpg"; declare module "*.png"; declare module "*.eot"; diff --git a/webpack/renderer.ts b/webpack/renderer.ts index bb38b284d4..c8c87e856f 100755 --- a/webpack/renderer.ts +++ b/webpack/renderer.ts @@ -132,7 +132,7 @@ export function iconsAndImagesWebpackRules(): webpack.RuleSetRule[] { return [ { test: /\.svg$/, - type: "asset/inline", // data:image/svg+xml;base64,... + type: "asset/source", // exports the source code of the asset, so we get XML }, { test: /\.(jpg|png|ico)$/, From 938d34739fb849e2d6acb5c6692c39645d48a6be Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 27 May 2022 15:16:11 +0300 Subject: [PATCH 12/43] fix: app-crash with multiple usages of monaco-editor component (#5479) how to reproduce: open one pod on monaco editor (ie edit), and then try to open another pod details (or try to edit it as well) --- .../components/monaco-editor/monaco-editor.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/renderer/components/monaco-editor/monaco-editor.tsx b/src/renderer/components/monaco-editor/monaco-editor.tsx index 781ac32ae4..4869cb4f48 100644 --- a/src/renderer/components/monaco-editor/monaco-editor.tsx +++ b/src/renderer/components/monaco-editor/monaco-editor.tsx @@ -16,7 +16,6 @@ import { UserStore } from "../../../common/user-store"; import type { ThemeStore } from "../../themes/store"; import { withInjectables } from "@ogre-tools/injectable-react"; import themeStoreInjectable from "../../themes/store.injectable"; -import logger from "../../../main/logger"; export type MonacoEditorId = string; @@ -66,6 +65,11 @@ class NonInjectedMonacoEditor extends React.Component Date: Fri, 27 May 2022 15:52:57 +0300 Subject: [PATCH 13/43] Remove deprecated nodeSelectorTerms from cluster metrics manifests (#5474) Signed-off-by: Lauri Nevala --- .../metrics-cluster-feature/resources/03-statefulset.yml.hb | 5 ----- .../resources/10-node-exporter-ds.yml.hb | 5 ----- .../resources/14-kube-state-metrics-deployment.yml.hb | 5 ----- 3 files changed, 15 deletions(-) diff --git a/extensions/metrics-cluster-feature/resources/03-statefulset.yml.hb b/extensions/metrics-cluster-feature/resources/03-statefulset.yml.hb index cc177204a3..288cd553b1 100644 --- a/extensions/metrics-cluster-feature/resources/03-statefulset.yml.hb +++ b/extensions/metrics-cluster-feature/resources/03-statefulset.yml.hb @@ -24,11 +24,6 @@ spec: operator: In values: - linux - - matchExpressions: - - key: beta.kubernetes.io/os - operator: In - values: - - linux # <%- if config.node_selector -%> # nodeSelector: # <%- node_selector.to_h.each do |key, value| -%> diff --git a/extensions/metrics-cluster-feature/resources/10-node-exporter-ds.yml.hb b/extensions/metrics-cluster-feature/resources/10-node-exporter-ds.yml.hb index 2ff46d8d0b..c02fb93321 100644 --- a/extensions/metrics-cluster-feature/resources/10-node-exporter-ds.yml.hb +++ b/extensions/metrics-cluster-feature/resources/10-node-exporter-ds.yml.hb @@ -30,11 +30,6 @@ spec: operator: In values: - linux - - matchExpressions: - - key: beta.kubernetes.io/os - operator: In - values: - - linux securityContext: runAsNonRoot: true runAsUser: 65534 diff --git a/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb b/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb index 5eaefe2cf9..0174d5c8f4 100644 --- a/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb +++ b/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb @@ -23,11 +23,6 @@ spec: operator: In values: - linux - - matchExpressions: - - key: beta.kubernetes.io/os - operator: In - values: - - linux serviceAccountName: kube-state-metrics containers: - name: kube-state-metrics From 72ae7173c258dadc244b2fbae4d80d176a59800e Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 30 May 2022 07:58:00 -0700 Subject: [PATCH 14/43] Fix crash in ProjectedVolume component (#5467) --- src/common/k8s-api/endpoints/pod.api.ts | 94 ++++--- .../__snapshots__/projected.test.tsx.snap | 229 ++++++++++++++++++ .../volumes/variants/projected.test.tsx | 197 +++++++++++++++ .../details/volumes/variants/projected.tsx | 16 +- src/renderer/utils/display-mode.ts | 11 + src/renderer/utils/index.ts | 1 + 6 files changed, 504 insertions(+), 44 deletions(-) create mode 100644 src/renderer/components/+workloads-pods/details/volumes/variants/__snapshots__/projected.test.tsx.snap create mode 100644 src/renderer/components/+workloads-pods/details/volumes/variants/projected.test.tsx create mode 100644 src/renderer/utils/display-mode.ts diff --git a/src/common/k8s-api/endpoints/pod.api.ts b/src/common/k8s-api/endpoints/pod.api.ts index 7b0bf55e9e..8e3aa51559 100644 --- a/src/common/k8s-api/endpoints/pod.api.ts +++ b/src/common/k8s-api/endpoints/pod.api.ts @@ -445,46 +445,62 @@ export interface PortworxVolumeSource { readOnly?: boolean; } +export interface KeyToPath { + key: string; + path: string; + mode?: number; +} + +export interface ConfigMapProjection { + name: string; + items?: KeyToPath[]; + optional?: boolean; +} + +export interface ObjectFieldSelector { + fieldPath: string; + apiVersion?: string; +} + +export interface ResourceFieldSelector { + resource: string; + containerName?: string; + divisor?: string; +} + +export interface DownwardAPIVolumeFile { + path: string; + fieldRef?: ObjectFieldSelector; + resourceFieldRef?: ResourceFieldSelector; + mode?: number; +} + +export interface DownwardAPIProjection { + items?: DownwardAPIVolumeFile[]; +} + +export interface SecretProjection { + name: string; + items?: KeyToPath[]; + optional?: boolean; +} + +export interface ServiceAccountTokenProjection { + audience?: string; + expirationSeconds?: number; + path: string; +} + +export interface VolumeProjection { + secret?: SecretProjection; + downwardAPI?: DownwardAPIProjection; + configMap?: ConfigMapProjection; + serviceAccountToken?: ServiceAccountTokenProjection; +} + export interface ProjectedSource { - sources: { - secret?: { - name: string; - items?: { - key: string; - path: string; - mode?: number; - }[]; - }; - downwardAPI?: { - items?: { - path: string; - fieldRef?: { - fieldPath: string; - apiVersion?: string; - }; - resourceFieldRef?: { - resource: string; - containerName?: string; - }; - mode?: number; - }[]; - }; - configMap?: { - name: string; - items?: { - key: string; - path: string; - mode?: number; - }[]; - optional?: boolean; - }; - serviceAccountToken?: { - audience?: string; - expirationSeconds?: number; - path: string; - }; - }[]; - defaultMode: number; + sources?: VolumeProjection[]; + defaultMode?: number; } export interface QuobyteSource { diff --git a/src/renderer/components/+workloads-pods/details/volumes/variants/__snapshots__/projected.test.tsx.snap b/src/renderer/components/+workloads-pods/details/volumes/variants/__snapshots__/projected.test.tsx.snap new file mode 100644 index 0000000000..ef0d3dd261 --- /dev/null +++ b/src/renderer/components/+workloads-pods/details/volumes/variants/__snapshots__/projected.test.tsx.snap @@ -0,0 +1,229 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders 1`] = ` + +
+
+ + Sources + + +
+
+ +`; + +exports[` renders a secret source including overriding mode 1`] = ` + +
+
+ + Default Mount Mode + + + 0o777 + +
+
+ + Sources + + +
+ Secret +
+
+ + Name + + + my-projected-secret + +
+
+ + Items + + +
    +
  • + foo⇢/bar + (0o666) +
  • +
+
+
+
+
+
+ +`; + +exports[` renders a secret source, when provided 1`] = ` + +
+
+ + Default Mount Mode + + + 0o777 + +
+
+ + Sources + + +
+ Secret +
+
+ + Name + + + my-projected-secret + +
+
+ + Items + + +
    +
  • + foo⇢/bar +
  • +
+
+
+
+
+
+ +`; + +exports[` renders default mount mode in octal when provided 1`] = ` + +
+
+ + Default Mount Mode + + + 0o777 + +
+
+ + Sources + + +
+
+ +`; + +exports[` renders when no sources array provided 1`] = ` + +
+
+ + Default Mount Mode + + + 0o777 + +
+
+ + Sources + + +
+
+ +`; diff --git a/src/renderer/components/+workloads-pods/details/volumes/variants/projected.test.tsx b/src/renderer/components/+workloads-pods/details/volumes/variants/projected.test.tsx new file mode 100644 index 0000000000..24405bc1a9 --- /dev/null +++ b/src/renderer/components/+workloads-pods/details/volumes/variants/projected.test.tsx @@ -0,0 +1,197 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { render } from "@testing-library/react"; +import React from "react"; +import type { ProjectedSource } from "../../../../../../common/k8s-api/endpoints"; +import { Pod } from "../../../../../../common/k8s-api/endpoints"; +import { Projected } from "./projected"; + +describe("", () => { + it("renders", () => { + const projectedVolume: ProjectedSource = { }; + const projectedVolumeName = "my-projected"; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + uid: "123", + selfLink: "/api/v1/pod/default/my-pod", + }, + spec: { + volumes: [{ + name: projectedVolumeName, + projected: projectedVolume, + }], + }, + }); + const result = render(( + + )); + + expect(result.baseElement).toMatchSnapshot(); + }); + + it("renders default mount mode in octal when provided", () => { + const projectedVolume: ProjectedSource = { + defaultMode: 0o777, + sources: [], + }; + const projectedVolumeName = "my-projected"; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + uid: "123", + selfLink: "/api/v1/pod/default/my-pod", + }, + spec: { + volumes: [{ + name: projectedVolumeName, + projected: projectedVolume, + }], + }, + }); + const result = render(( + + )); + + expect(result.baseElement).toMatchSnapshot(); + }); + + it("renders when no sources array provided", () => { + const projectedVolume: ProjectedSource = { + defaultMode: 0o777, + }; + const projectedVolumeName = "my-projected"; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + uid: "123", + selfLink: "/api/v1/pod/default/my-pod", + }, + spec: { + volumes: [{ + name: projectedVolumeName, + projected: projectedVolume, + }], + }, + }); + const result = render(( + + )); + + expect(result.baseElement).toMatchSnapshot(); + }); + + it("renders a secret source, when provided", () => { + const projectedVolume: ProjectedSource = { + defaultMode: 0o777, + sources: [{ + secret: { + name: "my-projected-secret", + items: [{ + key: "foo", + path: "/bar", + }], + }, + }], + }; + const projectedVolumeName = "my-projected"; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + uid: "123", + selfLink: "/api/v1/pod/default/my-pod", + }, + spec: { + volumes: [{ + name: projectedVolumeName, + projected: projectedVolume, + }], + }, + }); + const result = render(( + + )); + + expect(result.baseElement).toMatchSnapshot(); + expect(result.getByText("foo⇢/bar", { exact: false })).toBeTruthy(); + }); + + it("renders a secret source including overriding mode", () => { + const projectedVolume: ProjectedSource = { + defaultMode: 0o777, + sources: [{ + secret: { + name: "my-projected-secret", + items: [{ + key: "foo", + path: "/bar", + mode: 0o666, + }], + }, + }], + }; + const projectedVolumeName = "my-projected"; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + uid: "123", + selfLink: "/api/v1/pod/default/my-pod", + }, + spec: { + volumes: [{ + name: projectedVolumeName, + projected: projectedVolume, + }], + }, + }); + const result = render(( + + )); + + expect(result.baseElement).toMatchSnapshot(); + expect(result.getByText("(0o666)", { exact: false })).toBeTruthy(); + }); +}); diff --git a/src/renderer/components/+workloads-pods/details/volumes/variants/projected.tsx b/src/renderer/components/+workloads-pods/details/volumes/variants/projected.tsx index cd725f1c67..c7ca22c87f 100644 --- a/src/renderer/components/+workloads-pods/details/volumes/variants/projected.tsx +++ b/src/renderer/components/+workloads-pods/details/volumes/variants/projected.tsx @@ -4,18 +4,21 @@ */ import React from "react"; +import { displayMode } from "../../../../../utils"; import { DrawerItem, DrawerTitle } from "../../../../drawer"; import type { VolumeVariantComponent } from "../variant-helpers"; export const Projected: VolumeVariantComponent<"projected"> = ( ({ variant: { sources, defaultMode }}) => ( <> - - {`0o${defaultMode.toString(8)}`} - + {typeof defaultMode === "number" && ( + + {displayMode(defaultMode)} + + )} { - sources.map(({ secret, downwardAPI, configMap, serviceAccountToken }, index) => ( + sources?.map(({ secret, downwardAPI, configMap, serviceAccountToken }, index) => ( {secret && ( <> @@ -25,9 +28,12 @@ export const Projected: VolumeVariantComponent<"projected"> = (
    - {secret.items?.map(({ key, path }) => ( + {secret.items?.map(({ key, path, mode }) => (
  • {`${key}⇢${path}`} + {typeof mode === "number" && ( + ` (${displayMode(mode)})` + )}
  • ))}
diff --git a/src/renderer/utils/display-mode.ts b/src/renderer/utils/display-mode.ts new file mode 100644 index 0000000000..e0d2be5d88 --- /dev/null +++ b/src/renderer/utils/display-mode.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +/** + * Format `mode` in octal notation + */ +export function displayMode(mode: number): string { + return `0o${mode.toString(8)}`; +} diff --git a/src/renderer/utils/index.ts b/src/renderer/utils/index.ts index 8f41c54e57..fee7dfc56d 100755 --- a/src/renderer/utils/index.ts +++ b/src/renderer/utils/index.ts @@ -10,6 +10,7 @@ export * from "../../common/event-emitter"; export * from "./cssNames"; export * from "./cssVar"; export * from "./display-booleans"; +export * from "./display-mode"; export * from "./interval"; export * from "./isMiddleClick"; export * from "./isReactNode"; From 827cb8a886ee875476ccd88934d2aa8fa32ba4de Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 30 May 2022 08:01:08 -0700 Subject: [PATCH 15/43] Fix crash in (#5501) --- package.json | 3 + src/common/k8s-api/endpoints/pod.api.ts | 4 +- src/jest-after-env.setup.ts | 5 + .../__snapshots__/ceph-fs.test.tsx.snap | 229 ++++++++++++++++++ .../variants/__tests__/ceph-fs.test.tsx | 116 +++++++++ .../details/volumes/variants/ceph-fs.tsx | 4 +- 6 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 src/jest-after-env.setup.ts create mode 100644 src/renderer/components/+workloads-pods/details/volumes/variants/__tests__/__snapshots__/ceph-fs.test.tsx.snap create mode 100644 src/renderer/components/+workloads-pods/details/volumes/variants/__tests__/ceph-fs.test.tsx diff --git a/package.json b/package.json index 610ee7d886..9b4896ae81 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,9 @@ "/src/jest.setup.ts", "jest-canvas-mock" ], + "setupFilesAfterEnv": [ + "/src/jest-after-env.setup.ts" + ], "globals": { "ts-jest": { "isolatedModules": true diff --git a/src/common/k8s-api/endpoints/pod.api.ts b/src/common/k8s-api/endpoints/pod.api.ts index 8e3aa51559..fbacf4fb07 100644 --- a/src/common/k8s-api/endpoints/pod.api.ts +++ b/src/common/k8s-api/endpoints/pod.api.ts @@ -294,8 +294,10 @@ export interface CephfsSource { secretRef?: SecretReference; /** * Whether the filesystem is used as readOnly. + * + * @default false */ - readOnly: boolean; + readOnly?: boolean; } export interface CinderSource { diff --git a/src/jest-after-env.setup.ts b/src/jest-after-env.setup.ts new file mode 100644 index 0000000000..b9ee36c4cf --- /dev/null +++ b/src/jest-after-env.setup.ts @@ -0,0 +1,5 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import "@testing-library/jest-dom/extend-expect"; diff --git a/src/renderer/components/+workloads-pods/details/volumes/variants/__tests__/__snapshots__/ceph-fs.test.tsx.snap b/src/renderer/components/+workloads-pods/details/volumes/variants/__tests__/__snapshots__/ceph-fs.test.tsx.snap new file mode 100644 index 0000000000..627b2fb1a3 --- /dev/null +++ b/src/renderer/components/+workloads-pods/details/volumes/variants/__tests__/__snapshots__/ceph-fs.test.tsx.snap @@ -0,0 +1,229 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render 'false' for Readonly when false is provided 1`] = ` +
+
+ + Monitors + + +
    + +
+
+ + Mount Path + + + / + +
+
+ + Username + + + admin + +
+
+ + Secret Filepath + + + /etc/ceph/user.secret + +
+
+ + Readonly + + + false + +
+
+`; + +exports[` should render 'false' for Readonly when not provided 1`] = ` +
+
+ + Monitors + + +
    + +
+
+ + Mount Path + + + / + +
+
+ + Username + + + admin + +
+
+ + Secret Filepath + + + /etc/ceph/user.secret + +
+
+ + Readonly + + + false + +
+
+`; + +exports[` should render 'true' for Readonly when true is provided 1`] = ` +
+
+ + Monitors + + +
    + +
+
+ + Mount Path + + + / + +
+
+ + Username + + + admin + +
+
+ + Secret Filepath + + + /etc/ceph/user.secret + +
+
+ + Readonly + + + true + +
+
+`; diff --git a/src/renderer/components/+workloads-pods/details/volumes/variants/__tests__/ceph-fs.test.tsx b/src/renderer/components/+workloads-pods/details/volumes/variants/__tests__/ceph-fs.test.tsx new file mode 100644 index 0000000000..5555332328 --- /dev/null +++ b/src/renderer/components/+workloads-pods/details/volumes/variants/__tests__/ceph-fs.test.tsx @@ -0,0 +1,116 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { render } from "@testing-library/react"; +import React from "react"; +import type { CephfsSource } from "../../../../../../../common/k8s-api/endpoints"; +import { Pod } from "../../../../../../../common/k8s-api/endpoints"; +import { CephFs } from "../ceph-fs"; + +describe("", () => { + it("should render 'false' for Readonly when not provided", () => { + const cephfsName = "my-ceph"; + const cephfsVolume: CephfsSource = { + monitors: [], + }; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + uid: "123", + selfLink: "/api/v1/pod/default/my-pod", + }, + spec: { + volumes: [{ + name: cephfsName, + cephfs: cephfsVolume, + }], + }, + }); + const result = render(( + + )); + + expect(result.container).toMatchSnapshot(); + expect(result.getByTestId("cephfs-readonly")).toHaveTextContent("false"); + }); + + it("should render 'false' for Readonly when false is provided", () => { + const cephfsName = "my-ceph"; + const cephfsVolume: CephfsSource = { + monitors: [], + readOnly: false, + }; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + uid: "123", + selfLink: "/api/v1/pod/default/my-pod", + }, + spec: { + volumes: [{ + name: cephfsName, + cephfs: cephfsVolume, + }], + }, + }); + const result = render(( + + )); + + expect(result.container).toMatchSnapshot(); + expect(result.getByTestId("cephfs-readonly")).toHaveTextContent("false"); + }); + + it("should render 'true' for Readonly when true is provided", () => { + const cephfsName = "my-ceph"; + const cephfsVolume: CephfsSource = { + monitors: [], + readOnly: true, + }; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + uid: "123", + selfLink: "/api/v1/pod/default/my-pod", + }, + spec: { + volumes: [{ + name: cephfsName, + cephfs: cephfsVolume, + }], + }, + }); + const result = render(( + + )); + + expect(result.container).toMatchSnapshot(); + expect(result.getByTestId("cephfs-readonly")).toHaveTextContent("true"); + }); +}); diff --git a/src/renderer/components/+workloads-pods/details/volumes/variants/ceph-fs.tsx b/src/renderer/components/+workloads-pods/details/volumes/variants/ceph-fs.tsx index 0b44e7d355..a723f7be92 100644 --- a/src/renderer/components/+workloads-pods/details/volumes/variants/ceph-fs.tsx +++ b/src/renderer/components/+workloads-pods/details/volumes/variants/ceph-fs.tsx @@ -10,7 +10,7 @@ import type { VolumeVariantComponent } from "../variant-helpers"; import { LocalRef } from "../variant-helpers"; export const CephFs: VolumeVariantComponent<"cephfs"> = ( - ({ pod, variant: { monitors, path = "/", user = "admin", secretFile = "/etc/ceph/user.secret", secretRef, readOnly }}) => ( + ({ pod, variant: { monitors, path = "/", user = "admin", secretFile = "/etc/ceph/user.secret", secretRef, readOnly = false }}) => ( <>
    @@ -39,7 +39,7 @@ export const CephFs: VolumeVariantComponent<"cephfs"> = ( ) } - + {readOnly.toString()} From 5acfcf1b8959f5562e5ffbdedbe90a5f69f8781f Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 30 May 2022 08:31:33 -0700 Subject: [PATCH 16/43] Make terminal tab icon the same as the menu item icon (#5449) --- src/renderer/components/dock/dock.tsx | 6 +----- src/renderer/components/dock/terminal/dock-tab.tsx | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/renderer/components/dock/dock.tsx b/src/renderer/components/dock/dock.tsx index bcd6d19dcf..385d3e0fbc 100644 --- a/src/renderer/components/dock/dock.tsx +++ b/src/renderer/components/dock/dock.tsx @@ -166,11 +166,7 @@ class NonInjectedDock extends React.Component { closeOnScroll={false} > this.props.createTerminalTab()}> - + Terminal session this.props.createResourceTab()}> diff --git a/src/renderer/components/dock/terminal/dock-tab.tsx b/src/renderer/components/dock/terminal/dock-tab.tsx index 64fce73a03..e6e07f50ee 100644 --- a/src/renderer/components/dock/terminal/dock-tab.tsx +++ b/src/renderer/components/dock/terminal/dock-tab.tsx @@ -67,7 +67,6 @@ class NonInjectedTerminalTab exte } render() { - const tabIcon = ; const className = cssNames("TerminalTab", this.props.className, { disconnected: this.isDisconnected, }); @@ -78,7 +77,7 @@ class NonInjectedTerminalTab exte } moreActions={this.isDisconnected && ( Date: Mon, 30 May 2022 14:26:20 -0400 Subject: [PATCH 17/43] Bump @types/webpack-env from 1.16.4 to 1.17.0 (#5472) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9b4896ae81..aedba940b9 100644 --- a/package.json +++ b/package.json @@ -341,7 +341,7 @@ "@types/uuid": "^8.3.4", "@types/webpack": "^5.28.0", "@types/webpack-dev-server": "^4.7.2", - "@types/webpack-env": "^1.16.4", + "@types/webpack-env": "^1.17.0", "@types/webpack-node-externals": "^2.5.3", "@typescript-eslint/eslint-plugin": "^5.26.0", "@typescript-eslint/parser": "^5.17.0", diff --git a/yarn.lock b/yarn.lock index 817aa4542b..c223aa17ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2221,10 +2221,10 @@ dependencies: webpack-dev-server "*" -"@types/webpack-env@^1.16.4": - version "1.16.4" - resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.4.tgz#1f4969042bf76d7ef7b5914f59b3b60073f4e1f4" - integrity sha512-llS8qveOUX3wxHnSykP5hlYFFuMfJ9p5JvIyCiBgp7WTfl6K5ZcyHj8r8JsN/J6QODkAsRRCLIcTuOCu8etkUw== +"@types/webpack-env@^1.17.0": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.17.0.tgz#f99ce359f1bfd87da90cc4a57cab0a18f34a48d0" + integrity sha512-eHSaNYEyxRA5IAG0Ym/yCyf86niZUIF/TpWKofQI/CVfh5HsMEUyfE2kwFxha4ow0s5g0LfISQxpDKjbRDrizw== "@types/webpack-node-externals@^2.5.3": version "2.5.3" From c1b0698a16a8571023180e149ce6b774e52dccbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 14:26:28 -0400 Subject: [PATCH 18/43] Bump type-fest from 2.12.2 to 2.13.0 (#5471) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index aedba940b9..c05c259ffd 100644 --- a/package.json +++ b/package.json @@ -404,7 +404,7 @@ "ts-jest": "26.5.6", "ts-loader": "^9.2.8", "ts-node": "^10.7.0", - "type-fest": "^2.12.2", + "type-fest": "^2.13.0", "typed-emitter": "^1.4.0", "typedoc": "0.22.15", "typedoc-plugin-markdown": "^3.11.12", diff --git a/yarn.lock b/yarn.lock index c223aa17ce..f8f6047e02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13097,10 +13097,10 @@ type-fest@^1.0.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== -type-fest@^2.12.2: - version "2.12.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.12.2.tgz#80a53614e6b9b475eb9077472fb7498dc7aa51d0" - integrity sha512-qt6ylCGpLjZ7AaODxbpyBZSs9fCI9SkL3Z9q2oxMBQhs/uyY+VD8jHA8ULCGmWQJlBgqvO3EJeAngOHD8zQCrQ== +type-fest@^2.12.2, type-fest@^2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.13.0.tgz#d1ecee38af29eb2e863b22299a3d68ef30d2abfb" + integrity sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw== type-is@~1.6.18: version "1.6.18" From 34eb2f8a0b8fe9ba2c08be31ebf1e7718882ac0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 14:26:36 -0400 Subject: [PATCH 19/43] Bump marked from 4.0.15 to 4.0.16 (#5470) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c05c259ffd..2fd18af1c2 100644 --- a/package.json +++ b/package.json @@ -238,7 +238,7 @@ "jsdom": "^16.7.0", "lodash": "^4.17.15", "mac-ca": "^1.0.6", - "marked": "^4.0.15", + "marked": "^4.0.16", "md5-file": "^5.0.0", "mobx": "^6.5.0", "mobx-observable-history": "^2.0.3", diff --git a/yarn.lock b/yarn.lock index f8f6047e02..b048fccb02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8996,10 +8996,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^4.0.12, marked@^4.0.15: - version "4.0.15" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.15.tgz#0216b7c9d5fcf6ac5042343c41d81a8b1b5e1b4a" - integrity sha512-esX5lPdTfG4p8LDkv+obbRCyOKzB+820ZZyMOXJZygZBHrH9b3xXR64X4kT3sPe9Nx8qQXbmcz6kFSMt4Nfk6Q== +marked@^4.0.12, marked@^4.0.16: + version "4.0.16" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.16.tgz#9ec18fc1a723032eb28666100344d9428cf7a264" + integrity sha512-wahonIQ5Jnyatt2fn8KqF/nIqZM8mh3oRu2+l5EANGMhu6RFjiSG52QNE2eWzFMI94HqYSgN184NurgNG6CztA== matcher@^3.0.0: version "3.0.0" From e8b337d02299d1b4a82c67a89441f50e39a8cee9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 14:26:44 -0400 Subject: [PATCH 20/43] Bump sass from 1.51.0 to 1.52.1 (#5469) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2fd18af1c2..6431b522be 100644 --- a/package.json +++ b/package.json @@ -395,7 +395,7 @@ "react-select-event": "^5.5.0", "react-table": "^7.7.0", "react-window": "^1.8.7", - "sass": "^1.51.0", + "sass": "^1.52.1", "sass-loader": "^12.6.0", "sharp": "^0.30.4", "style-loader": "^3.3.1", diff --git a/yarn.lock b/yarn.lock index b048fccb02..30b688dc0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11685,10 +11685,10 @@ sass-loader@^12.6.0: klona "^2.0.4" neo-async "^2.6.2" -sass@^1.32.13, sass@^1.51.0: - version "1.51.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.51.0.tgz#25ea36cf819581fe1fe8329e8c3a4eaaf70d2845" - integrity sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA== +sass@^1.32.13, sass@^1.52.1: + version "1.52.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.52.1.tgz#554693da808543031f9423911d62c60a1acf7889" + integrity sha512-fSzYTbr7z8oQnVJ3Acp9hV80dM1fkMN7mSD/25mpcct9F7FPBMOI8krEYALgU1aZoqGhQNhTPsuSmxjnIvAm4Q== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" From fc804f3f4c481347ec7a031aefb23d9fc72f0033 Mon Sep 17 00:00:00 2001 From: Ryan Russell Date: Tue, 31 May 2022 07:33:53 -0500 Subject: [PATCH 21/43] Improve readability (#5468) Signed-off-by: r --- docs/extensions/get-started/anatomy.md | 2 +- docs/extensions/guides/renderer-extension.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/extensions/get-started/anatomy.md b/docs/extensions/get-started/anatomy.md index 481c18ac2c..8cfcd57076 100644 --- a/docs/extensions/get-started/anatomy.md +++ b/docs/extensions/get-started/anatomy.md @@ -79,7 +79,7 @@ Some of the most-important fields include: } ``` -## Webpack configuation +## Webpack configuration The following webpack `externals` are provided by `Lens` and must be used (when available) to make sure that the versions used are in sync. diff --git a/docs/extensions/guides/renderer-extension.md b/docs/extensions/guides/renderer-extension.md index 5d41dff89d..d90a343692 100644 --- a/docs/extensions/guides/renderer-extension.md +++ b/docs/extensions/guides/renderer-extension.md @@ -224,7 +224,7 @@ export default class ExampleExtension extends Renderer.LensExtension { { id: "bonjour", components: { - Page: () => , + Page: () => , }, }, ]; @@ -250,7 +250,7 @@ export default class ExampleExtension extends Renderer.LensExtension { target: { pageId: "bonjour" }, title: "Bonjour le monde", components: { - Icon: ExempleIcon, + Icon: ExampleIcon, }, }, ]; From 1e6c07e870c5e4a4dbcec22cf8cf60e7b98adfad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 May 2022 09:51:47 -0400 Subject: [PATCH 22/43] Bump eslint-plugin-react from 7.29.4 to 7.30.0 (#5506) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 6431b522be..3d2c1caf7f 100644 --- a/package.json +++ b/package.json @@ -363,7 +363,7 @@ "eslint": "^8.16.0", "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-react": "^7.29.4", + "eslint-plugin-react": "^7.30.0", "eslint-plugin-react-hooks": "^4.5.0", "eslint-plugin-unused-imports": "^2.0.0", "flex.box": "^3.4.4", diff --git a/yarn.lock b/yarn.lock index 30b688dc0c..a61d16c7a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2941,7 +2941,7 @@ array-flatten@^2.1.2: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== -array-includes@^3.1.4: +array-includes@^3.1.4, array-includes@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.5.tgz#2c320010db8d31031fd2a5f6b3bbd4b1aad31bdb" integrity sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ== @@ -2972,7 +2972,7 @@ array.prototype.flat@^1.2.5: es-abstract "^1.19.2" es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.2.5: +array.prototype.flatmap@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz#a7e8ed4225f4788a70cd910abcf0791e76a5534f" integrity sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg== @@ -5457,25 +5457,25 @@ eslint-plugin-react-hooks@^4.5.0: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.5.0.tgz#5f762dfedf8b2cf431c689f533c9d3fa5dcf25ad" integrity sha512-8k1gRt7D7h03kd+SAAlzXkQwWK22BnK6GKZG+FJA6BAGy22CFvl8kCIXKpVux0cCxMWDQUPqSok0LKaZ0aOcCw== -eslint-plugin-react@^7.29.4: - version "7.29.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz#4717de5227f55f3801a5fd51a16a4fa22b5914d2" - integrity sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ== +eslint-plugin-react@^7.30.0: + version "7.30.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.30.0.tgz#8e7b1b2934b8426ac067a0febade1b13bd7064e3" + integrity sha512-RgwH7hjW48BleKsYyHK5vUAvxtE9SMPDKmcPRQgtRCYaZA0XQPt5FSkrU3nhz5ifzMZcA8opwmRJ2cmOO8tr5A== dependencies: - array-includes "^3.1.4" - array.prototype.flatmap "^1.2.5" + array-includes "^3.1.5" + array.prototype.flatmap "^1.3.0" doctrine "^2.1.0" estraverse "^5.3.0" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" object.entries "^1.1.5" object.fromentries "^2.0.5" - object.hasown "^1.1.0" + object.hasown "^1.1.1" object.values "^1.1.5" prop-types "^15.8.1" resolve "^2.0.0-next.3" semver "^6.3.0" - string.prototype.matchall "^4.0.6" + string.prototype.matchall "^4.0.7" eslint-plugin-unused-imports@^2.0.0: version "2.0.0" @@ -9998,7 +9998,7 @@ object.getownpropertydescriptors@^2.0.3: define-properties "^1.1.3" es-abstract "^1.19.1" -object.hasown@^1.1.0: +object.hasown@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.1.tgz#ad1eecc60d03f49460600430d97f23882cf592a3" integrity sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A== @@ -12409,7 +12409,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string.prototype.matchall@^4.0.6: +string.prototype.matchall@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz#8e6ecb0d8a1fb1fda470d81acecb2dba057a481d" integrity sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg== From 8514e814411c38b8d42d76e8a14c65e4d446f093 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 May 2022 09:51:58 -0400 Subject: [PATCH 23/43] Bump esbuild-loader from 2.18.0 to 2.19.0 (#5507) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 218 +++++++++++++++++++++++++-------------------------- 2 files changed, 110 insertions(+), 110 deletions(-) diff --git a/package.json b/package.json index 3d2c1caf7f..ebc453d252 100644 --- a/package.json +++ b/package.json @@ -359,7 +359,7 @@ "electron-builder": "^23.0.3", "electron-notarize": "^0.3.0", "esbuild": "^0.14.38", - "esbuild-loader": "^2.18.0", + "esbuild-loader": "^2.19.0", "eslint": "^8.16.0", "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "^2.26.0", diff --git a/yarn.lock b/yarn.lock index a61d16c7a6..9ac4cc1d54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5220,143 +5220,143 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -esbuild-android-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.38.tgz#5b94a1306df31d55055f64a62ff6b763a47b7f64" - integrity sha512-aRFxR3scRKkbmNuGAK+Gee3+yFxkTJO/cx83Dkyzo4CnQl/2zVSurtG6+G86EQIZ+w+VYngVyK7P3HyTBKu3nw== +esbuild-android-64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.42.tgz#d7ab3d44d3671218d22bce52f65642b12908d954" + integrity sha512-P4Y36VUtRhK/zivqGVMqhptSrFILAGlYp0Z8r9UQqHJ3iWztRCNWnlBzD9HRx0DbueXikzOiwyOri+ojAFfW6A== -esbuild-android-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.38.tgz#78acc80773d16007de5219ccce544c036abd50b8" - integrity sha512-L2NgQRWuHFI89IIZIlpAcINy9FvBk6xFVZ7xGdOwIm8VyhX1vNCEqUJO3DPSSy945Gzdg98cxtNt8Grv1CsyhA== +esbuild-android-arm64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.42.tgz#45336d8bec49abddb3a022996a23373f45a57c27" + integrity sha512-0cOqCubq+RWScPqvtQdjXG3Czb3AWI2CaKw3HeXry2eoA2rrPr85HF7IpdU26UWdBXgPYtlTN1LUiuXbboROhg== -esbuild-darwin-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.38.tgz#e02b1291f629ebdc2aa46fabfacc9aa28ff6aa46" - integrity sha512-5JJvgXkX87Pd1Og0u/NJuO7TSqAikAcQQ74gyJ87bqWRVeouky84ICoV4sN6VV53aTW+NE87qLdGY4QA2S7KNA== +esbuild-darwin-64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.42.tgz#6dff5e44cd70a88c33323e2f5fb598e40c68a9e0" + integrity sha512-ipiBdCA3ZjYgRfRLdQwP82rTiv/YVMtW36hTvAN5ZKAIfxBOyPXY7Cejp3bMXWgzKD8B6O+zoMzh01GZsCuEIA== -esbuild-darwin-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.38.tgz#01eb6650ec010b18c990e443a6abcca1d71290a9" - integrity sha512-eqF+OejMI3mC5Dlo9Kdq/Ilbki9sQBw3QlHW3wjLmsLh+quNfHmGMp3Ly1eWm981iGBMdbtSS9+LRvR2T8B3eQ== +esbuild-darwin-arm64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.42.tgz#2c7313e1b12d2fa5b889c03213d682fb92ca8c4f" + integrity sha512-bU2tHRqTPOaoH/4m0zYHbFWpiYDmaA0gt90/3BMEFaM0PqVK/a6MA2V/ypV5PO0v8QxN6gH5hBPY4YJ2lopXgA== -esbuild-freebsd-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.38.tgz#790b8786729d4aac7be17648f9ea8e0e16475b5e" - integrity sha512-epnPbhZUt93xV5cgeY36ZxPXDsQeO55DppzsIgWM8vgiG/Rz+qYDLmh5ts3e+Ln1wA9dQ+nZmVHw+RjaW3I5Ig== +esbuild-freebsd-64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.42.tgz#ad1c5a564a7e473b8ce95ee7f76618d05d6daffc" + integrity sha512-75h1+22Ivy07+QvxHyhVqOdekupiTZVLN1PMwCDonAqyXd8TVNJfIRFrdL8QmSJrOJJ5h8H1I9ETyl2L8LQDaw== -esbuild-freebsd-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.38.tgz#b66340ab28c09c1098e6d9d8ff656db47d7211e6" - integrity sha512-/9icXUYJWherhk+y5fjPI5yNUdFPtXHQlwP7/K/zg8t8lQdHVj20SqU9/udQmeUo5pDFHMYzcEFfJqgOVeKNNQ== +esbuild-freebsd-arm64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.42.tgz#4bdb480234144f944f1930829bace7561135ddc7" + integrity sha512-W6Jebeu5TTDQMJUJVarEzRU9LlKpNkPBbjqSu+GUPTHDCly5zZEQq9uHkmHHl7OKm+mQ2zFySN83nmfCeZCyNA== -esbuild-linux-32@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.38.tgz#7927f950986fd39f0ff319e92839455912b67f70" - integrity sha512-QfgfeNHRFvr2XeHFzP8kOZVnal3QvST3A0cgq32ZrHjSMFTdgXhMhmWdKzRXP/PKcfv3e2OW9tT9PpcjNvaq6g== +esbuild-linux-32@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.42.tgz#ef18fd19f067e9d2b5f677d6b82fa81519f5a8c2" + integrity sha512-Ooy/Bj+mJ1z4jlWcK5Dl6SlPlCgQB9zg1UrTCeY8XagvuWZ4qGPyYEWGkT94HUsRi2hKsXvcs6ThTOjBaJSMfg== -esbuild-linux-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.38.tgz#4893d07b229d9cfe34a2b3ce586399e73c3ac519" - integrity sha512-uuZHNmqcs+Bj1qiW9k/HZU3FtIHmYiuxZ/6Aa+/KHb/pFKr7R3aVqvxlAudYI9Fw3St0VCPfv7QBpUITSmBR1Q== +esbuild-linux-64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.42.tgz#d84e7333b1c1b22cf8b5b9dbb5dd9b2ecb34b79f" + integrity sha512-2L0HbzQfbTuemUWfVqNIjOfaTRt9zsvjnme6lnr7/MO9toz/MJ5tZhjqrG6uDWDxhsaHI2/nsDgrv8uEEN2eoA== -esbuild-linux-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.38.tgz#8442402e37d0b8ae946ac616784d9c1a2041056a" - integrity sha512-HlMGZTEsBrXrivr64eZ/EO0NQM8H8DuSENRok9d+Jtvq8hOLzrxfsAT9U94K3KOGk2XgCmkaI2KD8hX7F97lvA== +esbuild-linux-arm64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.42.tgz#dc19e282f8c4ffbaa470c02a4d171e4ae0180cca" + integrity sha512-c3Ug3e9JpVr8jAcfbhirtpBauLxzYPpycjWulD71CF6ZSY26tvzmXMJYooQ2YKqDY4e/fPu5K8bm7MiXMnyxuA== -esbuild-linux-arm@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.38.tgz#d5dbf32d38b7f79be0ec6b5fb2f9251fd9066986" - integrity sha512-FiFvQe8J3VKTDXG01JbvoVRXQ0x6UZwyrU4IaLBZeq39Bsbatd94Fuc3F1RGqPF5RbIWW7RvkVQjn79ejzysnA== +esbuild-linux-arm@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.42.tgz#d49870e63e2242b8156bf473f2ee5154226be328" + integrity sha512-STq69yzCMhdRaWnh29UYrLSr/qaWMm/KqwaRF1pMEK7kDiagaXhSL1zQGXbYv94GuGY/zAwzK98+6idCMUOOCg== -esbuild-linux-mips64le@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.38.tgz#95081e42f698bbe35d8ccee0e3a237594b337eb5" - integrity sha512-qd1dLf2v7QBiI5wwfil9j0HG/5YMFBAmMVmdeokbNAMbcg49p25t6IlJFXAeLzogv1AvgaXRXvgFNhScYEUXGQ== +esbuild-linux-mips64le@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.42.tgz#f4e6ff9bf8a6f175470498826f48d093b054fc22" + integrity sha512-QuvpHGbYlkyXWf2cGm51LBCHx6eUakjaSrRpUqhPwjh/uvNUYvLmz2LgPTTPwCqaKt0iwL+OGVL0tXA5aDbAbg== -esbuild-linux-ppc64le@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.38.tgz#dceb0a1b186f5df679618882a7990bd422089b47" - integrity sha512-mnbEm7o69gTl60jSuK+nn+pRsRHGtDPfzhrqEUXyCl7CTOCLtWN2bhK8bgsdp6J/2NyS/wHBjs1x8aBWwP2X9Q== +esbuild-linux-ppc64le@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.42.tgz#ac9c66fc80ba9f8fda15a4cc08f4e55f6c0aed63" + integrity sha512-8ohIVIWDbDT+i7lCx44YCyIRrOW1MYlks9fxTo0ME2LS/fxxdoJBwHWzaDYhjvf8kNpA+MInZvyOEAGoVDrMHg== -esbuild-linux-riscv64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.38.tgz#61fb8edb75f475f9208c4a93ab2bfab63821afd2" - integrity sha512-+p6YKYbuV72uikChRk14FSyNJZ4WfYkffj6Af0/Tw63/6TJX6TnIKE+6D3xtEc7DeDth1fjUOEqm+ApKFXbbVQ== +esbuild-linux-riscv64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.42.tgz#21e0ae492a3a9bf4eecbfc916339a66e204256d0" + integrity sha512-DzDqK3TuoXktPyG1Lwx7vhaF49Onv3eR61KwQyxYo4y5UKTpL3NmuarHSIaSVlTFDDpcIajCDwz5/uwKLLgKiQ== -esbuild-linux-s390x@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.38.tgz#34c7126a4937406bf6a5e69100185fd702d12fe0" - integrity sha512-0zUsiDkGJiMHxBQ7JDU8jbaanUY975CdOW1YDrurjrM0vWHfjv9tLQsW9GSyEb/heSK1L5gaweRjzfUVBFoybQ== +esbuild-linux-s390x@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.42.tgz#06d40b957250ffd9a2183bfdfc9a03d6fd21b3e8" + integrity sha512-YFRhPCxl8nb//Wn6SiS5pmtplBi4z9yC2gLrYoYI/tvwuB1jldir9r7JwAGy1Ck4D7sE7wBN9GFtUUX/DLdcEQ== -esbuild-loader@^2.18.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/esbuild-loader/-/esbuild-loader-2.18.0.tgz#7b9548578ab954574fd94655693d22aa5ec74120" - integrity sha512-AKqxM3bI+gvGPV8o6NAhR+cBxVO8+dh+O0OXBHIXXwuSGumckbPWHzZ17subjBGI2YEGyJ1STH7Haj8aCrwL/w== +esbuild-loader@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/esbuild-loader/-/esbuild-loader-2.19.0.tgz#54f62d1da8262acfc3c5883c24da35af8324f116" + integrity sha512-urGNVE6Tl2rqx92ElKi/LiExXjGvcH6HfDBFzJ9Ppwqh4n6Jmx8x7RKAyMzSM78b6CAaJLhDncG5sPrL0ROh5Q== dependencies: - esbuild "^0.14.6" + esbuild "^0.14.39" joycon "^3.0.1" json5 "^2.2.0" loader-utils "^2.0.0" tapable "^2.2.0" webpack-sources "^2.2.0" -esbuild-netbsd-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.38.tgz#322ea9937d9e529183ee281c7996b93eb38a5d95" - integrity sha512-cljBAApVwkpnJZfnRVThpRBGzCi+a+V9Ofb1fVkKhtrPLDYlHLrSYGtmnoTVWDQdU516qYI8+wOgcGZ4XIZh0Q== +esbuild-netbsd-64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.42.tgz#185664f05f10914f14ed43bd9e22b7de584267f7" + integrity sha512-QYSD2k+oT9dqB/4eEM9c+7KyNYsIPgzYOSrmfNGDIyJrbT1d+CFVKvnKahDKNJLfOYj8N4MgyFaU9/Ytc6w5Vw== -esbuild-openbsd-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.38.tgz#1ca29bb7a2bf09592dcc26afdb45108f08a2cdbd" - integrity sha512-CDswYr2PWPGEPpLDUO50mL3WO/07EMjnZDNKpmaxUPsrW+kVM3LoAqr/CE8UbzugpEiflYqJsGPLirThRB18IQ== +esbuild-openbsd-64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.42.tgz#c29006f659eb4e55283044bbbd4eb4054fae8839" + integrity sha512-M2meNVIKWsm2HMY7+TU9AxM7ZVwI9havdsw6m/6EzdXysyCFFSoaTQ/Jg03izjCsK17FsVRHqRe26Llj6x0MNA== -esbuild-sunos-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.38.tgz#c9446f7d8ebf45093e7bb0e7045506a88540019b" - integrity sha512-2mfIoYW58gKcC3bck0j7lD3RZkqYA7MmujFYmSn9l6TiIcAMpuEvqksO+ntBgbLep/eyjpgdplF7b+4T9VJGOA== +esbuild-sunos-64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.42.tgz#aa9eec112cd1e7105e7bb37000eca7d460083f8f" + integrity sha512-uXV8TAZEw36DkgW8Ak3MpSJs1ofBb3Smkc/6pZ29sCAN1KzCAQzsje4sUwugf+FVicrHvlamCOlFZIXgct+iqQ== -esbuild-windows-32@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.38.tgz#f8e9b4602fd0ccbd48e5c8d117ec0ba4040f2ad1" - integrity sha512-L2BmEeFZATAvU+FJzJiRLFUP+d9RHN+QXpgaOrs2klshoAm1AE6Us4X6fS9k33Uy5SzScn2TpcgecbqJza1Hjw== +esbuild-windows-32@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.42.tgz#c3fc450853c61a74dacc5679de301db23b73e61e" + integrity sha512-4iw/8qWmRICWi9ZOnJJf9sYt6wmtp3hsN4TdI5NqgjfOkBVMxNdM9Vt3626G1Rda9ya2Q0hjQRD9W1o+m6Lz6g== -esbuild-windows-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.38.tgz#280f58e69f78535f470905ce3e43db1746518107" - integrity sha512-Khy4wVmebnzue8aeSXLC+6clo/hRYeNIm0DyikoEqX+3w3rcvrhzpoix0S+MF9vzh6JFskkIGD7Zx47ODJNyCw== +esbuild-windows-64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.42.tgz#b877aa37ff47d9fcf0ccb1ca6a24b31475a5e555" + integrity sha512-j3cdK+Y3+a5H0wHKmLGTJcq0+/2mMBHPWkItR3vytp/aUGD/ua/t2BLdfBIzbNN9nLCRL9sywCRpOpFMx3CxzA== -esbuild-windows-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.38.tgz#d97e9ac0f95a4c236d9173fa9f86c983d6a53f54" - integrity sha512-k3FGCNmHBkqdJXuJszdWciAH77PukEyDsdIryEHn9cKLQFxzhT39dSumeTuggaQcXY57UlmLGIkklWZo2qzHpw== +esbuild-windows-arm64@0.14.42: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.42.tgz#79da8744626f24bc016dc40d016950b5a4a2bac5" + integrity sha512-+lRAARnF+hf8J0mN27ujO+VbhPbDqJ8rCcJKye4y7YZLV6C4n3pTRThAb388k/zqF5uM0lS5O201u0OqoWSicw== -esbuild@^0.14.38, esbuild@^0.14.6: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.38.tgz#99526b778cd9f35532955e26e1709a16cca2fb30" - integrity sha512-12fzJ0fsm7gVZX1YQ1InkOE5f9Tl7cgf6JPYXRJtPIoE0zkWAbHdPHVPPaLi9tYAcEBqheGzqLn/3RdTOyBfcA== +esbuild@^0.14.38, esbuild@^0.14.39: + version "0.14.42" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.42.tgz#98587df0b024d5f6341b12a1d735a2bff55e1836" + integrity sha512-V0uPZotCEHokJdNqyozH6qsaQXqmZEOiZWrXnds/zaH/0SyrIayRXWRB98CENO73MIZ9T3HBIOsmds5twWtmgw== optionalDependencies: - esbuild-android-64 "0.14.38" - esbuild-android-arm64 "0.14.38" - esbuild-darwin-64 "0.14.38" - esbuild-darwin-arm64 "0.14.38" - esbuild-freebsd-64 "0.14.38" - esbuild-freebsd-arm64 "0.14.38" - esbuild-linux-32 "0.14.38" - esbuild-linux-64 "0.14.38" - esbuild-linux-arm "0.14.38" - esbuild-linux-arm64 "0.14.38" - esbuild-linux-mips64le "0.14.38" - esbuild-linux-ppc64le "0.14.38" - esbuild-linux-riscv64 "0.14.38" - esbuild-linux-s390x "0.14.38" - esbuild-netbsd-64 "0.14.38" - esbuild-openbsd-64 "0.14.38" - esbuild-sunos-64 "0.14.38" - esbuild-windows-32 "0.14.38" - esbuild-windows-64 "0.14.38" - esbuild-windows-arm64 "0.14.38" + esbuild-android-64 "0.14.42" + esbuild-android-arm64 "0.14.42" + esbuild-darwin-64 "0.14.42" + esbuild-darwin-arm64 "0.14.42" + esbuild-freebsd-64 "0.14.42" + esbuild-freebsd-arm64 "0.14.42" + esbuild-linux-32 "0.14.42" + esbuild-linux-64 "0.14.42" + esbuild-linux-arm "0.14.42" + esbuild-linux-arm64 "0.14.42" + esbuild-linux-mips64le "0.14.42" + esbuild-linux-ppc64le "0.14.42" + esbuild-linux-riscv64 "0.14.42" + esbuild-linux-s390x "0.14.42" + esbuild-netbsd-64 "0.14.42" + esbuild-openbsd-64 "0.14.42" + esbuild-sunos-64 "0.14.42" + esbuild-windows-32 "0.14.42" + esbuild-windows-64 "0.14.42" + esbuild-windows-arm64 "0.14.42" escalade@^3.1.1: version "3.1.1" From 86067836d029b12273dc54d527ef0e982a162495 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 May 2022 09:52:05 -0400 Subject: [PATCH 24/43] Bump postcss from 8.4.13 to 8.4.14 (#5508) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index ebc453d252..c484baff39 100644 --- a/package.json +++ b/package.json @@ -384,7 +384,7 @@ "node-loader": "^2.0.0", "nodemon": "^2.0.16", "playwright": "^1.20.2", - "postcss": "^8.4.12", + "postcss": "^8.4.14", "postcss-loader": "^6.2.1", "randomcolor": "^0.6.2", "react-beautiful-dnd": "^13.1.0", diff --git a/yarn.lock b/yarn.lock index 9ac4cc1d54..040b467aae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9387,7 +9387,7 @@ nan@^2.14.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== -nanoid@^3.3.3: +nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== @@ -10678,12 +10678,12 @@ postcss@^6.0.14, postcss@^6.0.2: source-map "^0.6.1" supports-color "^5.4.0" -postcss@^8.3.0, postcss@^8.4.12, postcss@^8.4.7: - version "8.4.13" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.13.tgz#7c87bc268e79f7f86524235821dfdf9f73e5d575" - integrity sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA== +postcss@^8.3.0, postcss@^8.4.12, postcss@^8.4.14, postcss@^8.4.7: + version "8.4.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" + integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== dependencies: - nanoid "^3.3.3" + nanoid "^3.3.4" picocolors "^1.0.0" source-map-js "^1.0.2" From a27c7452a45331956c549deb499ecf49e97a3894 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 May 2022 09:52:11 -0400 Subject: [PATCH 25/43] Bump dompurify from 2.3.6 to 2.3.8 (#5509) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c484baff39..f0d9c48416 100644 --- a/package.json +++ b/package.json @@ -354,7 +354,7 @@ "concurrently": "^7.2.1", "css-loader": "^6.7.1", "deepdash": "^5.3.9", - "dompurify": "^2.3.6", + "dompurify": "^2.3.8", "electron": "^14.2.9", "electron-builder": "^23.0.3", "electron-notarize": "^0.3.0", diff --git a/yarn.lock b/yarn.lock index 040b467aae..a47382829e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4862,10 +4862,10 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" -dompurify@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.6.tgz#2e019d7d7617aacac07cbbe3d88ae3ad354cf875" - integrity sha512-OFP2u/3T1R5CEgWCEONuJ1a5+MFKnOYpkywpUSxv/dj1LeBT1erK+JwM7zK0ROy2BRhqVCf0LRw/kHqKuMkVGg== +dompurify@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.8.tgz#224fe9ae57d7ebd9a1ae1ac18c1c1ca3f532226f" + integrity sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw== domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" From f12851ff3bc2cccf6b408d23d96773ef2c2bdb16 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 31 May 2022 07:35:59 -0700 Subject: [PATCH 26/43] Update slack invite link (#5515) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 595def597b..cb9f44b35d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Lens Open Source Project (OpenLens) [![Build Status](https://github.com/lensapp/lens/actions/workflows/test.yml/badge.svg)](https://github.com/lensapp/lens/actions/workflows/test.yml) -[![Chat on Slack](https://img.shields.io/badge/chat-on%20slack-blue.svg?logo=slack&longCache=true&style=flat)](https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI) +[![Chat on Slack](https://img.shields.io/badge/chat-on%20slack-blue.svg?logo=slack&longCache=true&style=flat)](https://join.slack.com/t/k8slens/shared_invite/zt-198iepl92-EPJsCckkJ~f887vWqJcgGA) ## The Repository From 94504eaec8662a8fc96587869053254d3910d926 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 12:55:43 -0400 Subject: [PATCH 27/43] Bump got from 11.8.3 to 11.8.5 (#5525) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f0d9c48416..980b2cebb9 100644 --- a/package.json +++ b/package.json @@ -227,7 +227,7 @@ "filehound": "^1.17.6", "fs-extra": "^9.0.1", "glob-to-regexp": "^0.4.1", - "got": "^11.8.3", + "got": "^11.8.5", "grapheme-splitter": "^1.0.4", "handlebars": "^4.7.7", "history": "^4.10.1", diff --git a/yarn.lock b/yarn.lock index a47382829e..0990108242 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6494,10 +6494,10 @@ globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -got@^11.8.0, got@^11.8.3: - version "11.8.3" - resolved "https://registry.yarnpkg.com/got/-/got-11.8.3.tgz#f496c8fdda5d729a90b4905d2b07dbd148170770" - integrity sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg== +got@^11.8.0, got@^11.8.5: + version "11.8.5" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" + integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ== dependencies: "@sindresorhus/is" "^4.0.0" "@szmarczak/http-timer" "^4.0.5" From 5934d5604b50d63a40ff7f8632fa9ba5c9c4e728 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 12:57:12 -0400 Subject: [PATCH 28/43] Bump webpack-dev-server from 4.9.0 to 4.9.1 (#5522) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 980b2cebb9..89985d093a 100644 --- a/package.json +++ b/package.json @@ -413,7 +413,7 @@ "typescript-plugin-css-modules": "^3.4.0", "webpack": "^5.72.0", "webpack-cli": "^4.9.2", - "webpack-dev-server": "^4.9.0", + "webpack-dev-server": "^4.9.1", "webpack-node-externals": "^3.0.0", "xterm": "^4.18.0", "xterm-addon-fit": "^0.5.0" diff --git a/yarn.lock b/yarn.lock index 0990108242..9c334ea66b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12059,7 +12059,7 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -sockjs@^0.3.21: +sockjs@^0.3.24: version "0.3.24" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== @@ -13599,10 +13599,10 @@ webpack-dev-middleware@^5.3.1: range-parser "^1.2.1" schema-utils "^4.0.0" -webpack-dev-server@*, webpack-dev-server@^4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.9.0.tgz#737dbf44335bb8bde68f8f39127fc401c97a1557" - integrity sha512-+Nlb39iQSOSsFv0lWUuUTim3jDQO8nhK3E68f//J2r5rIcp4lULHXz2oZ0UVdEeWXEh5lSzYUlzarZhDAeAVQw== +webpack-dev-server@*, webpack-dev-server@^4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.9.1.tgz#184607b0287c791aeaa45e58e8fe75fcb4d7e2a8" + integrity sha512-CTMfu2UMdR/4OOZVHRpdy84pNopOuigVIsRbGX3LVDMWNP8EUgC5mUBMErbwBlHTEX99ejZJpVqrir6EXAEajA== dependencies: "@types/bonjour" "^3.5.9" "@types/connect-history-api-fallback" "^1.3.5" @@ -13628,7 +13628,7 @@ webpack-dev-server@*, webpack-dev-server@^4.9.0: schema-utils "^4.0.0" selfsigned "^2.0.1" serve-index "^1.9.1" - sockjs "^0.3.21" + sockjs "^0.3.24" spdy "^4.0.2" webpack-dev-middleware "^5.3.1" ws "^8.4.2" From 6ca9f3ea0358c300eb01a4624bc94562f73477ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 12:57:20 -0400 Subject: [PATCH 29/43] Bump sharp from 0.30.4 to 0.30.6 (#5523) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 89985d093a..703c62fc7d 100644 --- a/package.json +++ b/package.json @@ -397,7 +397,7 @@ "react-window": "^1.8.7", "sass": "^1.52.1", "sass-loader": "^12.6.0", - "sharp": "^0.30.4", + "sharp": "^0.30.6", "style-loader": "^3.3.1", "tailwindcss": "^3.0.23", "tar-stream": "^2.2.0", diff --git a/yarn.lock b/yarn.lock index 9c334ea66b..37074aea58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9463,10 +9463,10 @@ node-addon-api@^1.6.3: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== -node-addon-api@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" - integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== +node-addon-api@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501" + integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA== node-fetch-npm@^2.0.2: version "2.0.4" @@ -10687,7 +10687,7 @@ postcss@^8.3.0, postcss@^8.4.12, postcss@^8.4.14, postcss@^8.4.7: picocolors "^1.0.0" source-map-js "^1.0.2" -prebuild-install@^7.0.1: +prebuild-install@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.0.tgz#991b6ac16c81591ba40a6d5de93fb33673ac1370" integrity sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA== @@ -11890,15 +11890,15 @@ shallow-clone@^3.0.0: dependencies: kind-of "^6.0.2" -sharp@^0.30.4: - version "0.30.4" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.30.4.tgz#73d9daa63bbc20da189c9328d75d5d395fc8fb73" - integrity sha512-3Onig53Y6lji4NIZo69s14mERXXY/GV++6CzOYx/Rd8bnTwbhFbL09WZd7Ag/CCnA0WxFID8tkY0QReyfL6v0Q== +sharp@^0.30.6: + version "0.30.6" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.30.6.tgz#02264e9826b5f1577509f70bb627716099778873" + integrity sha512-lSdVxFxcndzcXggDrak6ozdGJgmIgES9YVZWtAFrwi+a/H5vModaf51TghBtMPw+71sLxUsTy2j+aB7qLIODQg== dependencies: color "^4.2.3" detect-libc "^2.0.1" - node-addon-api "^4.3.0" - prebuild-install "^7.0.1" + node-addon-api "^5.0.0" + prebuild-install "^7.1.0" semver "^7.3.7" simple-get "^4.0.1" tar-fs "^2.1.1" From 2f13fdcfe156e764257b7675ffde2b7dfd4a7440 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 13:01:47 -0400 Subject: [PATCH 30/43] Bump typedoc from 0.22.15 to 0.22.16 (#5524) Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.22.15 to 0.22.16. - [Release notes](https://github.com/TypeStrong/TypeDoc/releases) - [Changelog](https://github.com/TypeStrong/typedoc/blob/master/CHANGELOG.md) - [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.22.15...v0.22.16) --- updated-dependencies: - dependency-name: typedoc dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 37 ++++++++++++++++++++++++------------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 703c62fc7d..12d03359ec 100644 --- a/package.json +++ b/package.json @@ -406,7 +406,7 @@ "ts-node": "^10.7.0", "type-fest": "^2.13.0", "typed-emitter": "^1.4.0", - "typedoc": "0.22.15", + "typedoc": "0.22.16", "typedoc-plugin-markdown": "^3.11.12", "typeface-roboto": "^1.1.13", "typescript": "^4.5.5", diff --git a/yarn.lock b/yarn.lock index 37074aea58..a578b70781 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6415,7 +6415,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: +glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -6427,6 +6427,17 @@ glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, gl once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" + integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + global-agent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-3.0.0.tgz#ae7cd31bd3583b93c5a16437a1afe27cc33a1ab6" @@ -8996,7 +9007,7 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^4.0.12, marked@^4.0.16: +marked@^4.0.16: version "4.0.16" resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.16.tgz#9ec18fc1a723032eb28666100344d9428cf7a264" integrity sha512-wahonIQ5Jnyatt2fn8KqF/nIqZM8mh3oRu2+l5EANGMhu6RFjiSG52QNE2eWzFMI94HqYSgN184NurgNG6CztA== @@ -9185,10 +9196,10 @@ minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.0, minimatch@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" - integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== +minimatch@^5.0.0, minimatch@^5.0.1, minimatch@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" + integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== dependencies: brace-expansion "^2.0.1" @@ -13139,15 +13150,15 @@ typedoc-plugin-markdown@^3.11.12: dependencies: handlebars "^4.7.7" -typedoc@0.22.15: - version "0.22.15" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.22.15.tgz#c6ad7ed9d017dc2c3a06c9189cb392bd8e2d8c3f" - integrity sha512-CMd1lrqQbFvbx6S9G6fL4HKp3GoIuhujJReWqlIvSb2T26vGai+8Os3Mde7Pn832pXYemd9BMuuYWhFpL5st0Q== +typedoc@0.22.16: + version "0.22.16" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.22.16.tgz#41e0ff099274ce13c5c3ea49cc5ad615d2f3119e" + integrity sha512-0Qf0/CsQe6JZTXoYwBM3Iql8gLAWLjQP7O/j9YzfkJp3G/WVGmIMRajKnldJuA/zVvhr+ifsHTgctQh5g2t4iw== dependencies: - glob "^7.2.0" + glob "^8.0.3" lunr "^2.3.9" - marked "^4.0.12" - minimatch "^5.0.1" + marked "^4.0.16" + minimatch "^5.1.0" shiki "^0.10.1" typeface-roboto@^1.1.13: From a332327b4ff4e163813f983fd1e98ef8be5e7459 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 1 Jun 2022 11:24:36 -0700 Subject: [PATCH 31/43] Cherry Pick: Fix remove and edit buttons not updating (#5505) (#5537) --- .../kube-object-menu.test.tsx.snap | 126 ++++++++++++++++++ .../kube-object-menu.test.tsx | 54 +++++++- .../kube-object-menu/kube-object-menu.tsx | 6 + 3 files changed, 180 insertions(+), 6 deletions(-) diff --git a/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap b/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap index d071fa21ea..3e932aeb06 100644 --- a/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap +++ b/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap @@ -130,6 +130,132 @@ exports[`kube-object-menu given kube object when removing kube object renders 1` `; +exports[`kube-object-menu given kube object when rerendered with different kube object renders 1`] = ` + +
    +
    + +
    +
    + +`; + +exports[`kube-object-menu given kube object when rerendered with different kube object when removing new kube object renders 1`] = ` + +
    +
    + +
    +
    + + +`; + exports[`kube-object-menu given kube object with namespace when removing kube object, renders confirmation dialog with namespace 1`] = `
    diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx index 4da76639b8..6a15c9c2ff 100644 --- a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx +++ b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx @@ -3,6 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import React from "react"; +import type { RenderResult } from "@testing-library/react"; import { screen, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import { KubeObject } from "../../../common/k8s-api/kube-object"; @@ -125,7 +126,7 @@ describe("kube-object-menu", () => { }); describe("given kube object", () => { - let baseElement: Element; + let result: RenderResult; let removeActionMock: AsyncFnMock<() => void>; beforeEach(async () => { @@ -142,8 +143,7 @@ describe("kube-object-menu", () => { }); removeActionMock = asyncFn(); - - ({ baseElement } = render( + result = render((
    @@ -152,18 +152,60 @@ describe("kube-object-menu", () => { toolbar={true} removeAction={removeActionMock} /> -
    , +
    )); }); it("renders", () => { - expect(baseElement).toMatchSnapshot(); + expect(result.baseElement).toMatchSnapshot(); }); it("does not open a confirmation dialog yet", () => { expect(screen.queryByTestId("confirmation-dialog")).toBeNull(); }); + describe("when rerendered with different kube object", () => { + beforeEach(() => { + const newObjectStub = KubeObject.create({ + apiVersion: "some-other-api-version", + kind: "some-other-kind", + metadata: { + uid: "some-other-uid", + name: "some-other-name", + resourceVersion: "some-other-resource-version", + namespace: "some-other-namespace", + }, + }); + + result.rerender( +
    + + + +
    , + ); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + describe("when removing new kube object", () => { + beforeEach(async () => { + userEvent.click(await screen.findByTestId("menu-action-delete")); + }); + + it("renders", async () => { + await screen.findByTestId("confirmation-dialog"); + expect(result.baseElement).toMatchSnapshot(); + }); + }); + }); + describe("when removing kube object", () => { beforeEach(async () => { userEvent.click(await screen.findByTestId("menu-action-delete")); @@ -171,7 +213,7 @@ describe("kube-object-menu", () => { it("renders", async () => { await screen.findByTestId("confirmation-dialog"); - expect(baseElement).toMatchSnapshot(); + expect(result.baseElement).toMatchSnapshot(); }); describe("when remove is confirmed", () => { diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.tsx index eed957499b..a69fded655 100644 --- a/src/renderer/components/kube-object-menu/kube-object-menu.tsx +++ b/src/renderer/components/kube-object-menu/kube-object-menu.tsx @@ -49,6 +49,12 @@ interface Dependencies { class NonInjectedKubeObjectMenu extends React.Component & Dependencies> { private menuItems = observable.array(); + componentDidUpdate(prevProps: Readonly & Dependencies>): void { + if (prevProps.object !== this.props.object && this.props.object) { + this.emitOnContextMenuOpen(this.props.object); + } + } + private renderRemoveMessage(object: KubeObject) { const breadcrumbParts = [object.getNs(), object.getName()]; const breadcrumb = breadcrumbParts.filter(identity).join("/"); From 856598c12dc9e855881c3b2a590b6d33fc59f4dd Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 1 Jun 2022 12:00:54 -0700 Subject: [PATCH 32/43] Fix failing kube-object-menu.test.tsx test (#5538) --- .../__snapshots__/kube-object-menu.test.tsx.snap | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap b/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap index 3e932aeb06..976c92f3d9 100644 --- a/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap +++ b/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap @@ -221,9 +221,7 @@ exports[`kube-object-menu given kube object when rerendered with different kube

    - Remove - some-other-kind - + Remove some-other-kind some-other-namespace/some-other-name From ec1df4717d2a9fd6691c8ae72205704fc963f0ea Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 1 Jun 2022 15:28:02 -0400 Subject: [PATCH 33/43] Fix type error in kube-object-menu.test.tsx (#5540) --- .../components/kube-object-menu/kube-object-menu.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx index 6a15c9c2ff..9dabdc9da4 100644 --- a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx +++ b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx @@ -174,6 +174,7 @@ describe("kube-object-menu", () => { name: "some-other-name", resourceVersion: "some-other-resource-version", namespace: "some-other-namespace", + selfLink: "some-other-api-version/some-other-kind/some-other-namespace/some-other-name", }, }); From 06db3119a6e9b5352d56afc94b5f88e8bb6118c5 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 1 Jun 2022 16:25:33 -0400 Subject: [PATCH 34/43] Fix ERR_FAILED for splash screen on windows sometimes (#5539) --- src/common/register-protocol.ts | 18 ------------ .../register-file-protocol.injectable.ts | 28 ------------------ src/main/getDiForUnitTesting.ts | 2 -- .../application-window.injectable.ts | 4 ++- .../create-electron-window-for.injectable.ts | 29 +++++++++++++++---- .../create-lens-window.injectable.ts | 16 +++++----- .../splash-window/splash-window.injectable.ts | 9 +++++- .../setup-file-protocol.injectable.ts | 27 ----------------- 8 files changed, 41 insertions(+), 92 deletions(-) delete mode 100644 src/common/register-protocol.ts delete mode 100644 src/main/electron-app/features/register-file-protocol.injectable.ts delete mode 100644 src/main/start-main-application/runnables/setup-file-protocol.injectable.ts diff --git a/src/common/register-protocol.ts b/src/common/register-protocol.ts deleted file mode 100644 index db00778b2e..0000000000 --- a/src/common/register-protocol.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -// Register custom protocols - -import { protocol } from "electron"; -import path from "path"; - -export function registerFileProtocol(name: string, basePath: string) { - protocol.registerFileProtocol(name, (request, callback) => { - const filePath = request.url.replace(`${name}://`, ""); - const absPath = path.resolve(basePath, filePath); - - callback({ path: absPath }); - }); -} diff --git a/src/main/electron-app/features/register-file-protocol.injectable.ts b/src/main/electron-app/features/register-file-protocol.injectable.ts deleted file mode 100644 index dfc75954e3..0000000000 --- a/src/main/electron-app/features/register-file-protocol.injectable.ts +++ /dev/null @@ -1,28 +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 { protocol } from "electron"; -import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable"; - -const registerFileProtocolInjectable = getInjectable({ - id: "register-file-protocol", - - instantiate: (di) => { - const getAbsolutePath = di.inject(getAbsolutePathInjectable); - - return (name: string, basePath: string) => { - protocol.registerFileProtocol(name, (request, callback) => { - const filePath = request.url.replace(`${name}://`, ""); - const absPath = getAbsolutePath(basePath, filePath); - - callback({ path: absPath }); - }); - }; - }, - - causesSideEffects: true, -}); - -export default registerFileProtocolInjectable; diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index efd065905c..50b5b05e3d 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -38,7 +38,6 @@ import type { AppEvent } from "../common/app-event-bus/event-bus"; import commandLineArgumentsInjectable from "./utils/command-line-arguments.injectable"; import initializeExtensionsInjectable from "./start-main-application/runnables/initialize-extensions.injectable"; import lensResourcesDirInjectable from "../common/vars/lens-resources-dir.injectable"; -import registerFileProtocolInjectable from "./electron-app/features/register-file-protocol.injectable"; import environmentVariablesInjectable from "../common/utils/environment-variables.injectable"; import setupIpcMainHandlersInjectable from "./electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable"; import setupLensProxyInjectable from "./start-main-application/runnables/setup-lens-proxy.injectable"; @@ -237,5 +236,4 @@ const overrideElectronFeatures = (di: DiContainer) => { di.override(setElectronAppPathInjectable, () => () => {}); di.override(isAutoUpdateEnabledInjectable, () => () => false); - di.override(registerFileProtocolInjectable, () => () => {}); }; diff --git a/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts index 029bfa7d82..dd3feb3020 100644 --- a/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts +++ b/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts @@ -29,7 +29,9 @@ const applicationWindowInjectable = getInjectable({ title: applicationName, defaultHeight: 900, defaultWidth: 1440, - getContentUrl: () => `http://localhost:${lensProxyPort.get()}`, + getContentSource: () => ({ + url: `http://localhost:${lensProxyPort.get()}`, + }), resizable: true, windowFrameUtilitiesAreShown: isMac, titleBarStyle: isMac ? "hiddenInset" : "hidden", diff --git a/src/main/start-main-application/lens-window/application-window/create-electron-window-for.injectable.ts b/src/main/start-main-application/lens-window/application-window/create-electron-window-for.injectable.ts index 700ab1e103..b0325c538c 100644 --- a/src/main/start-main-application/lens-window/application-window/create-electron-window-for.injectable.ts +++ b/src/main/start-main-application/lens-window/application-window/create-electron-window-for.injectable.ts @@ -7,18 +7,26 @@ import loggerInjectable from "../../../../common/logger.injectable"; import applicationWindowStateInjectable from "./application-window-state.injectable"; import { BrowserWindow } from "electron"; import { openBrowser } from "../../../../common/utils"; -import type { SendToViewArgs } from "./lens-window-injection-token"; import sendToChannelInElectronBrowserWindowInjectable from "./send-to-channel-in-electron-browser-window.injectable"; import type { LensWindow } from "./create-lens-window.injectable"; +import type { RequireExactlyOne } from "type-fest"; export type ElectronWindowTitleBarStyle = "hiddenInset" | "hidden" | "default" | "customButtonsOnHover"; +export interface FileSource { + file: string; +} +export interface UrlSource { + url: string; +} +export type ContentSource = RequireExactlyOne; + export interface ElectronWindowConfiguration { id: string; title: string; defaultHeight: number; defaultWidth: number; - getContentUrl: () => string; + getContentSource: () => ContentSource; resizable: boolean; windowFrameUtilitiesAreShown: boolean; centered: boolean; @@ -33,6 +41,10 @@ export interface ElectronWindowConfiguration { export type CreateElectronWindow = () => Promise; export type CreateElectronWindowFor = (config: ElectronWindowConfiguration) => CreateElectronWindow; +function isFileSource(src: ContentSource): src is FileSource { + return typeof (src as FileSource).file === "string"; +} + const createElectronWindowFor = getInjectable({ id: "create-electron-window-for", @@ -159,17 +171,22 @@ const createElectronWindowFor = getInjectable({ return { action: "deny" }; }); - const contentUrl = configuration.getContentUrl(); + const contentSource = configuration.getContentSource(); - logger.info(`[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from url: ${contentUrl}...`); + if (isFileSource(contentSource)) { + logger.info(`[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from file: ${contentSource.file}...`); + await browserWindow.loadFile(contentSource.file); + } else { + logger.info(`[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from url: ${contentSource.url}...`); + await browserWindow.loadURL(contentSource.url); + } - await browserWindow.loadURL(contentUrl); await configuration.beforeOpen?.(); return { show: () => browserWindow.show(), close: () => browserWindow.close(), - send: (args: SendToViewArgs) => sendToChannelInLensWindow(browserWindow, args), + send: (args) => sendToChannelInLensWindow(browserWindow, args), }; }; }, diff --git a/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts index cea2dace06..c93877ee6a 100644 --- a/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts +++ b/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import type { SendToViewArgs } from "./lens-window-injection-token"; -import type { ElectronWindowTitleBarStyle } from "./create-electron-window-for.injectable"; +import type { ContentSource, ElectronWindowTitleBarStyle } from "./create-electron-window-for.injectable"; import createElectronWindowForInjectable from "./create-electron-window-for.injectable"; export interface LensWindow { @@ -13,12 +13,12 @@ export interface LensWindow { send: (args: SendToViewArgs) => void; } -interface LensWindowConfiguration { +export interface LensWindowConfiguration { id: string; title: string; defaultHeight: number; defaultWidth: number; - getContentUrl: () => string; + getContentSource: () => ContentSource; resizable: boolean; windowFrameUtilitiesAreShown: boolean; centered: boolean; @@ -38,12 +38,10 @@ const createLensWindowInjectable = getInjectable({ return (configuration: LensWindowConfiguration) => { let browserWindow: LensWindow | undefined; - const createElectronWindow = createElectronWindowFor(Object.assign( - { - onClose: () => browserWindow = undefined, - }, - configuration, - )); + const createElectronWindow = createElectronWindowFor({ + ...configuration, + onClose: () => browserWindow = undefined, + }); return { get visible() { diff --git a/src/main/start-main-application/lens-window/splash-window/splash-window.injectable.ts b/src/main/start-main-application/lens-window/splash-window/splash-window.injectable.ts index bbef47f140..ab8c2c0c35 100644 --- a/src/main/start-main-application/lens-window/splash-window/splash-window.injectable.ts +++ b/src/main/start-main-application/lens-window/splash-window/splash-window.injectable.ts @@ -5,17 +5,24 @@ import { getInjectable } from "@ogre-tools/injectable"; import { lensWindowInjectionToken } from "../application-window/lens-window-injection-token"; import createLensWindowInjectable from "../application-window/create-lens-window.injectable"; +import staticFilesDirectoryInjectable from "../../../../common/vars/static-files-directory.injectable"; +import getAbsolutePathInjectable from "../../../../common/path/get-absolute-path.injectable"; const splashWindowInjectable = getInjectable({ id: "splash-window", instantiate: (di) => { const createLensWindow = di.inject(createLensWindowInjectable); + const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable); + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const splashWindowFile = getAbsolutePath(staticFilesDirectory, "splash.html"); return createLensWindow({ id: "splash", title: "Loading", - getContentUrl: () => "static://splash.html", + getContentSource: () => ({ + file: splashWindowFile, + }), defaultWidth: 500, defaultHeight: 300, resizable: false, diff --git a/src/main/start-main-application/runnables/setup-file-protocol.injectable.ts b/src/main/start-main-application/runnables/setup-file-protocol.injectable.ts deleted file mode 100644 index 77e76aaf55..0000000000 --- a/src/main/start-main-application/runnables/setup-file-protocol.injectable.ts +++ /dev/null @@ -1,27 +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 registerFileProtocolInjectable from "../../electron-app/features/register-file-protocol.injectable"; -import staticFilesDirectoryInjectable from "../../../common/vars/static-files-directory.injectable"; -import { beforeApplicationIsLoadingInjectionToken } from "../runnable-tokens/before-application-is-loading-injection-token"; - -const setupFileProtocolInjectable = getInjectable({ - id: "setup-file-protocol", - - instantiate: (di) => { - const registerFileProtocol = di.inject(registerFileProtocolInjectable); - const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable); - - return { - run: () => { - registerFileProtocol("static", staticFilesDirectory); - }, - }; - }, - - injectionToken: beforeApplicationIsLoadingInjectionToken, -}); - -export default setupFileProtocolInjectable; From 6dc435054103c1c14a747b1e6a93a80cc69ce8e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 22:21:48 -0400 Subject: [PATCH 35/43] Bump @typescript-eslint/eslint-plugin from 5.26.0 to 5.27.0 (#5541) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.26.0 to 5.27.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.27.0/packages/eslint-plugin) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 80 ++++++++++++++++++++++++++-------------------------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index 12d03359ec..5120561c3b 100644 --- a/package.json +++ b/package.json @@ -343,7 +343,7 @@ "@types/webpack-dev-server": "^4.7.2", "@types/webpack-env": "^1.17.0", "@types/webpack-node-externals": "^2.5.3", - "@typescript-eslint/eslint-plugin": "^5.26.0", + "@typescript-eslint/eslint-plugin": "^5.27.0", "@typescript-eslint/parser": "^5.17.0", "ansi_up": "^5.1.0", "chart.js": "^2.9.4", diff --git a/yarn.lock b/yarn.lock index a578b70781..c6b3d5c27c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2304,14 +2304,14 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.26.0.tgz#c1f98ccba9d345e38992975d3ca56ed6260643c2" - integrity sha512-oGCmo0PqnRZZndr+KwvvAUvD3kNE4AfyoGCwOZpoCncSh4MVD06JTE8XQa2u9u+NX5CsyZMBTEc2C72zx38eYA== +"@typescript-eslint/eslint-plugin@^5.27.0": + version "5.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.0.tgz#23d82a4f21aaafd8f69dbab7e716323bb6695cc8" + integrity sha512-DDrIA7GXtmHXr1VCcx9HivA39eprYBIFxbQEHI6NyraRDxCGpxAFiYQAT/1Y0vh1C+o2vfBiy4IuPoXxtTZCAQ== dependencies: - "@typescript-eslint/scope-manager" "5.26.0" - "@typescript-eslint/type-utils" "5.26.0" - "@typescript-eslint/utils" "5.26.0" + "@typescript-eslint/scope-manager" "5.27.0" + "@typescript-eslint/type-utils" "5.27.0" + "@typescript-eslint/utils" "5.27.0" debug "^4.3.4" functional-red-black-tree "^1.0.1" ignore "^5.2.0" @@ -2337,20 +2337,20 @@ "@typescript-eslint/types" "5.23.0" "@typescript-eslint/visitor-keys" "5.23.0" -"@typescript-eslint/scope-manager@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.26.0.tgz#44209c7f649d1a120f0717e0e82da856e9871339" - integrity sha512-gVzTJUESuTwiju/7NiTb4c5oqod8xt5GhMbExKsCTp6adU3mya6AGJ4Pl9xC7x2DX9UYFsjImC0mA62BCY22Iw== +"@typescript-eslint/scope-manager@5.27.0": + version "5.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.27.0.tgz#a272178f613050ed62f51f69aae1e19e870a8bbb" + integrity sha512-VnykheBQ/sHd1Vt0LJ1JLrMH1GzHO+SzX6VTXuStISIsvRiurue/eRkTqSrG0CexHQgKG8shyJfR4o5VYioB9g== dependencies: - "@typescript-eslint/types" "5.26.0" - "@typescript-eslint/visitor-keys" "5.26.0" + "@typescript-eslint/types" "5.27.0" + "@typescript-eslint/visitor-keys" "5.27.0" -"@typescript-eslint/type-utils@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.26.0.tgz#937dee97702361744a3815c58991acf078230013" - integrity sha512-7ccbUVWGLmcRDSA1+ADkDBl5fP87EJt0fnijsMFTVHXKGduYMgienC/i3QwoVhDADUAPoytgjbZbCOMj4TY55A== +"@typescript-eslint/type-utils@5.27.0": + version "5.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.27.0.tgz#36fd95f6747412251d79c795b586ba766cf0974b" + integrity sha512-vpTvRRchaf628Hb/Xzfek+85o//zEUotr1SmexKvTfs7czXfYjXVT/a5yDbpzLBX1rhbqxjDdr1Gyo0x1Fc64g== dependencies: - "@typescript-eslint/utils" "5.26.0" + "@typescript-eslint/utils" "5.27.0" debug "^4.3.4" tsutils "^3.21.0" @@ -2359,10 +2359,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.23.0.tgz#8733de0f58ae0ed318dbdd8f09868cdbf9f9ad09" integrity sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw== -"@typescript-eslint/types@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.26.0.tgz#cb204bb154d3c103d9cc4d225f311b08219469f3" - integrity sha512-8794JZFE1RN4XaExLWLI2oSXsVImNkl79PzTOOWt9h0UHROwJedNOD2IJyfL0NbddFllcktGIO2aOu10avQQyA== +"@typescript-eslint/types@5.27.0": + version "5.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.27.0.tgz#c3f44b9dda6177a9554f94a74745ca495ba9c001" + integrity sha512-lY6C7oGm9a/GWhmUDOs3xAVRz4ty/XKlQ2fOLr8GAIryGn0+UBOoJDWyHer3UgrHkenorwvBnphhP+zPmzmw0A== "@typescript-eslint/typescript-estree@5.23.0": version "5.23.0" @@ -2377,28 +2377,28 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.26.0.tgz#16cbceedb0011c2ed4f607255f3ee1e6e43b88c3" - integrity sha512-EyGpw6eQDsfD6jIqmXP3rU5oHScZ51tL/cZgFbFBvWuCwrIptl+oueUZzSmLtxFuSOQ9vDcJIs+279gnJkfd1w== +"@typescript-eslint/typescript-estree@5.27.0": + version "5.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.0.tgz#7965f5b553c634c5354a47dcce0b40b94611e995" + integrity sha512-QywPMFvgZ+MHSLRofLI7BDL+UczFFHyj0vF5ibeChDAJgdTV8k4xgEwF0geFhVlPc1p8r70eYewzpo6ps+9LJQ== dependencies: - "@typescript-eslint/types" "5.26.0" - "@typescript-eslint/visitor-keys" "5.26.0" + "@typescript-eslint/types" "5.27.0" + "@typescript-eslint/visitor-keys" "5.27.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.26.0.tgz#896b8480eb124096e99c8b240460bb4298afcfb4" - integrity sha512-PJFwcTq2Pt4AMOKfe3zQOdez6InIDOjUJJD3v3LyEtxHGVVRK3Vo7Dd923t/4M9hSH2q2CLvcTdxlLPjcIk3eg== +"@typescript-eslint/utils@5.27.0": + version "5.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.27.0.tgz#d0021cbf686467a6a9499bd0589e19665f9f7e71" + integrity sha512-nZvCrkIJppym7cIbP3pOwIkAefXOmfGPnCM0LQfzNaKxJHI6VjI8NC662uoiPlaf5f6ymkTy9C3NQXev2mdXmA== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.26.0" - "@typescript-eslint/types" "5.26.0" - "@typescript-eslint/typescript-estree" "5.26.0" + "@typescript-eslint/scope-manager" "5.27.0" + "@typescript-eslint/types" "5.27.0" + "@typescript-eslint/typescript-estree" "5.27.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" @@ -2410,12 +2410,12 @@ "@typescript-eslint/types" "5.23.0" eslint-visitor-keys "^3.0.0" -"@typescript-eslint/visitor-keys@5.26.0": - version "5.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.26.0.tgz#7195f756e367f789c0e83035297c45b417b57f57" - integrity sha512-wei+ffqHanYDOQgg/fS6Hcar6wAWv0CUPQ3TZzOWd2BLfgP539rb49bwua8WRAs7R6kOSLn82rfEu2ro6Llt8Q== +"@typescript-eslint/visitor-keys@5.27.0": + version "5.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.0.tgz#97aa9a5d2f3df8215e6d3b77f9d214a24db269bd" + integrity sha512-46cYrteA2MrIAjv9ai44OQDUoCZyHeGIc4lsjCUX2WT6r4C+kidz1bNiR4017wHOPUythYeH+Sc7/cFP97KEAA== dependencies: - "@typescript-eslint/types" "5.26.0" + "@typescript-eslint/types" "5.27.0" eslint-visitor-keys "^3.3.0" "@webassemblyjs/ast@1.11.1": From 65d0bab0b42869ae4e7dfb2a1e54f3c9190ee578 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 22:22:58 -0400 Subject: [PATCH 36/43] Bump react-table and @types/react-table (#5542) Bumps [react-table](https://github.com/tannerlinsley/react-table) and [@types/react-table](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-table). These dependencies needed to be updated together. Updates `react-table` from 7.7.0 to 7.8.0 - [Release notes](https://github.com/tannerlinsley/react-table/releases) - [Commits](https://github.com/tannerlinsley/react-table/compare/v7.7.0...v7.8.0) Updates `@types/react-table` from 7.7.11 to 7.7.12 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-table) --- updated-dependencies: - dependency-name: react-table dependency-type: direct:development update-type: version-update:semver-minor - dependency-name: "@types/react-table" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 4 ++-- yarn.lock | 33 ++++++++++++--------------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 5120561c3b..c37383a83b 100644 --- a/package.json +++ b/package.json @@ -323,7 +323,7 @@ "@types/react-dom": "^17.0.16", "@types/react-router": "^5.1.18", "@types/react-router-dom": "^5.3.3", - "@types/react-table": "^7.7.11", + "@types/react-table": "^7.7.12", "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", "@types/readable-stream": "^2.3.13", @@ -393,7 +393,7 @@ "react-router-dom": "^5.3.3", "react-select": "^5.3.2", "react-select-event": "^5.5.0", - "react-table": "^7.7.0", + "react-table": "^7.8.0", "react-window": "^1.8.7", "sass": "^1.52.1", "sass-loader": "^12.6.0", diff --git a/yarn.lock b/yarn.lock index c6b3d5c27c..5b20e88f25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1968,10 +1968,10 @@ "@types/history" "^4.7.11" "@types/react" "*" -"@types/react-table@^7.7.11": - version "7.7.11" - resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.11.tgz#0efbb69aabf5b4b9c26c4c027b1e1ceb0f342303" - integrity sha512-Ntfr4EMWgqf/m/CxfmiHww5HvE1nOfK3yEm3NJ3ZWv9IkdteqTOklG3rJtFCtICKAkr3q5pqajkm0y1+WnmdbA== +"@types/react-table@^7.7.12": + version "7.7.12" + resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.12.tgz#628011d3cb695b07c678704a61f2f1d5b8e567fd" + integrity sha512-bRUent+NR/WwtDGwI/BqhZ8XnHghwHw0HUKeohzB5xN3K2qKWYE5w19e7GCuOkL1CXD9Gi1HFy7TIm2AvgWUHg== dependencies: "@types/react" "*" @@ -1996,10 +1996,10 @@ dependencies: "@types/react" "*" -"@types/react@*": - version "17.0.30" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.30.tgz#2f8e6f5ab6415c091cc5e571942ee9064b17609e" - integrity sha512-3Dt/A8gd3TCXi2aRe84y7cK1K8G+N9CZRDG8kDGguOKa0kf/ZkSwTmVIDPsm/KbQOVMaDJXwhBtuOXxqwdpWVg== +"@types/react@*", "@types/react@^17", "@types/react@^17.0.45": + version "17.0.45" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.45.tgz#9b3d5b661fd26365fefef0e766a1c6c30ccf7b3f" + integrity sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -2014,15 +2014,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@^17", "@types/react@^17.0.45": - version "17.0.45" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.45.tgz#9b3d5b661fd26365fefef0e766a1c6c30ccf7b3f" - integrity sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - "@types/readable-stream@^2.3.13": version "2.3.13" resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-2.3.13.tgz#46451c1b87cb61010e420ac02a76cfc1b2c2089a" @@ -11122,10 +11113,10 @@ react-swipeable@^6.1.0: resolved "https://registry.yarnpkg.com/react-swipeable/-/react-swipeable-6.2.2.tgz#52ba570f3a7a90db7093094ec476f3d151f727d1" integrity sha512-Oz7nSFrssvq2yvy05aNL3F+yBUqSvLsK6x1mu+rQFOpMdQVnt4izKt1vyjvvTb70q6GQOaSpaB6qniROW2MAzQ== -react-table@^7.7.0: - version "7.7.0" - resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.7.0.tgz#e2ce14d7fe3a559f7444e9ecfe8231ea8373f912" - integrity sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA== +react-table@^7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.8.0.tgz#07858c01c1718c09f7f1aed7034fcfd7bda907d2" + integrity sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA== react-transition-group@^4.3.0, react-transition-group@^4.4.0: version "4.4.2" From 833650195acca36741be082981fa56d7a5567051 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 22:23:22 -0400 Subject: [PATCH 37/43] Bump cli-progress and @types/cli-progress (#5543) Bumps [cli-progress](https://github.com/npkgz/cli-progress) and [@types/cli-progress](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/cli-progress). These dependencies needed to be updated together. Updates `cli-progress` from 3.11.0 to 3.11.1 - [Release notes](https://github.com/npkgz/cli-progress/releases) - [Changelog](https://github.com/npkgz/cli-progress/blob/master/CHANGES.md) - [Commits](https://github.com/npkgz/cli-progress/compare/v3.11.0...v3.11.1) Updates `@types/cli-progress` from 3.9.2 to 3.11.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/cli-progress) --- updated-dependencies: - dependency-name: cli-progress dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: "@types/cli-progress" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index c37383a83b..4daeb1172b 100644 --- a/package.json +++ b/package.json @@ -294,7 +294,7 @@ "@types/byline": "^4.2.33", "@types/chart.js": "^2.9.36", "@types/circular-dependency-plugin": "5.0.5", - "@types/cli-progress": "^3.9.2", + "@types/cli-progress": "^3.11.0", "@types/color": "^3.0.3", "@types/command-line-args": "^5.2.0", "@types/crypto-js": "^3.1.47", @@ -348,7 +348,7 @@ "ansi_up": "^5.1.0", "chart.js": "^2.9.4", "circular-dependency-plugin": "^5.2.2", - "cli-progress": "^3.11.0", + "cli-progress": "^3.11.1", "color": "^3.2.1", "command-line-args": "^5.2.1", "concurrently": "^7.2.1", diff --git a/yarn.lock b/yarn.lock index 5b20e88f25..82bcdcd8b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1499,10 +1499,10 @@ "@types/node" "*" source-map "^0.6.0" -"@types/cli-progress@^3.9.2": - version "3.9.2" - resolved "https://registry.yarnpkg.com/@types/cli-progress/-/cli-progress-3.9.2.tgz#6ca355f96268af39bee9f9307f0ac96145639c26" - integrity sha512-VO5/X5Ij+oVgEVjg5u0IXVe3JQSKJX+Ev8C5x+0hPy0AuWyW+bF8tbajR7cPFnDGhs7pidztcac+ccrDtk5teA== +"@types/cli-progress@^3.11.0": + version "3.11.0" + resolved "https://registry.yarnpkg.com/@types/cli-progress/-/cli-progress-3.11.0.tgz#ec79df99b26757c3d1c7170af8422e0fc95eef7e" + integrity sha512-XhXhBv1R/q2ahF3BM7qT5HLzJNlIL0wbcGyZVjqOTqAybAnsLisd7gy1UCyIqpL+5Iv6XhlSyzjLCnI2sIdbCg== dependencies: "@types/node" "*" @@ -3762,10 +3762,10 @@ cli-columns@^3.1.2: string-width "^2.0.0" strip-ansi "^3.0.1" -cli-progress@^3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.11.0.tgz#03651defd06182a5396ddc2a41da17c2f257ecdf" - integrity sha512-ug+V4/Qy3+0jX9XkWPV/AwHD98RxKXqDpL37vJBOxQhD90qQ3rDqDKoFpef9se91iTUuOXKlyg2HUyHBo5lHsQ== +cli-progress@^3.11.1: + version "3.11.1" + resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.11.1.tgz#02afb11be9a123f2a302931beb087eafe3a0d971" + integrity sha512-TTMA2LHrYaZeNMcgZGO10oYqj9hvd03pltNtVbu4ddeyDTHlYV7gWxsFiuvaQlgwMBFCv1TukcjiODWFlb16tQ== dependencies: string-width "^4.2.3" From b86d409c925bcb8eb98f148cffab62ae961e3f70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 22:23:57 -0400 Subject: [PATCH 38/43] Bump mobx-react from 7.4.0 to 7.5.0 (#5544) Bumps [mobx-react](https://github.com/mobxjs/mobx) from 7.4.0 to 7.5.0. - [Release notes](https://github.com/mobxjs/mobx/releases) - [Commits](https://github.com/mobxjs/mobx/compare/mobx-react@7.4.0...mobx-react@7.5.0) --- updated-dependencies: - dependency-name: mobx-react dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4daeb1172b..36141a0ba3 100644 --- a/package.json +++ b/package.json @@ -242,7 +242,7 @@ "md5-file": "^5.0.0", "mobx": "^6.5.0", "mobx-observable-history": "^2.0.3", - "mobx-react": "^7.3.0", + "mobx-react": "^7.5.0", "mobx-utils": "^6.0.4", "mock-fs": "^5.1.2", "moment": "^2.29.3", diff --git a/yarn.lock b/yarn.lock index 82bcdcd8b1..6884aee6b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9284,10 +9284,10 @@ mobx-react-lite@^3.4.0: resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.4.0.tgz#d59156a96889cdadad751e5e4dab95f28926dfff" integrity sha512-bRuZp3C0itgLKHu/VNxi66DN/XVkQG7xtoBVWxpvC5FhAqbOCP21+nPhULjnzEqd7xBMybp6KwytdUpZKEgpIQ== -mobx-react@^7.3.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-7.4.0.tgz#29bbdc609c7f6c43e5af6c8e984c8b93a7fe08e8" - integrity sha512-gbUwaKZK09SiAleTMxNMKs1MYKTpoIEWJLTLRIR/xnALuuHET8wkL8j1nbc1/6cDkOWVyKz/ReftILx0Pdh2PQ== +mobx-react@^7.5.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-7.5.0.tgz#8da71f8d3c96409cf178112503ec50467e5cea70" + integrity sha512-riHu0XZJA6f64L1iXZoAaDjVt6suYoy8I2HIfuz2tX3O4FFaAe4lVA2CoObttmUQTTFPM7j3Df6T4re0cHkghQ== dependencies: mobx-react-lite "^3.4.0" From d4fbab71768deb22eafb5c0f57e5ea3027102827 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 2 Jun 2022 08:59:23 -0400 Subject: [PATCH 39/43] Remove legacy renderBooleans prop (#5483) --- .../custom-resource-definition.api.ts | 12 +- .../endpoints/types/external-documentation.ts | 9 + .../endpoints/types/json-schema-props.ts | 93 ++++++++ .../custom-resource-details.test.tsx.snap | 225 ++++++++++++++++++ .../custom-resource-details.test.tsx | 206 ++++++++++++++++ .../crd-resource-details.tsx | 18 +- .../components/drawer/drawer-item.tsx | 13 +- src/renderer/components/table/table-cell.tsx | 10 +- .../utils/__tests__/display-booleans.test.tsx | 23 -- src/renderer/utils/display-booleans.ts | 20 -- src/renderer/utils/index.ts | 1 - 11 files changed, 564 insertions(+), 66 deletions(-) create mode 100644 src/common/k8s-api/endpoints/types/external-documentation.ts create mode 100644 src/common/k8s-api/endpoints/types/json-schema-props.ts create mode 100644 src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap create mode 100644 src/renderer/components/+custom-resources/__tests__/custom-resource-details.test.tsx delete mode 100644 src/renderer/utils/__tests__/display-booleans.test.tsx delete mode 100644 src/renderer/utils/display-booleans.ts diff --git a/src/common/k8s-api/endpoints/custom-resource-definition.api.ts b/src/common/k8s-api/endpoints/custom-resource-definition.api.ts index 03d0903f71..90485a5ab8 100644 --- a/src/common/k8s-api/endpoints/custom-resource-definition.api.ts +++ b/src/common/k8s-api/endpoints/custom-resource-definition.api.ts @@ -10,12 +10,14 @@ import type { BaseKubeObjectCondition, KubeObjectScope } from "../kube-object"; import { KubeObject } from "../kube-object"; import type { DerivedKubeApiOptions } from "../kube-api"; import { KubeApi } from "../kube-api"; +import type { JSONSchemaProps } from "./types/json-schema-props"; interface AdditionalPrinterColumnsCommon { name: string; type: "integer" | "number" | "string" | "boolean" | "date"; - priority: number; - description: string; + priority?: number; + format?: "int32" | "int64" | "float" | "double" | "byte" | "binary" | "date" | "date-time" | "password"; + description?: string; } export type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & { @@ -26,11 +28,15 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & { JSONPath: string; }; +export interface CustomResourceValidation { + openAPIV3Schema?: JSONSchemaProps; +} + export interface CustomResourceDefinitionVersion { name: string; served: boolean; storage: boolean; - schema?: object; // required in v1 but not present in v1beta + schema?: CustomResourceValidation; // required in v1 but not present in v1beta additionalPrinterColumns?: AdditionalPrinterColumnsV1[]; } diff --git a/src/common/k8s-api/endpoints/types/external-documentation.ts b/src/common/k8s-api/endpoints/types/external-documentation.ts new file mode 100644 index 0000000000..b433785323 --- /dev/null +++ b/src/common/k8s-api/endpoints/types/external-documentation.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export interface ExternalDocumentation { + description?: string; + url?: string; +} diff --git a/src/common/k8s-api/endpoints/types/json-schema-props.ts b/src/common/k8s-api/endpoints/types/json-schema-props.ts new file mode 100644 index 0000000000..1c0f18a7d2 --- /dev/null +++ b/src/common/k8s-api/endpoints/types/json-schema-props.ts @@ -0,0 +1,93 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { JsonValue } from "type-fest"; +import type { ExternalDocumentation } from "./external-documentation"; + +export interface JSONSchemaProps { + $ref?: string; + $schema?: string; + additionalItems?: JSONSchemaProps | boolean; + additionalProperties?: JSONSchemaProps | boolean; + allOf?: JSONSchemaProps[]; + anyOf?: JSONSchemaProps[]; + + /** + * default is a default value for undefined object fields. + * Defaulting is a beta feature under the CustomResourceDefaulting feature gate. + * Defaulting requires spec.preserveUnknownFields to be false. + */ + _default?: object; + + definitions?: Partial>; + dependencies?: Partial>; + description?: string; + _enum?: object[]; + example?: JsonValue; + + exclusiveMaximum?: boolean; + exclusiveMinimum?: boolean; + externalDocs?: ExternalDocumentation; + + /** + * format is an OpenAPI v3 format string. + * Unknown formats are ignored. + * + * The following formats are validated: + * - bsonobjectid: a bson object ID, i.e. a 24 characters hex string + * - uri: an URI as parsed by Golang net/url.ParseRequestURI + * - email: an email address as parsed by Golang net/mail.ParseAddress + * - hostname: a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034]. + * - ipv4: an IPv4 IP as parsed by Golang net.ParseIP + * - ipv6: an IPv6 IP as parsed by Golang net.ParseIP + * - cidr: a CIDR as parsed by Golang net.ParseCIDR + * - mac: a MAC address as parsed by Golang net.ParseMAC + * - uuid: an UUID that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + * - uuid3: an UUID3 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + * - uuid4: an UUID4 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + * - uuid5: an UUID5 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + * - isbn: an ISBN10 or ISBN13 number string like "0321751043" or "978-0321751041" + * - isbn10: an ISBN10 number string like "0321751043" + * - isbn13: an ISBN13 number string like "978-0321751041" + * - creditcard: a credit card number defined by the regex ^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$ with any non digit characters mixed in + * - ssn: a U.S. social security number following the regex ^\\d{3}[- ]?\\d{2}[- ]?\\d{4}$ + * - hexcolor: an hexadecimal color code like "#FFFFFF: following the regex ^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ + * - rgbcolor: an RGB color code like rgb like "rgb(255,255,2559" + * - byte: base64 encoded binary data + * - password: any kind of string + * - date: a date string like "2006-01-02" as defined by full-date in RFC3339 + * - duration: a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration format + * - datetime: a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339. + */ + format?: string; + + id?: string; + items?: JSONSchemaProps | JSONSchemaProps[]; + maxItems?: number; + maxLength?: number; + maxProperties?: number; + maximum?: number; + minItems?: number; + minLength?: number; + minProperties?: number; + minimum?: number; + multipleOf?: number; + not?: JSONSchemaProps; + nullable?: boolean; + oneOf?: JSONSchemaProps[]; + pattern?: string; + patternProperties?: Partial>; + properties?: Partial>; + required?: Array; + title?: string; + type?: string; + uniqueItems?: boolean; + x_kubernetes_embedded_resource?: boolean; + x_kubernetes_int_or_string?: boolean; + x_kubernetes_list_map_keys?: string[]; + x_kubernetes_list_type?: string; + x_kubernetes_map_type?: string; + x_kubernetes_preserve_unknown_fields?: boolean; +} diff --git a/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap b/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap new file mode 100644 index 0000000000..d3e0cd1d5a --- /dev/null +++ b/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap @@ -0,0 +1,225 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` with a CRD with a boolean field should display false in an additionalPrinterColumn as 'false' 1`] = ` +

    +
    +
    + + Created + + + <unknown> + ago + +
    +
    + + Name + + + first-crd + +
    +
    + + MyField + + +
      +
    • + false +
    • +
    +
    +
    +
    +
    +`; + +exports[` with a CRD with a boolean field should display true in an additionalPrinterColumn as 'true' 1`] = ` +
    +
    +
    + + Created + + + <unknown> + ago + +
    +
    + + Name + + + first-crd + +
    +
    + + MyField + + +
      +
    • + true +
    • +
    +
    +
    +
    +
    +`; + +exports[` with a CRD with a number field should display 0 in an additionalPrinterColumn as '0' 1`] = ` +
    +
    +
    + + Created + + + <unknown> + ago + +
    +
    + + Name + + + first-crd + +
    +
    + + MyField + + +
      +
    • + 0 +
    • +
    +
    +
    +
    +
    +`; + +exports[` with a CRD with a number field should display 1234 in an additionalPrinterColumn as '1234' 1`] = ` +
    +
    +
    + + Created + + + <unknown> + ago + +
    +
    + + Name + + + first-crd + +
    +
    + + MyField + + +
      +
    • + 1234 +
    • +
    +
    +
    +
    +
    +`; diff --git a/src/renderer/components/+custom-resources/__tests__/custom-resource-details.test.tsx b/src/renderer/components/+custom-resources/__tests__/custom-resource-details.test.tsx new file mode 100644 index 0000000000..2b8e8b2b1e --- /dev/null +++ b/src/renderer/components/+custom-resources/__tests__/custom-resource-details.test.tsx @@ -0,0 +1,206 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import React from "react"; +import { CustomResourceDefinition } from "../../../../common/k8s-api/endpoints"; +import { KubeObject } from "../../../../common/k8s-api/kube-object"; +import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; +import type { DiRender } from "../../test-utils/renderFor"; +import { renderFor } from "../../test-utils/renderFor"; +import { CustomResourceDetails } from "../crd-resource-details"; + +describe("", () => { + let render: DiRender; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + render = renderFor(di); + }); + + describe("with a CRD with a boolean field", () => { + let crd: CustomResourceDefinition; + + beforeEach(() => { + crd = new CustomResourceDefinition({ + apiVersion: "apiextensions.k8s.io/v1", + kind: "CustomResourceDefinition", + metadata: { + name: "my-crd", + resourceVersion: "1", + selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/my-crd", + uid: "1", + }, + spec: { + versions: [{ + name: "v1", + served: true, + storage: true, + schema: { + openAPIV3Schema: { + type: "object", + properties: { + spec: { + type: "object", + properties: { + "my-field": { + type: "boolean", + }, + }, + }, + }, + }, + }, + additionalPrinterColumns: [ + { + name: "MyField", + jsonPath: ".spec.my-field", + type: "boolean", + }, + ], + }], + group: "stable.lens.dev", + names: { + kind: "MyCrd", + plural: "my-crds", + }, + scope: "Cluster", + }, + }); + }); + + it("should display false in an additionalPrinterColumn as 'false'", () => { + const cr = new KubeObject({ + apiVersion: "stable.lens.dev/v1", + kind: "MyCrd", + metadata: { + name: "first-crd", + resourceVersion: "1", + selfLink: "stable.lens.dev/v1/first-crd", + uid: "2", + }, + spec: { + "my-field": false, + }, + }); + const result = render(); + + expect(result.container).toMatchSnapshot(); + expect(result.getByText("false")).toBeTruthy(); + }); + + it("should display true in an additionalPrinterColumn as 'true'", () => { + const cr = new KubeObject({ + apiVersion: "stable.lens.dev/v1", + kind: "MyCrd", + metadata: { + name: "first-crd", + resourceVersion: "1", + selfLink: "stable.lens.dev/v1/first-crd", + uid: "2", + }, + spec: { + "my-field": true, + }, + }); + const result = render(); + + expect(result.container).toMatchSnapshot(); + expect(result.getByText("true")).toBeTruthy(); + }); + }); + + describe("with a CRD with a number field", () => { + let crd: CustomResourceDefinition; + + beforeEach(() => { + crd = new CustomResourceDefinition({ + apiVersion: "apiextensions.k8s.io/v1", + kind: "CustomResourceDefinition", + metadata: { + name: "my-crd", + resourceVersion: "1", + selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/my-crd", + uid: "1", + }, + spec: { + versions: [{ + name: "v1", + served: true, + storage: true, + schema: { + openAPIV3Schema: { + type: "object", + properties: { + spec: { + type: "object", + properties: { + "my-field": { + type: "number", + }, + }, + }, + }, + }, + }, + additionalPrinterColumns: [ + { + name: "MyField", + jsonPath: ".spec.my-field", + type: "number", + }, + ], + }], + group: "stable.lens.dev", + names: { + kind: "MyCrd", + plural: "my-crds", + }, + scope: "Cluster", + }, + }); + }); + + it("should display 0 in an additionalPrinterColumn as '0'", () => { + const cr = new KubeObject({ + apiVersion: "stable.lens.dev/v1", + kind: "MyCrd", + metadata: { + name: "first-crd", + resourceVersion: "1", + selfLink: "stable.lens.dev/v1/first-crd", + uid: "2", + }, + spec: { + "my-field": 0, + }, + }); + const result = render(); + + expect(result.container).toMatchSnapshot(); + expect(result.getByText("0")).toBeTruthy(); + }); + + it("should display 1234 in an additionalPrinterColumn as '1234'", () => { + const cr = new KubeObject({ + apiVersion: "stable.lens.dev/v1", + kind: "MyCrd", + metadata: { + name: "first-crd", + resourceVersion: "1", + selfLink: "stable.lens.dev/v1/first-crd", + uid: "2", + }, + spec: { + "my-field": 1234, + }, + }); + const result = render(); + + expect(result.container).toMatchSnapshot(); + expect(result.getByText("1234")).toBeTruthy(); + }); + }); +}); diff --git a/src/renderer/components/+custom-resources/crd-resource-details.tsx b/src/renderer/components/+custom-resources/crd-resource-details.tsx index 36c06de0e3..48049682e4 100644 --- a/src/renderer/components/+custom-resources/crd-resource-details.tsx +++ b/src/renderer/components/+custom-resources/crd-resource-details.tsx @@ -25,7 +25,7 @@ export interface CustomResourceDetailsProps extends KubeObjectDetailsProps @@ -50,18 +50,22 @@ function convertSpecValue(value: any): any { ); } - return value; + if ( + typeof value === "boolean" + || typeof value === "string" + || typeof value === "number" + ) { + return value.toString(); + } + + return null; } @observer export class CustomResourceDetails extends React.Component { renderAdditionalColumns(resource: KubeObject, columns: AdditionalPrinterColumnsV1[]) { return columns.map(({ name, jsonPath }) => ( - + {convertSpecValue(JSONPath.query(resource, convertKubectlJsonPathToNodeJsonPath(jsonPath)))} )); diff --git a/src/renderer/components/drawer/drawer-item.tsx b/src/renderer/components/drawer/drawer-item.tsx index 737749f783..72a311794c 100644 --- a/src/renderer/components/drawer/drawer-item.tsx +++ b/src/renderer/components/drawer/drawer-item.tsx @@ -5,14 +5,20 @@ import "./drawer-item.scss"; import React from "react"; -import { cssNames, displayBooleans } from "../../utils"; +import { cssNames } from "../../utils"; export interface DrawerItemProps extends React.HTMLAttributes { name: React.ReactNode; title?: string; labelsOnly?: boolean; hidden?: boolean; - renderBoolean?: boolean; // show "true" or "false" for all of the children elements are "typeof boolean" + + /** + * @deprecated This prop is no longer used, you should stringify the booleans yourself. + * + * This was only meant to be an internal prop anyway. + */ + renderBooleans?: boolean; } export function DrawerItem({ @@ -22,7 +28,6 @@ export function DrawerItem({ children, hidden = false, className, - renderBoolean, ...elemProps }: DrawerItemProps) { if (hidden) { @@ -36,7 +41,7 @@ export function DrawerItem({ title={title} > {name} - {displayBooleans(renderBoolean, children)} + {children} ); } diff --git a/src/renderer/components/table/table-cell.tsx b/src/renderer/components/table/table-cell.tsx index a002923e40..758c5cd951 100644 --- a/src/renderer/components/table/table-cell.tsx +++ b/src/renderer/components/table/table-cell.tsx @@ -8,7 +8,7 @@ import type { TableSortBy, TableSortParams } from "./table"; import type { ReactNode } from "react"; import React from "react"; -import { autoBind, cssNames, displayBooleans } from "../../utils"; +import { autoBind, cssNames } from "../../utils"; import { Icon } from "../icon"; import { Checkbox } from "../checkbox"; @@ -45,11 +45,6 @@ export interface TableCellProps extends React.DOMAttributes { */ isChecked?: boolean; - /** - * show "true" or "false" for all of the children elements are "typeof boolean" - */ - renderBoolean?: boolean; - /** * column name, must be same as key in sortable object */ @@ -136,7 +131,6 @@ export class TableCell extends React.Component { _nowrap, children, title, - renderBoolean: displayBoolean = false, showWithColumn, ...cellProps } = this.props; @@ -147,7 +141,7 @@ export class TableCell extends React.Component { nowrap: _nowrap, sorting: _sort && typeof sortBy === "string", }); - const content = displayBooleans(displayBoolean, title || children); + const content = title || children; return (
    { - it("should not do anything to div's if shouldShow is false", () => { - expect(displayBooleans(false,
    )).toStrictEqual(
    ); - }); - - it("should not do anything to booleans's if shouldShow is false", () => { - expect(displayBooleans(false, true)).toStrictEqual(true); - expect(displayBooleans(false, false)).toStrictEqual(false); - }); - - it("should stringify booleans when shouldShow is true", () => { - expect(displayBooleans(true, true)).toStrictEqual("true"); - expect(displayBooleans(true, false)).toStrictEqual("false"); - }); -}); diff --git a/src/renderer/utils/display-booleans.ts b/src/renderer/utils/display-booleans.ts deleted file mode 100644 index fc92004295..0000000000 --- a/src/renderer/utils/display-booleans.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 type React from "react"; - -export function displayBooleans(shouldShow: boolean | undefined, from: React.ReactNode): React.ReactNode { - if (shouldShow) { - if (typeof from === "boolean") { - return from.toString(); - } - - if (Array.isArray(from)) { - return from.map(node => displayBooleans(shouldShow, node)); - } - } - - return from; -} diff --git a/src/renderer/utils/index.ts b/src/renderer/utils/index.ts index fee7dfc56d..b4e315be17 100755 --- a/src/renderer/utils/index.ts +++ b/src/renderer/utils/index.ts @@ -9,7 +9,6 @@ export * from "../../common/event-emitter"; export * from "./cssNames"; export * from "./cssVar"; -export * from "./display-booleans"; export * from "./display-mode"; export * from "./interval"; export * from "./isMiddleClick"; From ac27077ef9038d4e986e06d7999b9d3967f2a4b1 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 2 Jun 2022 08:59:38 -0400 Subject: [PATCH 40/43] Fix *.modules.scss that don't get correctly typed (#5532) --- src/renderer/components/+add-cluster/add-cluster.module.scss | 2 ++ .../components/kubeconfig-dialog/kubeconfig-dialog.module.scss | 2 ++ src/renderer/components/switch/switch.module.scss | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/renderer/components/+add-cluster/add-cluster.module.scss b/src/renderer/components/+add-cluster/add-cluster.module.scss index 7418b4481d..0a43b2b805 100644 --- a/src/renderer/components/+add-cluster/add-cluster.module.scss +++ b/src/renderer/components/+add-cluster/add-cluster.module.scss @@ -3,6 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +@import "../../components/mixins.scss"; + .AddClusters { --flex-gap: calc(var(--unit) * 2); diff --git a/src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.module.scss b/src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.module.scss index c93374b2f7..c97c926a8f 100644 --- a/src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.module.scss +++ b/src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.module.scss @@ -3,6 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +@import "../../components/mixins.scss"; + .KubeConfigDialog { :global(.Wizard) { width: 50vw; diff --git a/src/renderer/components/switch/switch.module.scss b/src/renderer/components/switch/switch.module.scss index 0461716200..217ef5dc9c 100644 --- a/src/renderer/components/switch/switch.module.scss +++ b/src/renderer/components/switch/switch.module.scss @@ -3,6 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +@import "../../components/mixins.scss"; + .Switch { --thumb-size: 2rem; --thumb-color: hsl(0 0% 100%); From e1c1e00a2bbc843d499ea37d3f390d7f89bfdb81 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 2 Jun 2022 18:40:30 -0400 Subject: [PATCH 41/43] Fix downloading cluster specific kubectl (#5399) --- src/common/__tests__/cluster-store.test.ts | 6 +++ ...rectory-for-bundled-binaries.injectable.ts | 13 ------ src/common/vars.ts | 16 +++---- .../base-bundled-binaries-dir.injectable.ts | 18 ++++++++ ...led-binaries-normalized-arch.injectable.ts | 27 ++++++++++++ .../vars/bundled-resources-dir.injectable.ts | 22 ++++++++++ .../vars/normalized-platform.injectable.ts | 24 ++++++++++ src/main/__test__/cluster.test.ts | 6 +++ src/main/__test__/kube-auth-proxy.test.ts | 6 +++ src/main/__test__/kubeconfig-manager.test.ts | 6 +++ .../__test__/kubeconfig-sync.test.ts | 6 +++ src/main/getDiForUnitTesting.ts | 9 ++-- .../create-kube-auth-proxy.injectable.ts | 4 +- src/main/kubectl/binary-name.injectable.ts | 19 ++++++++ .../kubectl/bundled-binary-path.injectable.ts | 18 ++++++++ src/main/kubectl/create-kubectl.injectable.ts | 21 ++++++--- src/main/kubectl/kubectl.ts | 44 ++++++++++--------- .../kubectl/normalized-arch.injectable.ts | 27 ++++++++++++ src/main/router/router.test.ts | 6 +++ .../local-shell-session.ts | 4 +- src/main/shell-session/shell-session.ts | 2 +- .../__tests__/delete-cluster-dialog.test.tsx | 10 ++++- 22 files changed, 251 insertions(+), 63 deletions(-) delete mode 100644 src/common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable.ts create mode 100644 src/common/vars/base-bundled-binaries-dir.injectable.ts create mode 100644 src/common/vars/bundled-binaries-normalized-arch.injectable.ts create mode 100644 src/common/vars/bundled-resources-dir.injectable.ts create mode 100644 src/common/vars/normalized-platform.injectable.ts create mode 100644 src/main/kubectl/binary-name.injectable.ts create mode 100644 src/main/kubectl/bundled-binary-path.injectable.ts create mode 100644 src/main/kubectl/normalized-arch.injectable.ts diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 4f836d5566..82d7b97638 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -22,6 +22,9 @@ import getConfigurationFileModelInjectable from "../get-configuration-file-model import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable"; import assert from "assert"; import directoryForTempInjectable from "../app-paths/directory-for-temp/directory-for-temp.injectable"; +import kubectlBinaryNameInjectable from "../../main/kubectl/binary-name.injectable"; +import kubectlDownloadingNormalizedArchInjectable from "../../main/kubectl/normalized-arch.injectable"; +import normalizedPlatformInjectable from "../vars/normalized-platform.injectable"; console = new Console(stdout, stderr); @@ -84,6 +87,9 @@ describe("cluster-store", () => { mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); mainDi.override(directoryForTempInjectable, () => "some-temp-directory"); + mainDi.override(kubectlBinaryNameInjectable, () => "kubectl"); + mainDi.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); + mainDi.override(normalizedPlatformInjectable, () => "darwin"); mainDi.permitSideEffects(getConfigurationFileModelInjectable); mainDi.permitSideEffects(appVersionInjectable); diff --git a/src/common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable.ts b/src/common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable.ts deleted file mode 100644 index 9c25407d4d..0000000000 --- a/src/common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable.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 { getInjectable } from "@ogre-tools/injectable"; -import { baseBinariesDir } from "../../vars"; - -const directoryForBundledBinariesInjectable = getInjectable({ - id: "directory-for-bundled-binaries", - instantiate: () => baseBinariesDir.get(), -}); - -export default directoryForBundledBinariesInjectable; diff --git a/src/common/vars.ts b/src/common/vars.ts index efbf32ee67..1d47eac9d8 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -57,6 +57,9 @@ export const defaultThemeId: ThemeId = "lens-dark"; export const defaultFontSize = 12; export const defaultTerminalFontFamily = "RobotoMono"; export const defaultEditorFontFamily = "RobotoMono"; +/** + * @deprecated use `di.inject(normalizedPlatformInjectable)` instead + */ export const normalizedPlatform = (() => { switch (process.platform) { case "darwin": @@ -69,6 +72,9 @@ export const normalizedPlatform = (() => { throw new Error(`platform=${process.platform} is unsupported`); } })(); +/** + * @deprecated use `di.inject(bundledBinariesNormalizedArchInjectable)` instead + */ export const normalizedArch = (() => { switch (process.arch) { case "arm64": @@ -119,16 +125,6 @@ export const helmBinaryName = getBinaryName("helm"); */ export const helmBinaryPath = lazyInitialized(() => path.join(baseBinariesDir.get(), helmBinaryName)); -/** - * @deprecated for being explicit side effect. - */ -export const kubectlBinaryName = getBinaryName("kubectl"); - -/** - * @deprecated for being explicit side effect. - */ -export const kubectlBinaryPath = lazyInitialized(() => path.join(baseBinariesDir.get(), kubectlBinaryName)); - // Apis export const apiPrefix = "/api"; // local router apis export const apiKubePrefix = "/api-kube"; // k8s cluster apis diff --git a/src/common/vars/base-bundled-binaries-dir.injectable.ts b/src/common/vars/base-bundled-binaries-dir.injectable.ts new file mode 100644 index 0000000000..41f5a5e0a3 --- /dev/null +++ b/src/common/vars/base-bundled-binaries-dir.injectable.ts @@ -0,0 +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 path from "path"; +import bundledBinariesNormalizedArchInjectable from "./bundled-binaries-normalized-arch.injectable"; +import bundledResourcesDirectoryInjectable from "./bundled-resources-dir.injectable"; + +const baseBundeledBinariesDirectoryInjectable = getInjectable({ + id: "base-bundeled-binaries-directory", + instantiate: (di) => path.join( + di.inject(bundledResourcesDirectoryInjectable), + di.inject(bundledBinariesNormalizedArchInjectable), + ), +}); + +export default baseBundeledBinariesDirectoryInjectable; diff --git a/src/common/vars/bundled-binaries-normalized-arch.injectable.ts b/src/common/vars/bundled-binaries-normalized-arch.injectable.ts new file mode 100644 index 0000000000..3c838d626c --- /dev/null +++ b/src/common/vars/bundled-binaries-normalized-arch.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"; + +const bundledBinariesNormalizedArchInjectable = getInjectable({ + id: "bundled-binaries-normalized-arch", + instantiate: () => { + switch (process.arch) { + case "arm64": + return "arm64"; + case "x64": + case "amd64": + return "x64"; + case "386": + case "x32": + case "ia32": + return "ia32"; + default: + throw new Error(`arch=${process.arch} is unsupported`); + } + }, + causesSideEffects: true, +}); + +export default bundledBinariesNormalizedArchInjectable; diff --git a/src/common/vars/bundled-resources-dir.injectable.ts b/src/common/vars/bundled-resources-dir.injectable.ts new file mode 100644 index 0000000000..a73ff98e78 --- /dev/null +++ b/src/common/vars/bundled-resources-dir.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 path from "path"; +import isProductionInjectable from "./is-production.injectable"; +import normalizedPlatformInjectable from "./normalized-platform.injectable"; + +const bundledResourcesDirectoryInjectable = getInjectable({ + id: "bundled-resources-directory", + instantiate: (di) => { + const isProduction = di.inject(isProductionInjectable); + const normalizedPlatform = di.inject(normalizedPlatformInjectable); + + return isProduction + ? process.resourcesPath + : path.join(process.cwd(), "binaries", "client", normalizedPlatform); + }, +}); + +export default bundledResourcesDirectoryInjectable; diff --git a/src/common/vars/normalized-platform.injectable.ts b/src/common/vars/normalized-platform.injectable.ts new file mode 100644 index 0000000000..7177678407 --- /dev/null +++ b/src/common/vars/normalized-platform.injectable.ts @@ -0,0 +1,24 @@ +/** + * 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"; + +const normalizedPlatformInjectable = getInjectable({ + id: "normalized-platform", + instantiate: () => { + switch (process.platform) { + case "darwin": + return "darwin"; + case "linux": + return "linux"; + case "win32": + return "windows"; + default: + throw new Error(`platform=${process.platform} is unsupported`); + } + }, + causesSideEffects: true, +}); + +export default normalizedPlatformInjectable; diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts index 0d76513a8f..4086adefbd 100644 --- a/src/main/__test__/cluster.test.ts +++ b/src/main/__test__/cluster.test.ts @@ -21,6 +21,9 @@ import type { ClusterContextHandler } from "../context-handler/context-handler"; import { parse } from "url"; 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"; +import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; +import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; console = new Console(process.stdout, process.stderr); // fix mockFS @@ -59,6 +62,9 @@ describe("create clusters", () => { di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForTempInjectable, () => "some-directory-for-temp"); + di.override(kubectlBinaryNameInjectable, () => "kubectl"); + di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); + di.override(normalizedPlatformInjectable, () => "darwin"); di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true)); di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); di.override(createContextHandlerInjectable, () => (cluster) => ({ diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index d08dadaa4e..298c8f351b 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -59,6 +59,9 @@ import getConfigurationFileModelInjectable from "../../common/get-configuration- import appVersionInjectable from "../../common/get-configuration-file-model/app-version/app-version.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"; +import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; +import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; console = new Console(stdout, stderr); @@ -105,6 +108,9 @@ describe("kube auth proxy tests", () => { di.override(directoryForTempInjectable, () => "some-directory-for-temp"); di.override(spawnInjectable, () => mockSpawn); + di.override(kubectlBinaryNameInjectable, () => "kubectl"); + di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); + di.override(normalizedPlatformInjectable, () => "darwin"); di.permitSideEffects(getConfigurationFileModelInjectable); di.permitSideEffects(appVersionInjectable); diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts index 436a815686..604608bb25 100644 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -20,6 +20,9 @@ import loggerInjectable from "../../common/logger.injectable"; import type { Logger } from "../../common/logger"; import assert from "assert"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; +import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; +import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; console = new Console(process.stdout, process.stderr); // fix mockFS @@ -34,6 +37,9 @@ describe("kubeconfig manager tests", () => { di.override(directoryForTempInjectable, () => "some-directory-for-temp"); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + di.override(kubectlBinaryNameInjectable, () => "kubectl"); + di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); + di.override(normalizedPlatformInjectable, () => "darwin"); loggerMock = { warn: jest.fn(), diff --git a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts index 55f318697b..5946a113a0 100644 --- a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts +++ b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts @@ -20,6 +20,9 @@ import appVersionInjectable from "../../../common/get-configuration-file-model/a import clusterManagerInjectable from "../../cluster-manager.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 kubectlBinaryNameInjectable from "../../kubectl/binary-name.injectable"; +import kubectlDownloadingNormalizedArchInjectable from "../../kubectl/normalized-arch.injectable"; +import normalizedPlatformInjectable from "../../../common/vars/normalized-platform.injectable"; jest.mock("electron", () => ({ app: { @@ -47,6 +50,9 @@ describe("kubeconfig-sync.source tests", () => { di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForTempInjectable, () => "some-directory-for-temp"); + di.override(kubectlBinaryNameInjectable, () => "kubectl"); + di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); + di.override(normalizedPlatformInjectable, () => "darwin"); di.override(clusterStoreInjectable, () => ClusterStore.createInstance({ createCluster: () => null as never }), diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 50b5b05e3d..5889e30090 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -4,7 +4,7 @@ */ import glob from "glob"; -import { kebabCase, memoize, noop } from "lodash/fp"; +import { kebabCase, memoize } from "lodash/fp"; import type { DiContainer } from "@ogre-tools/injectable"; import { createContainer } from "@ogre-tools/injectable"; import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; @@ -13,7 +13,6 @@ import registerChannelInjectable from "./app-paths/register-channel/register-cha import writeJsonFileInjectable from "../common/fs/write-json-file.injectable"; import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; import readFileInjectable from "../common/fs/read-file.injectable"; -import directoryForBundledBinariesInjectable from "../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable"; import loggerInjectable from "../common/logger.injectable"; import spawnInjectable from "./child-process/spawn.injectable"; import extensionsStoreInjectable from "../extensions/extensions-store/extensions-store.injectable"; @@ -76,6 +75,8 @@ import broadcastMessageInjectable from "../common/ipc/broadcast-message.injectab import getElectronThemeInjectable from "./electron-app/features/get-electron-theme.injectable"; import syncThemeFromOperatingSystemInjectable from "./electron-app/features/sync-theme-from-operating-system.injectable"; import platformInjectable from "../common/vars/platform.injectable"; +import { noop } from "../renderer/utils"; +import baseBundeledBinariesDirectoryInjectable from "../common/vars/base-bundled-binaries-dir.injectable"; export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) { const { @@ -126,12 +127,10 @@ export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) { di.override(appNameInjectable, () => "some-app-name"); di.override(registerChannelInjectable, () => () => undefined); - di.override(directoryForBundledBinariesInjectable, () => "some-bin-directory"); - di.override(broadcastMessageInjectable, () => (channel) => { throw new Error(`Tried to broadcast message to channel "${channel}" over IPC without explicit override.`); }); - + di.override(baseBundeledBinariesDirectoryInjectable, () => "some-bin-directory"); di.override(spawnInjectable, () => () => { return { stderr: { on: jest.fn(), removeAllListeners: jest.fn() }, diff --git a/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts index c3706f7c08..07bb87bd63 100644 --- a/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts +++ b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts @@ -9,10 +9,10 @@ import type { Cluster } from "../../common/cluster/cluster"; import path from "path"; import selfsigned from "selfsigned"; import { getBinaryName } from "../../common/vars"; -import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable"; import spawnInjectable from "../child-process/spawn.injectable"; import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate"; import loggerInjectable from "../../common/logger.injectable"; +import baseBundeledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; @@ -25,7 +25,7 @@ const createKubeAuthProxyInjectable = getInjectable({ return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => { const clusterUrl = new URL(cluster.apiUrl); const dependencies: KubeAuthProxyDependencies = { - proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName), + proxyBinPath: path.join(di.inject(baseBundeledBinariesDirectoryInjectable), binaryName), proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate), spawn: di.inject(spawnInjectable), logger: di.inject(loggerInjectable), diff --git a/src/main/kubectl/binary-name.injectable.ts b/src/main/kubectl/binary-name.injectable.ts new file mode 100644 index 0000000000..66b42a6007 --- /dev/null +++ b/src/main/kubectl/binary-name.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 { getInjectable } from "@ogre-tools/injectable"; +import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; + +const kubectlBinaryNameInjectable = getInjectable({ + id: "kubectl-binary-name", + instantiate: (di) => { + const platform = di.inject(normalizedPlatformInjectable); + + return platform === "windows" + ? "kubectl.exe" + : "kubectl"; + }, +}); + +export default kubectlBinaryNameInjectable; diff --git a/src/main/kubectl/bundled-binary-path.injectable.ts b/src/main/kubectl/bundled-binary-path.injectable.ts new file mode 100644 index 0000000000..99cb7f2e7e --- /dev/null +++ b/src/main/kubectl/bundled-binary-path.injectable.ts @@ -0,0 +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 path from "path"; +import baseBundeledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; +import kubectlBinaryNameInjectable from "./binary-name.injectable"; + +const bundledKubectlBinaryPathInjectable = getInjectable({ + id: "bundled-kubectl-binary-path", + instantiate: (di) => path.join( + di.inject(baseBundeledBinariesDirectoryInjectable), + di.inject(kubectlBinaryNameInjectable), + ), +}); + +export default bundledKubectlBinaryPathInjectable; diff --git a/src/main/kubectl/create-kubectl.injectable.ts b/src/main/kubectl/create-kubectl.injectable.ts index 931f777535..d670e8e665 100644 --- a/src/main/kubectl/create-kubectl.injectable.ts +++ b/src/main/kubectl/create-kubectl.injectable.ts @@ -3,24 +3,31 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import type { KubectlDependencies } from "./kubectl"; import { Kubectl } from "./kubectl"; import directoryForKubectlBinariesInjectable from "../../common/app-paths/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable"; +import kubectlDownloadingNormalizedArchInjectable from "./normalized-arch.injectable"; +import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; +import kubectlBinaryNameInjectable from "./binary-name.injectable"; +import bundledKubectlBinaryPathInjectable from "./bundled-binary-path.injectable"; +import baseBundeledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; const createKubectlInjectable = getInjectable({ id: "create-kubectl", instantiate: (di) => { - const dependencies = { + const dependencies: KubectlDependencies = { userStore: di.inject(userStoreInjectable), - - directoryForKubectlBinaries: di.inject( - directoryForKubectlBinariesInjectable, - ), + directoryForKubectlBinaries: di.inject(directoryForKubectlBinariesInjectable), + normalizedDownloadArch: di.inject(kubectlDownloadingNormalizedArchInjectable), + normalizedDownloadPlatform: di.inject(normalizedPlatformInjectable), + kubectlBinaryName: di.inject(kubectlBinaryNameInjectable), + bundledKubectlBinaryPath: di.inject(bundledKubectlBinaryPathInjectable), + baseBundeledBinariesDirectory: di.inject(baseBundeledBinariesDirectoryInjectable), }; - return (clusterVersion: string) => - new Kubectl(dependencies, clusterVersion); + return (clusterVersion: string) => new Kubectl(dependencies, clusterVersion); }, }); diff --git a/src/main/kubectl/kubectl.ts b/src/main/kubectl/kubectl.ts index d2bc5697fc..55f7e4675d 100644 --- a/src/main/kubectl/kubectl.ts +++ b/src/main/kubectl/kubectl.ts @@ -10,7 +10,6 @@ import logger from "../logger"; import { ensureDir, pathExists } from "fs-extra"; import * as lockFile from "proper-lockfile"; import { getBundledKubectlVersion } from "../../common/utils/app-version"; -import { normalizedPlatform, normalizedArch, kubectlBinaryName, kubectlBinaryPath, baseBinariesDir } from "../../common/vars"; import { SemVer } from "semver"; import { defaultPackageMirror, packageMirrors } from "../../common/user-store/preferences-helpers"; import got from "got/dist/source"; @@ -40,27 +39,31 @@ const kubectlMap: Map = new Map([ ]); const initScriptVersionString = "# lens-initscript v3"; -interface Dependencies { - directoryForKubectlBinaries: string; - - userStore: { - kubectlBinariesPath?: string; - downloadBinariesPath?: string; - downloadKubectlBinaries: boolean; - downloadMirror: string; +export interface KubectlDependencies { + readonly directoryForKubectlBinaries: string; + readonly normalizedDownloadPlatform: "darwin" | "linux" | "windows"; + readonly normalizedDownloadArch: "amd64" | "arm64" | "386"; + readonly kubectlBinaryName: string; + readonly bundledKubectlBinaryPath: string; + readonly baseBundeledBinariesDirectory: string; + readonly userStore: { + readonly kubectlBinariesPath?: string; + readonly downloadBinariesPath?: string; + readonly downloadKubectlBinaries: boolean; + readonly downloadMirror: string; }; } export class Kubectl { - public kubectlVersion: string; - protected url: string; - protected path: string; - protected dirname: string; + public readonly kubectlVersion: string; + protected readonly url: string; + protected readonly path: string; + protected readonly dirname: string; public static readonly bundledKubectlVersion = bundledVersion; public static invalidBundle = false; - constructor(private dependencies: Dependencies, clusterVersion: string) { + constructor(protected readonly dependencies: KubectlDependencies, clusterVersion: string) { let version: SemVer; try { @@ -83,13 +86,13 @@ export class Kubectl { logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using fallback`); } - this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${normalizedPlatform}/${normalizedArch}/${kubectlBinaryName}`; + this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${this.dependencies.normalizedDownloadPlatform}/${this.dependencies.normalizedDownloadArch}/${this.dependencies.kubectlBinaryName}`; this.dirname = path.normalize(path.join(this.getDownloadDir(), this.kubectlVersion)); - this.path = path.join(this.dirname, kubectlBinaryName); + this.path = path.join(this.dirname, this.dependencies.kubectlBinaryName); } public getBundledPath() { - return kubectlBinaryPath.get(); + return this.dependencies.bundledKubectlBinaryPath; } public getPathFromPreferences() { @@ -279,12 +282,11 @@ export class Kubectl { } protected async writeInitScripts() { + const binariesDir = this.dependencies.baseBundeledBinariesDirectory; const kubectlPath = this.dependencies.userStore.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences()); - const binariesDir = baseBinariesDir.get(); - const bashScriptPath = path.join(this.dirname, ".bash_set_path"); const bashScript = [ initScriptVersionString, @@ -297,7 +299,7 @@ export class Kubectl { "elif test -f \"$HOME/.profile\"; then", " . \"$HOME/.profile\"", "fi", - `export PATH="${binariesDir}:${kubectlPath}:$PATH"`, + `export PATH="${kubectlPath}:${binariesDir}:$PATH"`, 'export KUBECONFIG="$tempkubeconfig"', `NO_PROXY=",\${NO_PROXY:-localhost},"`, `NO_PROXY="\${NO_PROXY//,localhost,/,}"`, @@ -328,7 +330,7 @@ export class Kubectl { "d=\":$PATH:\"", `d=\${d//$p/:}`, `d=\${d/#:/}`, - `export PATH="$binariesDir:$kubectlpath:\${d/%:/}"`, + `export PATH="$kubectlpath:$binariesDir:\${d/%:/}"`, "export KUBECONFIG=\"$tempkubeconfig\"", `NO_PROXY=",\${NO_PROXY:-localhost},"`, `NO_PROXY="\${NO_PROXY//,localhost,/,}"`, diff --git a/src/main/kubectl/normalized-arch.injectable.ts b/src/main/kubectl/normalized-arch.injectable.ts new file mode 100644 index 0000000000..88ec6b1067 --- /dev/null +++ b/src/main/kubectl/normalized-arch.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"; + +const kubectlDownloadingNormalizedArchInjectable = getInjectable({ + id: "kubectl-downloading-normalized-arch", + instantiate: () => { + switch (process.arch) { + case "arm64": + return "arm64"; + case "x64": + case "amd64": + return "amd64"; + case "386": + case "x32": + case "ia32": + return "386"; + default: + throw new Error(`arch=${process.arch} is unsupported`); + } + }, + causesSideEffects: true, +}); + +export default kubectlDownloadingNormalizedArchInjectable; diff --git a/src/main/router/router.test.ts b/src/main/router/router.test.ts index 20b8022efc..20d5f4f526 100644 --- a/src/main/router/router.test.ts +++ b/src/main/router/router.test.ts @@ -17,6 +17,9 @@ import mockFs from "mock-fs"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import type { Route } from "./route"; import type { SetRequired } from "type-fest"; +import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; +import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; +import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; describe("router", () => { let router: Router; @@ -31,6 +34,9 @@ describe("router", () => { di.override(parseRequestInjectable, () => () => Promise.resolve({ payload: "some-payload" })); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + di.override(kubectlBinaryNameInjectable, () => "kubectl"); + di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); + di.override(normalizedPlatformInjectable, () => "darwin"); const injectable = getInjectable({ id: "some-route", diff --git a/src/main/shell-session/local-shell-session/local-shell-session.ts b/src/main/shell-session/local-shell-session/local-shell-session.ts index bced1e52f6..9f42c7f242 100644 --- a/src/main/shell-session/local-shell-session/local-shell-session.ts +++ b/src/main/shell-session/local-shell-session/local-shell-session.ts @@ -50,11 +50,11 @@ export class LocalShellSession extends ShellSession { switch(path.basename(shell)) { case "powershell.exe": - return ["-NoExit", "-command", `& {$Env:PATH="${baseBinariesDir.get()};${kubectlPathDir};$Env:PATH"}`]; + return ["-NoExit", "-command", `& {$Env:PATH="${kubectlPathDir};${baseBinariesDir.get()};$Env:PATH"}`]; case "bash": return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")]; case "fish": - return ["--login", "--init-command", `export PATH="${baseBinariesDir.get()}:${kubectlPathDir}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`]; + return ["--login", "--init-command", `export PATH="${kubectlPathDir}:${baseBinariesDir.get()}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`]; case "zsh": return ["--login"]; default: diff --git a/src/main/shell-session/shell-session.ts b/src/main/shell-session/shell-session.ts index 93fcb55dd1..59827b2314 100644 --- a/src/main/shell-session/shell-session.ts +++ b/src/main/shell-session/shell-session.ts @@ -311,7 +311,7 @@ export abstract class ShellSession { protected async getShellEnv() { const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv()))); - const pathStr = [...this.getPathEntries(), await this.kubectlBinDirP, process.env.PATH].join(path.delimiter); + const pathStr = [await this.kubectlBinDirP, ...this.getPathEntries(), process.env.PATH].join(path.delimiter); const shell = UserStore.getInstance().resolvedShell; delete env.DEBUG; // don't pass DEBUG into shells diff --git a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx index a2a0e6a301..ded5cf8098 100644 --- a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx +++ b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx @@ -26,6 +26,9 @@ import { computed } from "mobx"; import { routeSpecificComponentInjectionToken } from "../../../routes/route-specific-component-injection-token"; import { navigateToRouteInjectionToken } from "../../../../common/front-end-routing/navigate-to-route-injection-token"; import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable"; +import normalizedPlatformInjectable from "../../../../common/vars/normalized-platform.injectable"; +import kubectlBinaryNameInjectable from "../../../../main/kubectl/binary-name.injectable"; +import kubectlDownloadingNormalizedArchInjectable from "../../../../main/kubectl/normalized-arch.injectable"; jest.mock("electron", () => ({ app: { @@ -101,8 +104,11 @@ describe("", () => { applicationBuilder = getApplicationBuilder(); applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => { - mainDi.override(createContextHandlerInjectable, () => () => undefined as any); - mainDi.override(createKubeconfigManagerInjectable, () => () => undefined as any); + mainDi.override(createContextHandlerInjectable, () => () => undefined as never); + mainDi.override(createKubeconfigManagerInjectable, () => () => undefined as never); + mainDi.override(kubectlBinaryNameInjectable, () => "kubectl"); + mainDi.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); + mainDi.override(normalizedPlatformInjectable, () => "darwin"); rendererDi.override(hotbarStoreInjectable, () => ({})); rendererDi.override(storesAndApisCanBeCreatedInjectable, () => true); From b414f9e06d28a6e3327dfdd1ff8e0a888e5709b1 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Fri, 3 Jun 2022 11:21:36 +0300 Subject: [PATCH 42/43] Introduce way to install update directly from tray. Make application updater unit testable... (#5433) * Extract product name as injectable Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make tray items comply with Open Closed Principle Signed-off-by: Janne Savolainen * Replace duplicated overrides with global Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Add behaviour for navigating to preferences using tray Signed-off-by: Janne Savolainen * Introduce a tray item for updating application Signed-off-by: Janne Savolainen * Tweak naming Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak more naming Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove redundant indirection Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak more naming Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce injectable for package.json being side-effect Signed-off-by: Janne Savolainen * Relocate file to directory containing feature Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Switch to using injectable for limiting side effect Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Add missing injection token for implementation of tray item Signed-off-by: Janne Savolainen * Remove resetting state for update is ready to be installed for being unclear Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Kill dead code Signed-off-by: Janne Savolainen * Make label of tray item reactive Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Extract updating is enabled to separate injectable Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce competition for tray Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Expand scope of behaviour for updating using tray also contain checking for updates Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove dead code Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Kill dead code Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Implement checking of updates from multiple update channels Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Start installing updates automatically when quitting application Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Show application window when checking of updates has happened Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Show notifications and dialog for downloading update Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Implement naive notifications for version updates Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Implement checking of Electron specific updates as responsibility Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Implement downloading of Electron specific updates as responsibility Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce competition for channel abstraction Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove redundant global override Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix typing after enabling strict mode Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce abstraction for a state that is shared between environments Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Extract states of application update to be usable from all environments Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Handle failing download of update Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make code for window visibility actually work Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Consolidate code for sending messages between processes to a window Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Split bloated dependency in smaller pieces Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make state of download progress accessible from all environments Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Rename files for accuracy Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Move channel abstraction to more global directory Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Enhance typing of channels and sync-box Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Consolidate channel abstraction types Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Update asyncFn to support strict mode Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix snapshot after rebase Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Add missing global override Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce injection token for channels to allow injecting all of them at once Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Add notifications about change in update status Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Rename property for accuracy Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak code style Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make notifications unit testable in behaviours Signed-off-by: Janne Savolainen * Add implementation for asking boolean over processes Signed-off-by: Janne Savolainen * Reorganize responsibilities for checking updates Signed-off-by: Janne Savolainen * Reorganize tests for installing update under separate scenarios Signed-off-by: Janne Savolainen * Make stuff happening when root frame is rendered unit testable Signed-off-by: Janne Savolainen * Introduce periodical check for updates Signed-off-by: Janne Savolainen * Allow downgrading app versions Signed-off-by: Janne Savolainen * Switch to using competition for checking of updates in application menu Signed-off-by: Janne Savolainen * Kill dead code Signed-off-by: Janne Savolainen * Make test less prone to fail for wrong reason Signed-off-by: Janne Savolainen * Remove redundant boilerplate Signed-off-by: Janne Savolainen * Make tests for specific migrations less prone to failing for wrong reason Signed-off-by: Janne Savolainen * Move shared stuff under common Signed-off-by: Janne Savolainen * Switch to using single source of truth for selected update channel Signed-off-by: Janne Savolainen * Extract tests for installing update from different update channels to separate scenario Signed-off-by: Janne Savolainen * Add missing global override Signed-off-by: Janne Savolainen * Switch to using release channel of installed application version as default value for selected update channel Signed-off-by: Janne Savolainen * Consolidate usage of channel abstraction to same implementation Signed-off-by: Janne Savolainen * Make Channel abstraction support return values Signed-off-by: Janne Savolainen * Fix direct calling of runnables Signed-off-by: Janne Savolainen * Synchronize initial values of sync boxes when window starts Signed-off-by: Janne Savolainen * Add missing global override Signed-off-by: Janne Savolainen * Tweak message of question from user Signed-off-by: Janne Savolainen * Consolidate names of directories Signed-off-by: Janne Savolainen * Add TODO Signed-off-by: Janne Savolainen * Remove unimplemented scenario from test Signed-off-by: Janne Savolainen * Simplify test Signed-off-by: Janne Savolainen * Improve name of test Signed-off-by: Janne Savolainen * Remove redundant overrides Signed-off-by: Janne Savolainen * Fix code style Signed-off-by: Janne Savolainen * Make Animate deterministic in unit tests Signed-off-by: Janne Savolainen * Simplify naming Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Simplify more naming Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Simplify even more naming Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Simplify more and more naming Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Add todo for cleaning unacceptable code encountered Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Improve name of behaviour Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Make unit test more strict Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Enhance name of behaviour Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Introduce dependency to get random IDs Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Make asking of boolean value from user not require explicit ID for question Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Simplify code for asking of boolean value from user Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Make setting of initial state for sync boxes not trigger irrelevant messaging to main Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Make a channel have default type for sent and returned message Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Introduce higher order function to log errors in decorated functions Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Export type for error logging Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak test name Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce higher order function for suppressing errors Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Relocate some explicit error handlings to proper level of abstraction Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make higher order function for logging errors support asynchronous rejecting with non error instance Signed-off-by: Janne Savolainen * Make overridden version of application exactly the one required by unit test Signed-off-by: Janne Savolainen * Mark injectable causing side effects Signed-off-by: Janne Savolainen * Revert not required changes Signed-off-by: Janne Savolainen * Make code for asserting a promise more strict Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make dependencies readonly Signed-off-by: Janne Savolainen * Remove duplication for disposers Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Implement initial values for sync-boxes Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Separate concept of message and request channels Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Introduce tests for requesting from channel in renderer Signed-off-by: Janne Savolainen * Implement requesting from renderer in main Signed-off-by: Janne Savolainen * Revert "Implement requesting from renderer in main" This reverts commit d3e7899d7900516f3dbfacdb317a453202318305. Signed-off-by: Janne Savolainen * Tweak typing of request channel listeners to get rid of unexpected undefined Signed-off-by: Janne Savolainen * Remove unused variable Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak timing of sentry setup Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Require messages for MessageChannels be JsonValues for serialization Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Require requests and responses for RequestChannels be JsonValues for serialization Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Make different MessageChannels not require explicit "extends JsonObject" Note: Non-escaped lint breaks type here for forcing interface over type. Reasonable effort brought no understanding for what is the relevant difference between the two. Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Make a primitive argument an object for readability Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Make typing of higher order function for error suppression not lie Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Serialize messages in channels to make IPC not blow up Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce a way to make intentional orphan promises uniform, controllable and deliberate Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Make downloading of update and what follows more deliberate as orphan promise Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Move utility function under directory Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Move another utility function under directory Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix incorrect name of file Signed-off-by: Janne Savolainen * Remove redundant code Signed-off-by: Janne Savolainen * Kill dead code Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Round percentage of update download progress in tray Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix rebase conflicts Signed-off-by: Janne Savolainen * Fix CheckForUpdate type errors Signed-off-by: Sebastian Malton Co-authored-by: Iku-turso Co-authored-by: Sebastian Malton --- package.json | 2 +- ...acters-in-page-registrations.test.tsx.snap | 11 +- .../navigate-to-extension-page.test.tsx.snap | 20 +- .../navigating-between-routes.test.tsx.snap | 6 + ...ation-using-application-menu.test.tsx.snap | 11 +- ...navigation-using-application-menu.test.tsx | 7 +- .../installing-update-using-tray.test.ts.snap | 536 +++++++++++++++++ .../installing-update.test.ts.snap | 81 +++ ...eriodical-checking-of-updates.test.ts.snap | 11 + ...selection-of-update-stability.test.ts.snap | 11 + .../downgrading-version-update.test.ts | 87 +++ .../installing-update-using-tray.test.ts | 235 ++++++++ .../installing-update.test.ts | 225 ++++++++ .../periodical-checking-of-updates.test.ts | 117 ++++ .../selection-of-update-stability.test.ts | 331 +++++++++++ .../order-of-sidebar-items.test.tsx.snap | 6 + ...-and-tab-navigation-for-core.test.tsx.snap | 21 + ...ab-navigation-for-extensions.test.tsx.snap | 24 + .../visibility-of-sidebar-items.test.tsx.snap | 6 + ...gation-using-application-menu.test.ts.snap | 11 +- .../navigation-using-application-menu.test.ts | 13 +- .../navigation-to-helm-charts.test.ts.snap | 3 + .../closing-preferences.test.tsx.snap | 40 +- ...on-to-application-preferences.test.ts.snap | 14 +- ...igation-to-editor-preferences.test.ts.snap | 14 +- ...tension-specific-preferences.test.tsx.snap | 25 +- ...ion-to-kubernetes-preferences.test.ts.snap | 45 +- ...vigation-to-proxy-preferences.test.ts.snap | 14 +- ...ion-to-telemetry-preferences.test.tsx.snap | 31 +- ...ation-to-terminal-preferences.test.ts.snap | 14 +- ...gation-using-application-menu.test.ts.snap | 19 +- .../navigation-using-tray.test.ts.snap | 542 ++++++++++++++++++ ...navigation-to-terminal-preferences.test.ts | 5 - .../preferences/navigation-using-tray.test.ts | 44 ++ ...gation-using-application-menu.test.ts.snap | 11 +- .../navigation-using-application-menu.test.ts | 5 +- src/common/__tests__/cluster-store.test.ts | 2 + src/common/__tests__/user-store.test.ts | 11 +- .../app-paths/app-path-injection-token.ts | 3 - .../app-paths/app-paths-channel.injectable.ts | 22 + ...cation-update-status-channel.injectable.ts | 29 + .../discovered-update-version.injectable.ts | 28 + .../progress-of-update-download.injectable.ts | 25 + .../default-update-channel.injectable.ts | 27 + .../selected-update-channel.injectable.ts | 39 ++ .../application-update/update-channels.ts | 36 ++ .../update-is-being-downloaded.injectable.ts | 21 + ...updates-are-being-discovered.injectable.ts | 21 + .../ask-boolean-answer-channel.injectable.ts | 21 + ...ask-boolean-question-channel.injectable.ts | 23 + .../app-navigation-channel.injectable.ts | 22 + ...ter-frame-navigation-channel.injectable.ts | 22 + .../navigation-ipc-channel.ts | 9 - .../app-version/app-version.injectable.ts | 5 +- .../create-channel/create-channel.ts | 10 - src/common/ipc/index.ts | 1 - src/common/ipc/update-available.ts | 52 -- .../root-frame-rendered-channel.injectable.ts | 21 + src/common/user-store/preferences-helpers.ts | 38 +- .../user-store/user-store.injectable.ts | 5 +- src/common/user-store/user-store.ts | 28 +- .../utils/channel/channel-injection-token.ts | 12 + src/common/utils/channel/channel.test.ts | 273 +++++++++ ...essage-channel-listener-injection-token.ts | 16 + ...equest-channel-listener-injection-token.ts | 16 + .../listening-of-channels.injectable.ts | 32 ++ .../message-channel-injection-token.ts | 16 + ...essage-channel-listener-injection-token.ts | 18 + .../message-to-channel-injection-token.ts | 23 + .../request-channel-injection-token.ts | 20 + ...equest-channel-listener-injection-token.ts | 25 + .../request-from-channel-injection-token.ts | 21 + src/common/utils/get-random-id.injectable.ts | 14 + .../utils/is-promise/is-promise.test.ts | 31 + .../is-promise/is-promise.ts} | 5 +- .../sync-box/create-sync-box.injectable.ts | 41 ++ .../sync-box-channel-listener.injectable.ts | 35 ++ .../sync-box/sync-box-channel.injectable.ts | 21 + ...nc-box-initial-value-channel.injectable.ts | 24 + .../sync-box/sync-box-injection-token.ts | 17 + .../sync-box/sync-box-state.injectable.ts | 18 + src/common/utils/sync-box/sync-box.test.ts | 179 ++++++ src/common/utils/tentative-parse-json.ts | 15 + src/common/utils/tentative-stringify-json.ts | 15 + .../with-error-logging.injectable.ts | 47 ++ .../with-error-logging.test.ts | 243 ++++++++ .../with-error-suppression.test.ts | 104 ++++ .../with-error-suppression.ts | 28 + .../with-orphan-promise.injectable.ts | 29 + .../with-orphan-promise.test.ts | 59 ++ src/common/vars.ts | 2 - src/common/vars/package-json.injectable.ts | 14 + src/main/__test__/kube-auth-proxy.test.ts | 4 - .../app-paths/app-name/app-name.injectable.ts | 5 +- .../app-name/product-name.injectable.ts | 14 + ...ths-request-channel-listener.injectable.ts | 27 + .../get-electron-app-path.test.ts | 2 - .../register-channel.injectable.ts | 17 - .../register-channel/register-channel.ts | 18 - .../app-paths/setup-app-paths.injectable.ts | 5 - src/main/app-updater.ts | 133 ----- .../check-for-platform-updates.injectable.ts | 60 ++ .../check-for-platform-updates.test.ts | 128 +++++ .../check-for-updates-tray-item.injectable.ts | 79 +++ ...st-change-in-updating-status.injectable.ts | 23 + ...pdates-starting-from-channel.injectable.ts | 54 ++ ...process-checking-for-updates.injectable.ts | 93 +++ .../update-can-be-downgraded.injectable.ts | 29 + .../download-platform-update.injectable.ts | 45 ++ .../download-platform-update.test.ts | 155 +++++ .../download-update.injectable.ts | 49 ++ ...application-update-tray-item.injectable.ts | 56 ++ ...periodical-check-for-updates.injectable.ts | 35 ++ .../start-checking-for-updates.injectable.ts | 29 + .../stop-checking-for-updates.injectable.ts | 27 + .../publish-is-configured.injectable.ts | 20 + .../updating-is-enabled.injectable.ts | 20 + ...update-should-happen-on-quit.injectable.ts | 25 + ...update-should-happen-on-quit.injectable.ts | 25 + ...update-should-happen-on-quit.injectable.ts | 54 ++ .../__snapshots__/ask-boolean.test.ts.snap | 336 +++++++++++ ...lean-answer-channel-listener.injectable.ts | 29 + .../ask-boolean-promise.injectable.ts | 33 ++ .../ask-boolean/ask-boolean.injectable.ts | 39 ++ src/main/ask-boolean/ask-boolean.test.ts | 206 +++++++ .../catalog-sync-to-renderer.injectable.ts | 2 + .../electron-updater-is-active.injectable.ts | 18 + .../features/electron-updater.injectable.ts | 14 + .../quit-and-install-update.injectable.ts | 20 + .../features/set-update-on-quit.injectable.ts | 20 + .../setup-update-checking.injectable.ts | 25 - src/main/getDiForUnitTesting.ts | 47 +- src/main/is-auto-update-enabled.injectable.ts | 19 - .../menu/application-menu-items.injectable.ts | 21 +- .../application-window.injectable.ts | 2 +- .../create-lens-window.injectable.ts | 4 +- .../lens-window-injection-token.ts | 2 +- .../navigate-for-extension.injectable.ts | 2 +- .../lens-window/navigate.injectable.ts | 2 +- .../start-kube-config-sync.injectable.ts | 2 + ...me-rendered-channel-listener.injectable.ts | 35 ++ .../runnables/setup-sentry.injectable.ts | 4 +- src/main/start-update-checking.injectable.ts | 19 - .../electron-tray/electron-tray.injectable.ts | 116 ++++ .../electron-tray/start-tray.injectable.ts | 25 + .../electron-tray/stop-tray.injectable.ts | 28 + src/main/tray/install-tray.injectable.ts | 25 - .../reactive-tray-menu-items.injectable.ts | 24 + ...art-reactive-tray-menu-items.injectable.ts | 28 + ...top-reactive-tray-menu-items.injectable.ts | 25 + .../about-app-tray-item.injectable.ts | 51 ++ .../open-app-tray-item.injectable.ts | 47 ++ .../open-preferences-tray-item.injectable.ts | 43 ++ ...quit-app-separator-tray-item.injectable.ts | 24 + .../quit-app-tray-item.injectable.ts | 43 ++ .../tray-menu-item-injection-token.ts | 25 + .../tray-menu-item-registrator.injectable.ts | 90 +++ .../tray-menu-items.injectable.ts | 49 ++ src/main/tray/tray.injectable.ts | 42 -- src/main/tray/tray.ts | 118 ++-- src/main/tray/uninstall-tray.injectable.ts | 25 - .../utils/__test__/update-channel.test.ts | 33 -- ...ist-message-channel-listener.injectable.ts | 38 ++ .../enlist-message-channel-listener.test.ts | 97 ++++ ...ist-request-channel-listener.injectable.ts | 34 ++ .../enlist-request-channel-listener.test.ts | 147 +++++ .../start-listening-of-channels.injectable.ts | 25 + .../channel}/ipc-main/ipc-main.injectable.ts | 0 .../channel/message-to-channel.injectable.ts | 39 ++ .../utils/channel/message-to-channel.test.ts | 167 ++++++ ...itial-value-channel-listener.injectable.ts | 31 + src/main/utils/update-channel.ts | 21 - ...alue-from-registered-channel.injectable.ts | 20 - .../get-value-from-registered-channel.ts | 16 - ...egister-ipc-channel-listener.injectable.ts | 25 - .../app-paths/setup-app-paths.injectable.ts | 28 +- ...ation-update-status-listener.injectable.ts | 57 ++ ...n-question-channel-listener.injectable.tsx | 107 ++++ src/renderer/bootstrap.tsx | 7 +- .../components/+catalog/catalog.test.tsx | 9 - .../+extensions/__tests__/extensions.test.tsx | 4 - .../components/+preferences/application.tsx | 29 +- .../__tests__/dialog.test.tsx | 5 - .../+role-bindings/__tests__/dialog.test.tsx | 5 - src/renderer/components/animate/animate.tsx | 28 +- .../request-animation-frame.injectable.ts | 13 + .../dock/__test__/dock-store.test.ts | 5 - .../dock/__test__/dock-tabs.test.tsx | 5 - .../__test__/log-resource-selector.test.tsx | 5 - .../__tests__/hotbar-remove-command.test.tsx | 5 - .../notifications-store.injectable.ts | 13 + .../notifications/notifications.store.tsx | 2 - .../notifications/notifications.tsx | 128 +++-- .../show-info-notification.injectable.ts | 26 + .../components/select/select.test.tsx | 7 +- .../test-utils/get-application-builder.tsx | 79 ++- .../__tests__/update-button.test.tsx | 12 +- ...-that-root-frame-is-rendered.injectable.ts | 22 + .../init-root-frame.injectable.ts | 2 +- src/renderer/frames/root-frame/root-frame.tsx | 24 +- src/renderer/getDiForUnitTesting.tsx | 27 +- .../ipc-channel-listener-injection-token.ts | 17 - ...gister-ipc-channel-listeners.injectable.ts | 28 - ...amespaces-forbidden-handler.injectable.tsx | 4 +- src/renderer/ipc/register-listeners.tsx | 94 +-- .../focus-window.injectable.ts | 0 ...navigation-channel-listener.injectable.ts} | 26 +- .../about-port-forwarding.injectable.ts | 6 +- ...notify-error-port-forwarding.injectable.ts | 6 +- .../port-forward/port-forward-notify.tsx | 7 +- src/renderer/themes/store.injectable.ts | 2 +- ...ist-message-channel-listener.injectable.ts | 38 ++ .../enlist-message-channel-listener.test.ts | 97 ++++ ...ist-request-channel-listener.injectable.ts | 19 + .../start-listening-of-channels.injectable.ts | 25 + .../channel}/ipc-renderer.injectable.ts | 0 .../channel/message-to-channel.injectable.ts | 26 + .../utils/channel/message-to-channel.test.ts | 57 ++ .../request-from-channel.injectable.ts | 30 + .../channel/request-from-channel.test.ts | 121 ++++ .../utils/channel/send-to-main.injectable.ts | 25 + ...nitial-values-for-sync-boxes.injectable.ts | 33 ++ .../channel-fakes/override-channels.ts | 20 + .../override-messaging-from-main-to-window.ts | 91 +++ .../override-messaging-from-window-to-main.ts | 64 +++ ...override-requesting-from-window-to-main.ts | 59 ++ src/test-utils/get-dis-for-unit-testing.ts | 23 - src/test-utils/override-ipc-bridge.ts | 104 ---- yarn.lock | 8 +- 229 files changed, 8652 insertions(+), 1217 deletions(-) create mode 100644 src/behaviours/application-update/__snapshots__/installing-update-using-tray.test.ts.snap create mode 100644 src/behaviours/application-update/__snapshots__/installing-update.test.ts.snap create mode 100644 src/behaviours/application-update/__snapshots__/periodical-checking-of-updates.test.ts.snap create mode 100644 src/behaviours/application-update/__snapshots__/selection-of-update-stability.test.ts.snap create mode 100644 src/behaviours/application-update/downgrading-version-update.test.ts create mode 100644 src/behaviours/application-update/installing-update-using-tray.test.ts create mode 100644 src/behaviours/application-update/installing-update.test.ts create mode 100644 src/behaviours/application-update/periodical-checking-of-updates.test.ts create mode 100644 src/behaviours/application-update/selection-of-update-stability.test.ts create mode 100644 src/behaviours/preferences/__snapshots__/navigation-using-tray.test.ts.snap create mode 100644 src/behaviours/preferences/navigation-using-tray.test.ts create mode 100644 src/common/app-paths/app-paths-channel.injectable.ts create mode 100644 src/common/application-update/application-update-status-channel.injectable.ts create mode 100644 src/common/application-update/discovered-update-version/discovered-update-version.injectable.ts create mode 100644 src/common/application-update/progress-of-update-download/progress-of-update-download.injectable.ts create mode 100644 src/common/application-update/selected-update-channel/default-update-channel.injectable.ts create mode 100644 src/common/application-update/selected-update-channel/selected-update-channel.injectable.ts create mode 100644 src/common/application-update/update-channels.ts create mode 100644 src/common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable.ts create mode 100644 src/common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable.ts create mode 100644 src/common/ask-boolean/ask-boolean-answer-channel.injectable.ts create mode 100644 src/common/ask-boolean/ask-boolean-question-channel.injectable.ts create mode 100644 src/common/front-end-routing/app-navigation-channel.injectable.ts create mode 100644 src/common/front-end-routing/cluster-frame-navigation-channel.injectable.ts delete mode 100644 src/common/front-end-routing/navigation-ipc-channel.ts delete mode 100644 src/common/ipc-channel/create-channel/create-channel.ts delete mode 100644 src/common/ipc/update-available.ts create mode 100644 src/common/root-frame-rendered-channel/root-frame-rendered-channel.injectable.ts create mode 100644 src/common/utils/channel/channel-injection-token.ts create mode 100644 src/common/utils/channel/channel.test.ts create mode 100644 src/common/utils/channel/enlist-message-channel-listener-injection-token.ts create mode 100644 src/common/utils/channel/enlist-request-channel-listener-injection-token.ts create mode 100644 src/common/utils/channel/listening-of-channels.injectable.ts create mode 100644 src/common/utils/channel/message-channel-injection-token.ts create mode 100644 src/common/utils/channel/message-channel-listener-injection-token.ts create mode 100644 src/common/utils/channel/message-to-channel-injection-token.ts create mode 100644 src/common/utils/channel/request-channel-injection-token.ts create mode 100644 src/common/utils/channel/request-channel-listener-injection-token.ts create mode 100644 src/common/utils/channel/request-from-channel-injection-token.ts create mode 100644 src/common/utils/get-random-id.injectable.ts create mode 100644 src/common/utils/is-promise/is-promise.test.ts rename src/common/{ipc-channel/channel.ts => utils/is-promise/is-promise.ts} (56%) create mode 100644 src/common/utils/sync-box/create-sync-box.injectable.ts create mode 100644 src/common/utils/sync-box/sync-box-channel-listener.injectable.ts create mode 100644 src/common/utils/sync-box/sync-box-channel.injectable.ts create mode 100644 src/common/utils/sync-box/sync-box-initial-value-channel.injectable.ts create mode 100644 src/common/utils/sync-box/sync-box-injection-token.ts create mode 100644 src/common/utils/sync-box/sync-box-state.injectable.ts create mode 100644 src/common/utils/sync-box/sync-box.test.ts create mode 100644 src/common/utils/tentative-parse-json.ts create mode 100644 src/common/utils/tentative-stringify-json.ts create mode 100644 src/common/utils/with-error-logging/with-error-logging.injectable.ts create mode 100644 src/common/utils/with-error-logging/with-error-logging.test.ts create mode 100644 src/common/utils/with-error-suppression/with-error-suppression.test.ts create mode 100644 src/common/utils/with-error-suppression/with-error-suppression.ts create mode 100644 src/common/utils/with-orphan-promise/with-orphan-promise.injectable.ts create mode 100644 src/common/utils/with-orphan-promise/with-orphan-promise.test.ts create mode 100644 src/common/vars/package-json.injectable.ts create mode 100644 src/main/app-paths/app-name/product-name.injectable.ts create mode 100644 src/main/app-paths/app-paths-request-channel-listener.injectable.ts delete mode 100644 src/main/app-paths/register-channel/register-channel.injectable.ts delete mode 100644 src/main/app-paths/register-channel/register-channel.ts delete mode 100644 src/main/app-updater.ts create mode 100644 src/main/application-update/check-for-platform-updates/check-for-platform-updates.injectable.ts create mode 100644 src/main/application-update/check-for-platform-updates/check-for-platform-updates.test.ts create mode 100644 src/main/application-update/check-for-updates-tray-item.injectable.ts create mode 100644 src/main/application-update/check-for-updates/broadcast-change-in-updating-status.injectable.ts create mode 100644 src/main/application-update/check-for-updates/check-for-updates-starting-from-channel.injectable.ts create mode 100644 src/main/application-update/check-for-updates/process-checking-for-updates.injectable.ts create mode 100644 src/main/application-update/check-for-updates/update-can-be-downgraded.injectable.ts create mode 100644 src/main/application-update/download-platform-update/download-platform-update.injectable.ts create mode 100644 src/main/application-update/download-platform-update/download-platform-update.test.ts create mode 100644 src/main/application-update/download-update/download-update.injectable.ts create mode 100644 src/main/application-update/install-application-update-tray-item.injectable.ts create mode 100644 src/main/application-update/periodical-check-for-updates/periodical-check-for-updates.injectable.ts create mode 100644 src/main/application-update/periodical-check-for-updates/start-checking-for-updates.injectable.ts create mode 100644 src/main/application-update/periodical-check-for-updates/stop-checking-for-updates.injectable.ts create mode 100644 src/main/application-update/publish-is-configured.injectable.ts create mode 100644 src/main/application-update/updating-is-enabled.injectable.ts create mode 100644 src/main/application-update/watch-if-update-should-happen-on-quit/start-watching-if-update-should-happen-on-quit.injectable.ts create mode 100644 src/main/application-update/watch-if-update-should-happen-on-quit/stop-watching-if-update-should-happen-on-quit.injectable.ts create mode 100644 src/main/application-update/watch-if-update-should-happen-on-quit/watch-if-update-should-happen-on-quit.injectable.ts create mode 100644 src/main/ask-boolean/__snapshots__/ask-boolean.test.ts.snap create mode 100644 src/main/ask-boolean/ask-boolean-answer-channel-listener.injectable.ts create mode 100644 src/main/ask-boolean/ask-boolean-promise.injectable.ts create mode 100644 src/main/ask-boolean/ask-boolean.injectable.ts create mode 100644 src/main/ask-boolean/ask-boolean.test.ts create mode 100644 src/main/electron-app/features/electron-updater-is-active.injectable.ts create mode 100644 src/main/electron-app/features/electron-updater.injectable.ts create mode 100644 src/main/electron-app/features/quit-and-install-update.injectable.ts create mode 100644 src/main/electron-app/features/set-update-on-quit.injectable.ts delete mode 100644 src/main/electron-app/runnables/setup-update-checking.injectable.ts delete mode 100644 src/main/is-auto-update-enabled.injectable.ts create mode 100644 src/main/start-main-application/runnables/root-frame-rendered-channel-listener/root-frame-rendered-channel-listener.injectable.ts delete mode 100644 src/main/start-update-checking.injectable.ts create mode 100644 src/main/tray/electron-tray/electron-tray.injectable.ts create mode 100644 src/main/tray/electron-tray/start-tray.injectable.ts create mode 100644 src/main/tray/electron-tray/stop-tray.injectable.ts delete mode 100644 src/main/tray/install-tray.injectable.ts create mode 100644 src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts create mode 100644 src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts create mode 100644 src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable.ts create mode 100644 src/main/tray/tray-menu-item/implementations/about-app-tray-item.injectable.ts create mode 100644 src/main/tray/tray-menu-item/implementations/open-app-tray-item.injectable.ts create mode 100644 src/main/tray/tray-menu-item/implementations/open-preferences-tray-item.injectable.ts create mode 100644 src/main/tray/tray-menu-item/implementations/quit-app-separator-tray-item.injectable.ts create mode 100644 src/main/tray/tray-menu-item/implementations/quit-app-tray-item.injectable.ts create mode 100644 src/main/tray/tray-menu-item/tray-menu-item-injection-token.ts create mode 100644 src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts create mode 100644 src/main/tray/tray-menu-item/tray-menu-items.injectable.ts delete mode 100644 src/main/tray/tray.injectable.ts delete mode 100644 src/main/tray/uninstall-tray.injectable.ts delete mode 100644 src/main/utils/__test__/update-channel.test.ts create mode 100644 src/main/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts create mode 100644 src/main/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts create mode 100644 src/main/utils/channel/channel-listeners/enlist-request-channel-listener.injectable.ts create mode 100644 src/main/utils/channel/channel-listeners/enlist-request-channel-listener.test.ts create mode 100644 src/main/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts rename src/main/{app-paths/register-channel => utils/channel}/ipc-main/ipc-main.injectable.ts (100%) create mode 100644 src/main/utils/channel/message-to-channel.injectable.ts create mode 100644 src/main/utils/channel/message-to-channel.test.ts create mode 100644 src/main/utils/sync-box/sync-box-initial-value-channel-listener.injectable.ts delete mode 100644 src/main/utils/update-channel.ts delete mode 100644 src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable.ts delete mode 100644 src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts delete mode 100644 src/renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable.ts create mode 100644 src/renderer/application-update/application-update-status-listener.injectable.ts create mode 100644 src/renderer/ask-boolean/ask-boolean-question-channel-listener.injectable.tsx create mode 100644 src/renderer/components/animate/request-animation-frame.injectable.ts create mode 100644 src/renderer/components/notifications/notifications-store.injectable.ts create mode 100644 src/renderer/components/notifications/show-info-notification.injectable.ts create mode 100644 src/renderer/frames/root-frame/broadcast-that-root-frame-is-rendered.injectable.ts delete mode 100644 src/renderer/ipc-channel-listeners/ipc-channel-listener-injection-token.ts delete mode 100644 src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts rename src/renderer/{ipc-channel-listeners => navigation}/focus-window.injectable.ts (100%) rename src/renderer/{ipc-channel-listeners/navigation-listener.injectable.ts => navigation/navigation-channel-listener.injectable.ts} (52%) create mode 100644 src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts create mode 100644 src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts create mode 100644 src/renderer/utils/channel/channel-listeners/enlist-request-channel-listener.injectable.ts create mode 100644 src/renderer/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts rename src/renderer/{app-paths/get-value-from-registered-channel/ipc-renderer => utils/channel}/ipc-renderer.injectable.ts (100%) create mode 100644 src/renderer/utils/channel/message-to-channel.injectable.ts create mode 100644 src/renderer/utils/channel/message-to-channel.test.ts create mode 100644 src/renderer/utils/channel/request-from-channel.injectable.ts create mode 100644 src/renderer/utils/channel/request-from-channel.test.ts create mode 100644 src/renderer/utils/channel/send-to-main.injectable.ts create mode 100644 src/renderer/utils/sync-box/provide-initial-values-for-sync-boxes.injectable.ts create mode 100644 src/test-utils/channel-fakes/override-channels.ts create mode 100644 src/test-utils/channel-fakes/override-messaging-from-main-to-window.ts create mode 100644 src/test-utils/channel-fakes/override-messaging-from-window-to-main.ts create mode 100644 src/test-utils/channel-fakes/override-requesting-from-window-to-main.ts delete mode 100644 src/test-utils/get-dis-for-unit-testing.ts delete mode 100644 src/test-utils/override-ipc-bridge.ts diff --git a/package.json b/package.json index 36141a0ba3..00bfd584f2 100644 --- a/package.json +++ b/package.json @@ -281,7 +281,7 @@ "ws": "^8.5.0" }, "devDependencies": { - "@async-fn/jest": "1.6.0", + "@async-fn/jest": "1.6.1", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", diff --git a/src/behaviours/__snapshots__/extension-special-characters-in-page-registrations.test.tsx.snap b/src/behaviours/__snapshots__/extension-special-characters-in-page-registrations.test.tsx.snap index 3b43a51f66..80b0028469 100644 --- a/src/behaviours/__snapshots__/extension-special-characters-in-page-registrations.test.tsx.snap +++ b/src/behaviours/__snapshots__/extension-special-characters-in-page-registrations.test.tsx.snap @@ -1,11 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`extension special characters in page registrations renders 1`] = `
    `; +exports[`extension special characters in page registrations renders 1`] = ` +
    +
    +
    +`; exports[`extension special characters in page registrations when navigating to route with ID having special characters renders 1`] = `
    Some page
    +
    `; diff --git a/src/behaviours/__snapshots__/navigate-to-extension-page.test.tsx.snap b/src/behaviours/__snapshots__/navigate-to-extension-page.test.tsx.snap index edab04b903..c96763fe6e 100644 --- a/src/behaviours/__snapshots__/navigate-to-extension-page.test.tsx.snap +++ b/src/behaviours/__snapshots__/navigate-to-extension-page.test.tsx.snap @@ -1,12 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`navigate to extension page renders 1`] = `
    `; +exports[`navigate to extension page renders 1`] = ` +
    +
    +
    +`; exports[`navigate to extension page when extension navigates to child route renders 1`] = `
    Child page
    +
    `; @@ -31,6 +40,9 @@ exports[`navigate to extension page when extension navigates to route with param Some button
    +
    `; @@ -55,6 +67,9 @@ exports[`navigate to extension page when extension navigates to route without pa Some button
    +
    `; @@ -79,5 +94,8 @@ exports[`navigate to extension page when extension navigates to route without pa Some button
    +
    `; diff --git a/src/behaviours/__snapshots__/navigating-between-routes.test.tsx.snap b/src/behaviours/__snapshots__/navigating-between-routes.test.tsx.snap index 90ff615b2b..10e9eb2d39 100644 --- a/src/behaviours/__snapshots__/navigating-between-routes.test.tsx.snap +++ b/src/behaviours/__snapshots__/navigating-between-routes.test.tsx.snap @@ -8,6 +8,9 @@ exports[`navigating between routes given route with optional path parameters whe "someOtherParameter": "some-other-value" } +
    `; @@ -16,5 +19,8 @@ exports[`navigating between routes given route without path parameters when navi
    Some component
    +
    `; diff --git a/src/behaviours/add-cluster/__snapshots__/navigation-using-application-menu.test.tsx.snap b/src/behaviours/add-cluster/__snapshots__/navigation-using-application-menu.test.tsx.snap index d19612eac3..0fd00133aa 100644 --- a/src/behaviours/add-cluster/__snapshots__/navigation-using-application-menu.test.tsx.snap +++ b/src/behaviours/add-cluster/__snapshots__/navigation-using-application-menu.test.tsx.snap @@ -1,6 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`add-cluster - navigation using application menu renders 1`] = `
    `; +exports[`add-cluster - navigation using application menu renders 1`] = ` +
    +
    +
    +`; exports[`add-cluster - navigation using application menu when navigating to add cluster using application menu renders 1`] = `
    @@ -85,5 +91,8 @@ exports[`add-cluster - navigation using application menu when navigating to add
    +
    `; diff --git a/src/behaviours/add-cluster/navigation-using-application-menu.test.tsx b/src/behaviours/add-cluster/navigation-using-application-menu.test.tsx index e982b9de1c..bb68918c1a 100644 --- a/src/behaviours/add-cluster/navigation-using-application-menu.test.tsx +++ b/src/behaviours/add-cluster/navigation-using-application-menu.test.tsx @@ -6,16 +6,17 @@ import type { RenderResult } from "@testing-library/react"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; -import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable"; import React from "react"; // TODO: Make components free of side effects by making them deterministic jest.mock("../../renderer/components/tooltip/tooltip", () => ({ Tooltip: () => null, })); + jest.mock("../../renderer/components/tooltip/withTooltip", () => ({ withTooltip: (Target: any) => ({ tooltip, tooltipOverrideDisabled, ...props }: any) => , })); + jest.mock("../../renderer/components/monaco-editor/monaco-editor", () => ({ MonacoEditor: () => null, })); @@ -25,9 +26,7 @@ describe("add-cluster - navigation using application menu", () => { let rendered: RenderResult; beforeEach(async () => { - applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ mainDi }) => { - mainDi.override(isAutoUpdateEnabledInjectable, () => () => false); - }); + applicationBuilder = getApplicationBuilder(); rendered = await applicationBuilder.render(); }); diff --git a/src/behaviours/application-update/__snapshots__/installing-update-using-tray.test.ts.snap b/src/behaviours/application-update/__snapshots__/installing-update-using-tray.test.ts.snap new file mode 100644 index 0000000000..32e6cb1cb1 --- /dev/null +++ b/src/behaviours/application-update/__snapshots__/installing-update-using-tray.test.ts.snap @@ -0,0 +1,536 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`installing update using tray when started renders 1`] = ` + +
    +
    +
    + +`; + +exports[`installing update using tray when started when user checks for updates using tray renders 1`] = ` + +
    +
    +
    +
    + + + info_outline + + +
    +
    + Checking for updates... +
    +
    + + + close + + +
    +
    +
    +
    + +`; + +exports[`installing update using tray when started when user checks for updates using tray when new update is discovered renders 1`] = ` + +
    +
    +
    +
    + + + info_outline + + +
    +
    + Checking for updates... +
    +
    + + + close + + +
    +
    +
    +
    + + + info_outline + + +
    +
    + Download for version some-version started... +
    +
    + + + close + + +
    +
    +
    +
    + +`; + +exports[`installing update using tray when started when user checks for updates using tray when new update is discovered when download fails renders 1`] = ` + +
    +
    +
    +
    + + + info_outline + + +
    +
    + Checking for updates... +
    +
    + + + close + + +
    +
    +
    +
    + + + info_outline + + +
    +
    + Download for version some-version started... +
    +
    + + + close + + +
    +
    +
    +
    + + + info_outline + + +
    +
    + Download of update failed +
    +
    + + + close + + +
    +
    +
    +
    + +`; + +exports[`installing update using tray when started when user checks for updates using tray when new update is discovered when download succeeds renders 1`] = ` + +
    +
    +
    +
    + + + info_outline + + +
    +
    + Checking for updates... +
    +
    + + + close + + +
    +
    +
    +
    + + + info_outline + + +
    +
    + Download for version some-version started... +
    +
    + + + close + + +
    +
    +
    +
    + + + info_outline + + +
    +
    +
    + + Update Available + +

    + Version some-version of Lens IDE is available and ready to be installed. Would you like to update now? + +Lens should restart automatically, if it doesn't please restart manually. Installed extensions might require updating. +

    +
    + + +
    +
    +
    +
    + + + close + + +
    +
    +
    +
    + +`; + +exports[`installing update using tray when started when user checks for updates using tray when no new update is discovered renders 1`] = ` + +
    +
    +
    +
    + + + info_outline + + +
    +
    + Checking for updates... +
    +
    + + + close + + +
    +
    +
    +
    + + + info_outline + + +
    +
    + No new updates available +
    +
    + + + close + + +
    +
    +
    +
    + +`; diff --git a/src/behaviours/application-update/__snapshots__/installing-update.test.ts.snap b/src/behaviours/application-update/__snapshots__/installing-update.test.ts.snap new file mode 100644 index 0000000000..7025289254 --- /dev/null +++ b/src/behaviours/application-update/__snapshots__/installing-update.test.ts.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`installing update when started renders 1`] = ` + +
    +
    +
    + +`; + +exports[`installing update when started when user checks for updates renders 1`] = ` + +
    +
    +
    + +`; + +exports[`installing update when started when user checks for updates when new update is discovered renders 1`] = ` + +
    +
    +
    + +`; + +exports[`installing update when started when user checks for updates when new update is discovered when download fails renders 1`] = ` + +
    +
    +
    + +`; + +exports[`installing update when started when user checks for updates when new update is discovered when download succeeds renders 1`] = ` + +
    +
    +
    + +`; + +exports[`installing update when started when user checks for updates when new update is discovered when download succeeds when user answers not to install the update renders 1`] = ` + +
    +
    +
    + +`; + +exports[`installing update when started when user checks for updates when new update is discovered when download succeeds when user answers to install the update renders 1`] = ` + +
    +
    +
    + +`; + +exports[`installing update when started when user checks for updates when no new update is discovered renders 1`] = ` + +
    +
    +
    + +`; diff --git a/src/behaviours/application-update/__snapshots__/periodical-checking-of-updates.test.ts.snap b/src/behaviours/application-update/__snapshots__/periodical-checking-of-updates.test.ts.snap new file mode 100644 index 0000000000..84fa35ae04 --- /dev/null +++ b/src/behaviours/application-update/__snapshots__/periodical-checking-of-updates.test.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`periodical checking of updates given updater is enabled and configuration exists, when started renders 1`] = ` + +
    +
    +
    + +`; diff --git a/src/behaviours/application-update/__snapshots__/selection-of-update-stability.test.ts.snap b/src/behaviours/application-update/__snapshots__/selection-of-update-stability.test.ts.snap new file mode 100644 index 0000000000..dc96c447b0 --- /dev/null +++ b/src/behaviours/application-update/__snapshots__/selection-of-update-stability.test.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`selection of update stability when started renders 1`] = ` + +
    +
    +
    + +`; diff --git a/src/behaviours/application-update/downgrading-version-update.test.ts b/src/behaviours/application-update/downgrading-version-update.test.ts new file mode 100644 index 0000000000..e8e5635fb3 --- /dev/null +++ b/src/behaviours/application-update/downgrading-version-update.test.ts @@ -0,0 +1,87 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable"; +import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { CheckForPlatformUpdates } from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable"; +import checkForPlatformUpdatesInjectable from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable"; +import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable"; +import selectedUpdateChannelInjectable from "../../common/application-update/selected-update-channel/selected-update-channel.injectable"; +import type { DiContainer } from "@ogre-tools/injectable"; +import appVersionInjectable from "../../common/get-configuration-file-model/app-version/app-version.injectable"; +import { updateChannels } from "../../common/application-update/update-channels"; + +describe("downgrading version update", () => { + let applicationBuilder: ApplicationBuilder; + let checkForPlatformUpdatesMock: AsyncFnMock; + let mainDi: DiContainer; + + beforeEach(() => { + jest.useFakeTimers(); + + applicationBuilder = getApplicationBuilder(); + + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + checkForPlatformUpdatesMock = asyncFn(); + + mainDi.override( + checkForPlatformUpdatesInjectable, + () => checkForPlatformUpdatesMock, + ); + + mainDi.override(electronUpdaterIsActiveInjectable, () => true); + mainDi.override(publishIsConfiguredInjectable, () => true); + }); + + mainDi = applicationBuilder.dis.mainDi; + }); + + [ + { + updateChannel: updateChannels.latest, + appVersion: "4.0.0-beta", + downgradeIsAllowed: true, + }, + { + updateChannel: updateChannels.beta, + appVersion: "4.0.0-beta", + downgradeIsAllowed: false, + }, + { + updateChannel: updateChannels.beta, + appVersion: "4.0.0-beta.1", + downgradeIsAllowed: false, + }, + { + updateChannel: updateChannels.alpha, + appVersion: "4.0.0-beta", + downgradeIsAllowed: true, + }, + { + updateChannel: updateChannels.alpha, + appVersion: "4.0.0-alpha", + downgradeIsAllowed: false, + }, + ].forEach(({ appVersion, updateChannel, downgradeIsAllowed }) => { + it(`given application version "${appVersion}" and update channel "${updateChannel.id}", when checking for updates, can${downgradeIsAllowed ? "": "not"} downgrade`, async () => { + mainDi.override(appVersionInjectable, () => appVersion); + + await applicationBuilder.render(); + + const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable); + + selectedUpdateChannel.setValue(updateChannel.id); + + const processCheckingForUpdates = mainDi.inject(processCheckingForUpdatesInjectable); + + processCheckingForUpdates(); + + expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(expect.any(Object), { allowDowngrade: downgradeIsAllowed }); + }); + }); +}); diff --git a/src/behaviours/application-update/installing-update-using-tray.test.ts b/src/behaviours/application-update/installing-update-using-tray.test.ts new file mode 100644 index 0000000000..f6570bb8fa --- /dev/null +++ b/src/behaviours/application-update/installing-update-using-tray.test.ts @@ -0,0 +1,235 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import type { RenderResult } from "@testing-library/react"; +import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable"; +import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable"; +import type { CheckForPlatformUpdates } from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable"; +import checkForPlatformUpdatesInjectable from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { DownloadPlatformUpdate } from "../../main/application-update/download-platform-update/download-platform-update.injectable"; +import downloadPlatformUpdateInjectable from "../../main/application-update/download-platform-update/download-platform-update.injectable"; +import showApplicationWindowInjectable from "../../main/start-main-application/lens-window/show-application-window.injectable"; +import progressOfUpdateDownloadInjectable from "../../common/application-update/progress-of-update-download/progress-of-update-download.injectable"; + +describe("installing update using tray", () => { + let applicationBuilder: ApplicationBuilder; + let checkForPlatformUpdatesMock: AsyncFnMock; + let downloadPlatformUpdateMock: AsyncFnMock; + let showApplicationWindowMock: jest.Mock; + + beforeEach(() => { + applicationBuilder = getApplicationBuilder(); + + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + checkForPlatformUpdatesMock = asyncFn(); + downloadPlatformUpdateMock = asyncFn(); + showApplicationWindowMock = jest.fn(); + + mainDi.override(showApplicationWindowInjectable, () => showApplicationWindowMock); + + mainDi.override( + checkForPlatformUpdatesInjectable, + () => checkForPlatformUpdatesMock, + ); + + mainDi.override( + downloadPlatformUpdateInjectable, + () => downloadPlatformUpdateMock, + ); + + mainDi.override(electronUpdaterIsActiveInjectable, () => true); + mainDi.override(publishIsConfiguredInjectable, () => true); + }); + }); + + describe("when started", () => { + let rendered: RenderResult; + + beforeEach(async () => { + rendered = await applicationBuilder.render(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("user cannot install update yet", () => { + expect(applicationBuilder.tray.get("install-update")).toBeUndefined(); + }); + + describe("when user checks for updates using tray", () => { + let processCheckingForUpdatesPromise: Promise; + + beforeEach(async () => { + processCheckingForUpdatesPromise = + applicationBuilder.tray.click("check-for-updates"); + }); + + it("does not show application window yet", () => { + expect(showApplicationWindowMock).not.toHaveBeenCalled(); + }); + + it("user cannot check for updates again", () => { + expect( + applicationBuilder.tray.get("check-for-updates")?.enabled.get(), + ).toBe(false); + }); + + it("name of tray item for checking updates indicates that checking is happening", () => { + expect( + applicationBuilder.tray.get("check-for-updates")?.label?.get(), + ).toBe("Checking for updates..."); + }); + + it("user cannot install update yet", () => { + expect(applicationBuilder.tray.get("install-update")).toBeUndefined(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + describe("when no new update is discovered", () => { + beforeEach(async () => { + await checkForPlatformUpdatesMock.resolve({ + updateWasDiscovered: false, + }); + + await processCheckingForUpdatesPromise; + }); + + it("shows application window", () => { + expect(showApplicationWindowMock).toHaveBeenCalled(); + }); + + it("user cannot install update", () => { + expect(applicationBuilder.tray.get("install-update")).toBeUndefined(); + }); + + it("user can check for updates again", () => { + expect( + applicationBuilder.tray.get("check-for-updates")?.enabled.get(), + ).toBe(true); + }); + + it("name of tray item for checking updates no longer indicates that checking is happening", () => { + expect( + applicationBuilder.tray.get("check-for-updates")?.label?.get(), + ).toBe("Check for updates"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + }); + + describe("when new update is discovered", () => { + beforeEach(async () => { + await checkForPlatformUpdatesMock.resolve({ + updateWasDiscovered: true, + version: "some-version", + }); + + await processCheckingForUpdatesPromise; + }); + + it("shows application window", () => { + expect(showApplicationWindowMock).toHaveBeenCalled(); + }); + + it("user cannot check for updates again yet", () => { + expect( + applicationBuilder.tray.get("check-for-updates")?.enabled.get(), + ).toBe(false); + }); + + it("name of tray item for checking updates indicates that downloading is happening", () => { + expect( + applicationBuilder.tray.get("check-for-updates")?.label?.get(), + ).toBe("Downloading update some-version (0%)..."); + }); + + it("when download progresses with decimals, percentage increases as integers", () => { + const progressOfUpdateDownload = applicationBuilder.dis.mainDi.inject( + progressOfUpdateDownloadInjectable, + ); + + progressOfUpdateDownload.set({ percentage: 42.424242 }); + + expect( + applicationBuilder.tray.get("check-for-updates")?.label?.get(), + ).toBe("Downloading update some-version (42%)..."); + }); + + it("user still cannot install update", () => { + expect(applicationBuilder.tray.get("install-update")).toBeUndefined(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + describe("when download fails", () => { + beforeEach(async () => { + await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: false }); + }); + + it("user cannot install update", () => { + expect( + applicationBuilder.tray.get("install-update"), + ).toBeUndefined(); + }); + + it("user can check for updates again", () => { + expect( + applicationBuilder.tray.get("check-for-updates")?.enabled.get(), + ).toBe(true); + }); + + it("name of tray item for checking updates no longer indicates that downloading is happening", () => { + expect( + applicationBuilder.tray.get("check-for-updates")?.label?.get(), + ).toBe("Check for updates"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + }); + + describe("when download succeeds", () => { + beforeEach(async () => { + await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true }); + }); + + it("user can install update", () => { + expect( + applicationBuilder.tray.get("install-update")?.label?.get(), + ).toBe("Install update some-version"); + }); + + it("user can check for updates again", () => { + expect( + applicationBuilder.tray.get("check-for-updates")?.enabled.get(), + ).toBe(true); + }); + + it("name of tray item for checking updates no longer indicates that downloading is happening", () => { + expect( + applicationBuilder.tray.get("check-for-updates")?.label?.get(), + ).toBe("Check for updates"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + }); + }); + }); + }); +}); diff --git a/src/behaviours/application-update/installing-update.test.ts b/src/behaviours/application-update/installing-update.test.ts new file mode 100644 index 0000000000..3fec5f6d27 --- /dev/null +++ b/src/behaviours/application-update/installing-update.test.ts @@ -0,0 +1,225 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import quitAndInstallUpdateInjectable from "../../main/electron-app/features/quit-and-install-update.injectable"; +import type { RenderResult } from "@testing-library/react"; +import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable"; +import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable"; +import type { CheckForPlatformUpdates } from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable"; +import checkForPlatformUpdatesInjectable from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { DownloadPlatformUpdate } from "../../main/application-update/download-platform-update/download-platform-update.injectable"; +import downloadPlatformUpdateInjectable from "../../main/application-update/download-platform-update/download-platform-update.injectable"; +import setUpdateOnQuitInjectable from "../../main/electron-app/features/set-update-on-quit.injectable"; +import type { AskBoolean } from "../../main/ask-boolean/ask-boolean.injectable"; +import askBooleanInjectable from "../../main/ask-boolean/ask-boolean.injectable"; +import showInfoNotificationInjectable from "../../renderer/components/notifications/show-info-notification.injectable"; +import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable"; + +describe("installing update", () => { + let applicationBuilder: ApplicationBuilder; + let quitAndInstallUpdateMock: jest.Mock; + let checkForPlatformUpdatesMock: AsyncFnMock; + let downloadPlatformUpdateMock: AsyncFnMock; + let setUpdateOnQuitMock: jest.Mock; + let showInfoNotificationMock: jest.Mock; + let askBooleanMock: AsyncFnMock; + + beforeEach(() => { + applicationBuilder = getApplicationBuilder(); + + applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => { + quitAndInstallUpdateMock = jest.fn(); + checkForPlatformUpdatesMock = asyncFn(); + downloadPlatformUpdateMock = asyncFn(); + setUpdateOnQuitMock = jest.fn(); + showInfoNotificationMock = jest.fn(() => () => {}); + askBooleanMock = asyncFn(); + + rendererDi.override(showInfoNotificationInjectable, () => showInfoNotificationMock); + + mainDi.override(askBooleanInjectable, () => askBooleanMock); + mainDi.override(setUpdateOnQuitInjectable, () => setUpdateOnQuitMock); + + mainDi.override( + checkForPlatformUpdatesInjectable, + () => checkForPlatformUpdatesMock, + ); + + mainDi.override( + downloadPlatformUpdateInjectable, + () => downloadPlatformUpdateMock, + ); + + mainDi.override( + quitAndInstallUpdateInjectable, + () => quitAndInstallUpdateMock, + ); + + mainDi.override(electronUpdaterIsActiveInjectable, () => true); + mainDi.override(publishIsConfiguredInjectable, () => true); + }); + }); + + describe("when started", () => { + let rendered: RenderResult; + let processCheckingForUpdates: () => Promise; + + beforeEach(async () => { + rendered = await applicationBuilder.render(); + + processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + describe("when user checks for updates", () => { + let processCheckingForUpdatesPromise: Promise; + + beforeEach(async () => { + processCheckingForUpdatesPromise = processCheckingForUpdates(); + }); + + it("checks for updates", () => { + expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith( + expect.any(Object), + { allowDowngrade: true }, + ); + }); + + it("notifies the user that checking for updates is happening", () => { + expect(showInfoNotificationMock).toHaveBeenCalledWith("Checking for updates..."); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + describe("when no new update is discovered", () => { + beforeEach(async () => { + showInfoNotificationMock.mockClear(); + + await checkForPlatformUpdatesMock.resolve({ + updateWasDiscovered: false, + }); + + await processCheckingForUpdatesPromise; + }); + + it("notifies the user", () => { + expect(showInfoNotificationMock).toHaveBeenCalledWith("No new updates available"); + }); + + it("does not start downloading update", () => { + expect(downloadPlatformUpdateMock).not.toHaveBeenCalled(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + }); + + describe("when new update is discovered", () => { + beforeEach(async () => { + await checkForPlatformUpdatesMock.resolve({ + updateWasDiscovered: true, + version: "some-version", + }); + + await processCheckingForUpdatesPromise; + }); + + it("starts downloading the update", () => { + expect(downloadPlatformUpdateMock).toHaveBeenCalled(); + }); + + it("notifies the user that download is happening", () => { + expect(showInfoNotificationMock).toHaveBeenCalledWith("Download for version some-version started..."); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + describe("when download fails", () => { + beforeEach(async () => { + await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: false }); + }); + + it("does not quit and install update yet", () => { + expect(quitAndInstallUpdateMock).not.toHaveBeenCalled(); + }); + + it("notifies the user about failed download", () => { + expect(showInfoNotificationMock).toHaveBeenCalledWith("Download of update failed"); + }); + + it("does not ask user to install update", () => { + expect(askBooleanMock).not.toHaveBeenCalled(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + }); + + describe("when download succeeds", () => { + beforeEach(async () => { + await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true }); + }); + + it("does not quit and install update yet", () => { + expect(quitAndInstallUpdateMock).not.toHaveBeenCalled(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("asks user to install update immediately", () => { + expect(askBooleanMock).toHaveBeenCalledWith({ + title: "Update Available", + question: + "Version some-version of Lens IDE is available and ready to be installed. Would you like to update now?\n\n" + + "Lens should restart automatically, if it doesn't please restart manually. Installed extensions might require updating.", + }); + }); + + describe("when user answers to install the update", () => { + beforeEach(async () => { + await askBooleanMock.resolve(true); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("quits application and installs the update", () => { + expect(quitAndInstallUpdateMock).toHaveBeenCalled(); + }); + }); + + describe("when user answers not to install the update", () => { + beforeEach(async () => { + await askBooleanMock.resolve(false); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not quit application and install the update", () => { + expect(quitAndInstallUpdateMock).not.toHaveBeenCalled(); + }); + }); + }); + }); + }); + }); +}); diff --git a/src/behaviours/application-update/periodical-checking-of-updates.test.ts b/src/behaviours/application-update/periodical-checking-of-updates.test.ts new file mode 100644 index 0000000000..e81c002e34 --- /dev/null +++ b/src/behaviours/application-update/periodical-checking-of-updates.test.ts @@ -0,0 +1,117 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import type { RenderResult } from "@testing-library/react"; +import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable"; +import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable"; +import periodicalCheckForUpdatesInjectable from "../../main/application-update/periodical-check-for-updates/periodical-check-for-updates.injectable"; + +const ENOUGH_TIME = 1000 * 60 * 60 * 2; + +describe("periodical checking of updates", () => { + let applicationBuilder: ApplicationBuilder; + let processCheckingForUpdatesMock: AsyncFnMock<() => Promise>; + + beforeEach(() => { + jest.useFakeTimers(); + + applicationBuilder = getApplicationBuilder(); + + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + mainDi.unoverride(periodicalCheckForUpdatesInjectable); + mainDi.permitSideEffects(periodicalCheckForUpdatesInjectable); + + processCheckingForUpdatesMock = asyncFn(); + + mainDi.override( + processCheckingForUpdatesInjectable, + () => processCheckingForUpdatesMock, + ); + }); + }); + + describe("given updater is enabled and configuration exists, when started", () => { + let rendered: RenderResult; + + beforeEach(async () => { + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + mainDi.override(electronUpdaterIsActiveInjectable, () => true); + mainDi.override(publishIsConfiguredInjectable, () => true); + }); + + rendered = await applicationBuilder.render(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("checks for updates", () => { + expect(processCheckingForUpdatesMock).toHaveBeenCalled(); + }); + + it("when just not enough time passes, does not check for updates again automatically yet", () => { + processCheckingForUpdatesMock.mockClear(); + + jest.advanceTimersByTime(ENOUGH_TIME - 1); + + expect(processCheckingForUpdatesMock).not.toHaveBeenCalled(); + }); + + it("when just enough time passes, checks for updates again automatically", () => { + processCheckingForUpdatesMock.mockClear(); + + jest.advanceTimersByTime(ENOUGH_TIME); + + expect(processCheckingForUpdatesMock).toHaveBeenCalled(); + }); + }); + + describe("given updater is enabled but no configuration exist, when started", () => { + beforeEach(async () => { + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + mainDi.override(electronUpdaterIsActiveInjectable, () => true); + mainDi.override(publishIsConfiguredInjectable, () => false); + }); + + await applicationBuilder.render(); + }); + + it("does not check for updates", () => { + expect(processCheckingForUpdatesMock).not.toHaveBeenCalled(); + }); + + it("when time passes, never checks for updates", () => { + jest.runOnlyPendingTimers(); + + expect(processCheckingForUpdatesMock).not.toHaveBeenCalled(); + }); + }); + + describe("given updater is not enabled but and configuration exist, when started", () => { + beforeEach(async () => { + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + mainDi.override(electronUpdaterIsActiveInjectable, () => false); + mainDi.override(publishIsConfiguredInjectable, () => true); + }); + + await applicationBuilder.render(); + }); + + it("does not check for updates", () => { + expect(processCheckingForUpdatesMock).not.toHaveBeenCalled(); + }); + + it("when time passes, never checks for updates", () => { + jest.runOnlyPendingTimers(); + + expect(processCheckingForUpdatesMock).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/behaviours/application-update/selection-of-update-stability.test.ts b/src/behaviours/application-update/selection-of-update-stability.test.ts new file mode 100644 index 0000000000..1792fcd484 --- /dev/null +++ b/src/behaviours/application-update/selection-of-update-stability.test.ts @@ -0,0 +1,331 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import quitAndInstallUpdateInjectable from "../../main/electron-app/features/quit-and-install-update.injectable"; +import type { RenderResult } from "@testing-library/react"; +import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable"; +import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable"; +import type { CheckForPlatformUpdates } from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable"; +import checkForPlatformUpdatesInjectable from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { UpdateChannel, UpdateChannelId } from "../../common/application-update/update-channels"; +import { updateChannels } from "../../common/application-update/update-channels"; +import type { DownloadPlatformUpdate } from "../../main/application-update/download-platform-update/download-platform-update.injectable"; +import downloadPlatformUpdateInjectable from "../../main/application-update/download-platform-update/download-platform-update.injectable"; +import selectedUpdateChannelInjectable from "../../common/application-update/selected-update-channel/selected-update-channel.injectable"; +import type { IComputedValue } from "mobx"; +import setUpdateOnQuitInjectable from "../../main/electron-app/features/set-update-on-quit.injectable"; +import type { AskBoolean } from "../../main/ask-boolean/ask-boolean.injectable"; +import askBooleanInjectable from "../../main/ask-boolean/ask-boolean.injectable"; +import showInfoNotificationInjectable from "../../renderer/components/notifications/show-info-notification.injectable"; +import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable"; +import appVersionInjectable from "../../common/get-configuration-file-model/app-version/app-version.injectable"; + +describe("selection of update stability", () => { + let applicationBuilder: ApplicationBuilder; + let quitAndInstallUpdateMock: jest.Mock; + let checkForPlatformUpdatesMock: AsyncFnMock; + let downloadPlatformUpdateMock: AsyncFnMock; + let setUpdateOnQuitMock: jest.Mock; + let showInfoNotificationMock: jest.Mock; + let askBooleanMock: AsyncFnMock; + + beforeEach(() => { + applicationBuilder = getApplicationBuilder(); + + applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => { + quitAndInstallUpdateMock = jest.fn(); + checkForPlatformUpdatesMock = asyncFn(); + downloadPlatformUpdateMock = asyncFn(); + setUpdateOnQuitMock = jest.fn(); + showInfoNotificationMock = jest.fn(() => () => {}); + askBooleanMock = asyncFn(); + + rendererDi.override(showInfoNotificationInjectable, () => showInfoNotificationMock); + + mainDi.override(askBooleanInjectable, () => askBooleanMock); + mainDi.override(setUpdateOnQuitInjectable, () => setUpdateOnQuitMock); + + mainDi.override( + checkForPlatformUpdatesInjectable, + () => checkForPlatformUpdatesMock, + ); + + mainDi.override( + downloadPlatformUpdateInjectable, + () => downloadPlatformUpdateMock, + ); + + mainDi.override( + quitAndInstallUpdateInjectable, + () => quitAndInstallUpdateMock, + ); + + mainDi.override(electronUpdaterIsActiveInjectable, () => true); + mainDi.override(publishIsConfiguredInjectable, () => true); + }); + }); + + describe("when started", () => { + let rendered: RenderResult; + let processCheckingForUpdates: () => Promise; + + beforeEach(async () => { + rendered = await applicationBuilder.render(); + + processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + describe('given update channel "alpha" is selected, when checking for updates', () => { + let selectedUpdateChannel: { + value: IComputedValue; + setValue: (channelId: UpdateChannelId) => void; + }; + + beforeEach(() => { + selectedUpdateChannel = applicationBuilder.dis.mainDi.inject( + selectedUpdateChannelInjectable, + ); + + selectedUpdateChannel.setValue(updateChannels.alpha.id); + + processCheckingForUpdates(); + }); + + it('checks updates from update channel "alpha"', () => { + expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith( + updateChannels.alpha, + { allowDowngrade: true }, + ); + }); + + it("when update is discovered, does not check update from other update channels", async () => { + checkForPlatformUpdatesMock.mockClear(); + + await checkForPlatformUpdatesMock.resolve({ + updateWasDiscovered: true, + version: "some-version", + }); + + expect(checkForPlatformUpdatesMock).not.toHaveBeenCalled(); + }); + + describe("when no update is discovered", () => { + beforeEach(async () => { + checkForPlatformUpdatesMock.mockClear(); + + await checkForPlatformUpdatesMock.resolve({ + updateWasDiscovered: false, + }); + }); + + it('checks updates from update channel "beta"', () => { + expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith( + updateChannels.beta, + { allowDowngrade: true }, + ); + }); + + it("when update is discovered, does not check update from other update channels", async () => { + checkForPlatformUpdatesMock.mockClear(); + + await checkForPlatformUpdatesMock.resolve({ + updateWasDiscovered: true, + version: "some-version", + }); + + expect(checkForPlatformUpdatesMock).not.toHaveBeenCalled(); + }); + + describe("when no update is discovered again", () => { + beforeEach(async () => { + checkForPlatformUpdatesMock.mockClear(); + + await checkForPlatformUpdatesMock.resolve({ + updateWasDiscovered: false, + }); + }); + + it('finally checks updates from update channel "latest"', () => { + expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith( + updateChannels.latest, + { allowDowngrade: true }, + ); + }); + + it("when update is discovered, does not check update from other update channels", async () => { + checkForPlatformUpdatesMock.mockClear(); + + await checkForPlatformUpdatesMock.resolve({ + updateWasDiscovered: true, + version: "some-version", + }); + + expect(checkForPlatformUpdatesMock).not.toHaveBeenCalled(); + }); + }); + }); + }); + + describe('given update channel "beta" is selected', () => { + let selectedUpdateChannel: { + value: IComputedValue; + setValue: (channelId: UpdateChannelId) => void; + }; + + beforeEach(() => { + selectedUpdateChannel = applicationBuilder.dis.mainDi.inject( + selectedUpdateChannelInjectable, + ); + + selectedUpdateChannel.setValue(updateChannels.beta.id); + }); + + describe("when checking for updates", () => { + beforeEach(() => { + processCheckingForUpdates(); + }); + + describe('when update from "beta" channel is discovered', () => { + beforeEach(async () => { + await checkForPlatformUpdatesMock.resolve({ + updateWasDiscovered: true, + version: "some-beta-version", + }); + }); + + describe("when update is downloaded", () => { + beforeEach(async () => { + await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true }); + }); + + it("when user would close the application, installs the update", () => { + expect(setUpdateOnQuitMock).toHaveBeenLastCalledWith(true); + }); + + it('given user changes update channel to "latest", when user would close the application, does not install the update for not being stable enough', () => { + selectedUpdateChannel.setValue(updateChannels.latest.id); + + expect(setUpdateOnQuitMock).toHaveBeenLastCalledWith(false); + }); + + it('given user changes update channel to "alpha", when user would close the application, installs the update for being stable enough', () => { + selectedUpdateChannel.setValue(updateChannels.alpha.id); + + expect(setUpdateOnQuitMock).toHaveBeenLastCalledWith(false); + }); + }); + }); + }); + }); + }); + + it("given valid update channel selection is stored, when checking for updates, checks for updates from the update channel", async () => { + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + // TODO: Switch to more natural way of setting initial value + // TODO: UserStore is currently responsible for getting and setting initial value + const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable); + + selectedUpdateChannel.setValue(updateChannels.beta.id); + }); + + await applicationBuilder.render(); + + const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable); + + processCheckingForUpdates(); + + expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.beta, expect.any(Object)); + }); + + it("given invalid update channel selection is stored, when checking for updates, checks for updates from the update channel", async () => { + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + // TODO: Switch to more natural way of setting initial value + // TODO: UserStore is currently responsible for getting and setting initial value + const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable); + + selectedUpdateChannel.setValue("something-invalid" as UpdateChannelId); + }); + + await applicationBuilder.render(); + + const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable); + + processCheckingForUpdates(); + + expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.latest, expect.any(Object)); + }); + + it('given no update channel selection is stored and currently using stable release, when user checks for updates, checks for updates from "latest" update channel by default', async () => { + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + mainDi.override(appVersionInjectable, () => "1.0.0"); + }); + + await applicationBuilder.render(); + + const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable); + + processCheckingForUpdates(); + + expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith( + updateChannels.latest, + { allowDowngrade: true }, + ); + }); + + it('given no update channel selection is stored and currently using alpha release, when checking for updates, checks for updates from "alpha" channel', async () => { + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + mainDi.override(appVersionInjectable, () => "1.0.0-alpha"); + }); + + await applicationBuilder.render(); + + const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable); + + processCheckingForUpdates(); + + expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.alpha, expect.any(Object)); + }); + + it('given no update channel selection is stored and currently using beta release, when checking for updates, checks for updates from "beta" channel', async () => { + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + mainDi.override(appVersionInjectable, () => "1.0.0-beta"); + }); + + await applicationBuilder.render(); + + const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable); + + processCheckingForUpdates(); + + expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.beta, expect.any(Object)); + }); + + it("given update channel selection is stored and currently using prerelease, when checking for updates, checks for updates from stored channel", async () => { + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + mainDi.override(appVersionInjectable, () => "1.0.0-alpha"); + + // TODO: Switch to more natural way of setting initial value + // TODO: UserStore is currently responsible for getting and setting initial value + const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable); + + selectedUpdateChannel.setValue(updateChannels.beta.id); + }); + + await applicationBuilder.render(); + + const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable); + + processCheckingForUpdates(); + + expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.beta, expect.any(Object)); + }); +}); diff --git a/src/behaviours/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap b/src/behaviours/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap index 092337ec82..9af01f0969 100644 --- a/src/behaviours/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap +++ b/src/behaviours/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap @@ -328,6 +328,9 @@ exports[`cluster - order of sidebar items when rendered renders 1`] = `
    +
    `; @@ -723,5 +726,8 @@ exports[`cluster - order of sidebar items when rendered when parent is expanded
    +
    `; diff --git a/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap b/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap index ad1f7a8d4c..06d1a1e210 100644 --- a/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap +++ b/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap @@ -293,6 +293,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
    +
    `; @@ -589,6 +592,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
    +
    `; @@ -909,6 +915,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
    +
    `; @@ -1234,6 +1243,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
    +
    `; @@ -1534,6 +1546,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
    +
    `; @@ -1854,6 +1869,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
    +
    `; @@ -2150,5 +2168,8 @@ exports[`cluster - sidebar and tab navigation for core given core registrations +
    `; diff --git a/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap b/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap index be9c321cd5..19cb615cce 100644 --- a/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap +++ b/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap @@ -293,6 +293,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit +
    `; @@ -589,6 +592,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit +
    `; @@ -929,6 +935,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit +
    `; @@ -1313,6 +1322,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit +
    `; @@ -1697,6 +1709,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit +
    `; @@ -2036,6 +2051,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit +
    `; @@ -2376,6 +2394,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit +
    `; @@ -2672,5 +2693,8 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit +
    `; diff --git a/src/behaviours/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap b/src/behaviours/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap index 4f28c6ecef..cc44f56496 100644 --- a/src/behaviours/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap +++ b/src/behaviours/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap @@ -261,6 +261,9 @@ exports[`cluster - visibility of sidebar items given kube resource for route is +
    `; @@ -573,5 +576,8 @@ exports[`cluster - visibility of sidebar items given kube resource for route is +
    `; diff --git a/src/behaviours/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap b/src/behaviours/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap index 3a1b1309dd..c14ccb6160 100644 --- a/src/behaviours/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap +++ b/src/behaviours/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap @@ -1,6 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`extensions - navigation using application menu renders 1`] = `
    `; +exports[`extensions - navigation using application menu renders 1`] = ` +
    +
    +
    +`; exports[`extensions - navigation using application menu when navigating to extensions using application menu renders 1`] = `
    @@ -118,5 +124,8 @@ exports[`extensions - navigation using application menu when navigating to exten
    +
    `; diff --git a/src/behaviours/extensions/navigation-using-application-menu.test.ts b/src/behaviours/extensions/navigation-using-application-menu.test.ts index 75ffa0ebef..5d05ec31c2 100644 --- a/src/behaviours/extensions/navigation-using-application-menu.test.ts +++ b/src/behaviours/extensions/navigation-using-application-menu.test.ts @@ -6,12 +6,7 @@ import type { RenderResult } from "@testing-library/react"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; -import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable"; -import extensionsStoreInjectable from "../../extensions/extensions-store/extensions-store.injectable"; -import type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store"; -import fileSystemProvisionerStoreInjectable from "../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable"; -import type { FileSystemProvisionerStore } from "../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store"; -import focusWindowInjectable from "../../renderer/ipc-channel-listeners/focus-window.injectable"; +import focusWindowInjectable from "../../renderer/navigation/focus-window.injectable"; // TODO: Make components free of side effects by making them deterministic jest.mock("../../renderer/components/input/input"); @@ -22,11 +17,7 @@ describe("extensions - navigation using application menu", () => { let focusWindowMock: jest.Mock; beforeEach(async () => { - applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ mainDi, rendererDi }) => { - mainDi.override(isAutoUpdateEnabledInjectable, () => () => false); - rendererDi.override(extensionsStoreInjectable, () => ({}) as unknown as ExtensionsStore); - rendererDi.override(fileSystemProvisionerStoreInjectable, () => ({}) as unknown as FileSystemProvisionerStore); - + applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ rendererDi }) => { focusWindowMock = jest.fn(); rendererDi.override(focusWindowInjectable, () => focusWindowMock); diff --git a/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap b/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap index 4f1555049d..e323205008 100644 --- a/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap +++ b/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap @@ -454,5 +454,8 @@ exports[`helm-charts - navigation to Helm charts when navigating to Helm charts +
    `; diff --git a/src/behaviours/preferences/__snapshots__/closing-preferences.test.tsx.snap b/src/behaviours/preferences/__snapshots__/closing-preferences.test.tsx.snap index cb4973203d..5cdee87f39 100644 --- a/src/behaviours/preferences/__snapshots__/closing-preferences.test.tsx.snap +++ b/src/behaviours/preferences/__snapshots__/closing-preferences.test.tsx.snap @@ -356,13 +356,12 @@ exports[`preferences - closing-preferences given accessing preferences directly class="Select__control css-1s2u09g-control" >
    - Select... + Stable
    +
    `; @@ -679,6 +680,9 @@ exports[`preferences - closing-preferences given accessing preferences directly +
    `; @@ -687,6 +691,9 @@ exports[`preferences - closing-preferences given accessing preferences directly
    Some front page
    +
    `; @@ -695,6 +702,9 @@ exports[`preferences - closing-preferences given accessing preferences directly
    Some front page
    +
    `; @@ -1054,13 +1064,12 @@ exports[`preferences - closing-preferences given already in a page and then navi class="Select__control css-1s2u09g-control" >
    - Select... + Stable
    +
    `; @@ -1377,6 +1388,9 @@ exports[`preferences - closing-preferences given already in a page and then navi +
    `; @@ -1519,6 +1533,9 @@ exports[`preferences - closing-preferences given already in a page and then navi +
    `; @@ -1661,5 +1678,8 @@ exports[`preferences - closing-preferences given already in a page and then navi +
    `; diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-application-preferences.test.ts.snap b/src/behaviours/preferences/__snapshots__/navigation-to-application-preferences.test.ts.snap index f67337e80c..7cd89769f5 100644 --- a/src/behaviours/preferences/__snapshots__/navigation-to-application-preferences.test.ts.snap +++ b/src/behaviours/preferences/__snapshots__/navigation-to-application-preferences.test.ts.snap @@ -199,6 +199,9 @@ exports[`preferences - navigation to application preferences given in some child +
    `; @@ -546,13 +549,12 @@ exports[`preferences - navigation to application preferences given in some child class="Select__control css-1s2u09g-control" >
    - Select... + Stable
    +
    `; diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-editor-preferences.test.ts.snap b/src/behaviours/preferences/__snapshots__/navigation-to-editor-preferences.test.ts.snap index 4e92ac4f95..9f99faceb3 100644 --- a/src/behaviours/preferences/__snapshots__/navigation-to-editor-preferences.test.ts.snap +++ b/src/behaviours/preferences/__snapshots__/navigation-to-editor-preferences.test.ts.snap @@ -344,13 +344,12 @@ exports[`preferences - navigation to editor preferences given in preferences, wh class="Select__control css-1s2u09g-control" >
    - Select... + Stable
    +
    `; @@ -935,5 +936,8 @@ exports[`preferences - navigation to editor preferences given in preferences, wh +
    `; diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-extension-specific-preferences.test.tsx.snap b/src/behaviours/preferences/__snapshots__/navigation-to-extension-specific-preferences.test.tsx.snap index d3f42e6d63..ec00c0e498 100644 --- a/src/behaviours/preferences/__snapshots__/navigation-to-extension-specific-preferences.test.tsx.snap +++ b/src/behaviours/preferences/__snapshots__/navigation-to-extension-specific-preferences.test.tsx.snap @@ -344,13 +344,12 @@ exports[`preferences - navigation to extension specific preferences given in pre class="Select__control css-1s2u09g-control" >
    - Select... + Stable
    +
    `; @@ -884,13 +885,12 @@ exports[`preferences - navigation to extension specific preferences given in pre class="Select__control css-1s2u09g-control" >
    - Select... + Stable
    +
    `; @@ -1239,5 +1241,8 @@ exports[`preferences - navigation to extension specific preferences given in pre +
    `; diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-kubernetes-preferences.test.ts.snap b/src/behaviours/preferences/__snapshots__/navigation-to-kubernetes-preferences.test.ts.snap index 2e9b7722aa..8ab873fac4 100644 --- a/src/behaviours/preferences/__snapshots__/navigation-to-kubernetes-preferences.test.ts.snap +++ b/src/behaviours/preferences/__snapshots__/navigation-to-kubernetes-preferences.test.ts.snap @@ -344,13 +344,12 @@ exports[`preferences - navigation to kubernetes preferences given in preferences class="Select__control css-1s2u09g-control" >
    - Select... + Stable
    +
    `; @@ -836,7 +837,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences class="flex gaps" >
    +
    @@ -969,5 +983,8 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
    +
    `; diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-proxy-preferences.test.ts.snap b/src/behaviours/preferences/__snapshots__/navigation-to-proxy-preferences.test.ts.snap index 8c6507ef0a..4710bb1957 100644 --- a/src/behaviours/preferences/__snapshots__/navigation-to-proxy-preferences.test.ts.snap +++ b/src/behaviours/preferences/__snapshots__/navigation-to-proxy-preferences.test.ts.snap @@ -344,13 +344,12 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe class="Select__control css-1s2u09g-control" >
    - Select... + Stable
    +
    `; @@ -727,5 +728,8 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe +
    `; diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-telemetry-preferences.test.tsx.snap b/src/behaviours/preferences/__snapshots__/navigation-to-telemetry-preferences.test.tsx.snap index 80f5b61bb1..68901d7a4d 100644 --- a/src/behaviours/preferences/__snapshots__/navigation-to-telemetry-preferences.test.tsx.snap +++ b/src/behaviours/preferences/__snapshots__/navigation-to-telemetry-preferences.test.tsx.snap @@ -185,6 +185,9 @@ exports[`preferences - navigation to telemetry preferences given URL for Sentry +
    `; @@ -532,13 +535,12 @@ exports[`preferences - navigation to telemetry preferences given in preferences, class="Select__control css-1s2u09g-control" >
    - Select... + Stable
    +
    `; @@ -1072,13 +1076,12 @@ exports[`preferences - navigation to telemetry preferences given in preferences, class="Select__control css-1s2u09g-control" >
    - Select... + Stable
    +
    `; @@ -1429,6 +1434,9 @@ exports[`preferences - navigation to telemetry preferences given in preferences, +
    `; @@ -1568,5 +1576,8 @@ exports[`preferences - navigation to telemetry preferences given no URL for Sent +
    `; diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-terminal-preferences.test.ts.snap b/src/behaviours/preferences/__snapshots__/navigation-to-terminal-preferences.test.ts.snap index 5e5934c3bb..729309888e 100644 --- a/src/behaviours/preferences/__snapshots__/navigation-to-terminal-preferences.test.ts.snap +++ b/src/behaviours/preferences/__snapshots__/navigation-to-terminal-preferences.test.ts.snap @@ -344,13 +344,12 @@ exports[`preferences - navigation to terminal preferences given in preferences, class="Select__control css-1s2u09g-control" >
    - Select... + Stable
    +
    `; @@ -845,5 +846,8 @@ exports[`preferences - navigation to terminal preferences given in preferences, +
    `; diff --git a/src/behaviours/preferences/__snapshots__/navigation-using-application-menu.test.ts.snap b/src/behaviours/preferences/__snapshots__/navigation-using-application-menu.test.ts.snap index 141279f4e4..367ac869ba 100644 --- a/src/behaviours/preferences/__snapshots__/navigation-using-application-menu.test.ts.snap +++ b/src/behaviours/preferences/__snapshots__/navigation-using-application-menu.test.ts.snap @@ -1,6 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`preferences - navigation using application menu renders 1`] = `
    `; +exports[`preferences - navigation using application menu renders 1`] = ` +
    +
    +
    +`; exports[`preferences - navigation using application menu when navigating to preferences using application menu renders 1`] = `
    @@ -346,13 +352,12 @@ exports[`preferences - navigation using application menu when navigating to pref class="Select__control css-1s2u09g-control" >
    - Select... + Stable
    +
    `; diff --git a/src/behaviours/preferences/__snapshots__/navigation-using-tray.test.ts.snap b/src/behaviours/preferences/__snapshots__/navigation-using-tray.test.ts.snap new file mode 100644 index 0000000000..57b42580d9 --- /dev/null +++ b/src/behaviours/preferences/__snapshots__/navigation-using-tray.test.ts.snap @@ -0,0 +1,542 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`show-about-using-tray renders 1`] = ` + +
    +
    +
    + +`; + +exports[`show-about-using-tray when navigating using tray renders 1`] = ` + +
    +
    + +
    +
    +
    +

    + Application +

    +
    +
    + Theme + +
    +
    + + +
    +
    +
    + Select... +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + Extension Install Registry + +
    +
    + + +
    +
    +
    + Select... +
    +
    + +
    +
    +
    + + +
    +
    +
    +

    + This setting is to change the registry URL for installing extensions by name. + If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your + + .npmrc + + file or in the input below. +

    +
    + +
    +
    +
    +
    +
    +
    + Start-up + +
    + +
    +
    +
    +
    + Update Channel + +
    +
    + + +
    +
    +
    + Stable +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + Locale Timezone + +
    +
    + + +
    +
    +
    + Select... +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + close + + +
    + +
    +
    +
    +
    +
    +
    +
    + +`; diff --git a/src/behaviours/preferences/navigation-to-terminal-preferences.test.ts b/src/behaviours/preferences/navigation-to-terminal-preferences.test.ts index 5d7e1e08fb..73eff39006 100644 --- a/src/behaviours/preferences/navigation-to-terminal-preferences.test.ts +++ b/src/behaviours/preferences/navigation-to-terminal-preferences.test.ts @@ -5,17 +5,12 @@ import type { RenderResult } from "@testing-library/react"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; -import defaultShellInjectable from "../../renderer/components/+preferences/default-shell.injectable"; describe("preferences - navigation to terminal preferences", () => { let applicationBuilder: ApplicationBuilder; beforeEach(() => { applicationBuilder = getApplicationBuilder(); - - applicationBuilder.beforeApplicationStart(({ rendererDi }) => { - rendererDi.override(defaultShellInjectable, () => "some-default-shell"); - }); }); describe("given in preferences, when rendered", () => { diff --git a/src/behaviours/preferences/navigation-using-tray.test.ts b/src/behaviours/preferences/navigation-using-tray.test.ts new file mode 100644 index 0000000000..065cc54f4b --- /dev/null +++ b/src/behaviours/preferences/navigation-using-tray.test.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { RenderResult } from "@testing-library/react"; +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; + +describe("show-about-using-tray", () => { + let applicationBuilder: ApplicationBuilder; + let rendered: RenderResult; + + beforeEach(async () => { + applicationBuilder = getApplicationBuilder(); + + rendered = await applicationBuilder.render(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not show application preferences page yet", () => { + const actual = rendered.queryByTestId("application-preferences-page"); + + expect(actual).toBeNull(); + }); + + describe("when navigating using tray", () => { + beforeEach(async () => { + await applicationBuilder.tray.click("open-preferences"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("shows application preferences page", () => { + const actual = rendered.getByTestId("application-preferences-page"); + + expect(actual).not.toBeNull(); + }); + }); +}); diff --git a/src/behaviours/welcome/__snapshots__/navigation-using-application-menu.test.ts.snap b/src/behaviours/welcome/__snapshots__/navigation-using-application-menu.test.ts.snap index d59f7d040a..05eee498bf 100644 --- a/src/behaviours/welcome/__snapshots__/navigation-using-application-menu.test.ts.snap +++ b/src/behaviours/welcome/__snapshots__/navigation-using-application-menu.test.ts.snap @@ -1,6 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`welcome - navigation using application menu renders 1`] = `
    `; +exports[`welcome - navigation using application menu renders 1`] = ` +
    +
    +
    +`; exports[`welcome - navigation using application menu when navigating to welcome using application menu renders 1`] = `
    @@ -87,5 +93,8 @@ exports[`welcome - navigation using application menu when navigating to welcome
    +
    `; diff --git a/src/behaviours/welcome/navigation-using-application-menu.test.ts b/src/behaviours/welcome/navigation-using-application-menu.test.ts index e84e5b391f..a9f09c783c 100644 --- a/src/behaviours/welcome/navigation-using-application-menu.test.ts +++ b/src/behaviours/welcome/navigation-using-application-menu.test.ts @@ -6,16 +6,13 @@ import type { RenderResult } from "@testing-library/react"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; -import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable"; describe("welcome - navigation using application menu", () => { let applicationBuilder: ApplicationBuilder; let rendered: RenderResult; beforeEach(async () => { - applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ mainDi }) => { - mainDi.override(isAutoUpdateEnabledInjectable, () => () => false); - }); + applicationBuilder = getApplicationBuilder(); rendered = await applicationBuilder.render(); }); diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 82d7b97638..ca52f6f1d2 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -369,6 +369,8 @@ users: mockFs(mockOpts); + mainDi.override(appVersionInjectable, () => "3.6.0"); + createCluster = mainDi.inject(createClusterInjectionToken); clusterStore = mainDi.inject(clusterStoreInjectable); diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index 042b6363f7..238fc5bce8 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -21,7 +21,7 @@ jest.mock("electron", () => ({ }, })); -import { UserStore } from "../user-store"; +import type { UserStore } from "../user-store"; import { Console } from "console"; import { SemVer } from "semver"; import electron from "electron"; @@ -49,14 +49,15 @@ describe("user store tests", () => { di.override(writeFileInjectable, () => () => Promise.resolve()); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); - di.override(userStoreInjectable, () => UserStore.createInstance()); - di.permitSideEffects(getConfigurationFileModelInjectable); + di.permitSideEffects(appVersionInjectable); + di.permitSideEffects(userStoreInjectable); + + di.unoverride(userStoreInjectable); }); afterEach(() => { - UserStore.resetInstance(); mockFs.restore(); }); @@ -126,6 +127,8 @@ describe("user store tests", () => { }, }); + di.override(appVersionInjectable, () => "10.0.0"); + userStore = di.inject(userStoreInjectable); }); diff --git a/src/common/app-paths/app-path-injection-token.ts b/src/common/app-paths/app-path-injection-token.ts index 3b03e44daf..e29bcdbebf 100644 --- a/src/common/app-paths/app-path-injection-token.ts +++ b/src/common/app-paths/app-path-injection-token.ts @@ -4,12 +4,9 @@ */ import { getInjectionToken } from "@ogre-tools/injectable"; import type { PathName } from "./app-path-names"; -import { createChannel } from "../ipc-channel/create-channel/create-channel"; export type AppPaths = Record; export const appPathsInjectionToken = getInjectionToken({ id: "app-paths-token" }); -export const appPathsIpcChannel = createChannel("app-paths"); - diff --git a/src/common/app-paths/app-paths-channel.injectable.ts b/src/common/app-paths/app-paths-channel.injectable.ts new file mode 100644 index 0000000000..99fc738b41 --- /dev/null +++ b/src/common/app-paths/app-paths-channel.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 { AppPaths } from "./app-path-injection-token"; +import type { RequestChannel } from "../utils/channel/request-channel-injection-token"; +import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token"; + +export type AppPathsChannel = RequestChannel; + +const appPathsChannelInjectable = getInjectable({ + id: "app-paths-channel", + + instantiate: (): AppPathsChannel => ({ + id: "app-paths", + }), + + injectionToken: messageChannelInjectionToken, +}); + +export default appPathsChannelInjectable; diff --git a/src/common/application-update/application-update-status-channel.injectable.ts b/src/common/application-update/application-update-status-channel.injectable.ts new file mode 100644 index 0000000000..1365fd19af --- /dev/null +++ b/src/common/application-update/application-update-status-channel.injectable.ts @@ -0,0 +1,29 @@ +/** + * 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 { MessageChannel } from "../utils/channel/message-channel-injection-token"; +import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token"; + +export type ApplicationUpdateStatusEventId = + | "checking-for-updates" + | "no-updates-available" + | "download-for-update-started" + | "download-for-update-failed"; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type ApplicationUpdateStatusChannelMessage = { eventId: ApplicationUpdateStatusEventId; version?: string }; +export type ApplicationUpdateStatusChannel = MessageChannel; + +const applicationUpdateStatusChannelInjectable = getInjectable({ + id: "application-update-status-channel", + + instantiate: (): ApplicationUpdateStatusChannel => ({ + id: "application-update-status-channel", + }), + + injectionToken: messageChannelInjectionToken, +}); + +export default applicationUpdateStatusChannelInjectable; diff --git a/src/common/application-update/discovered-update-version/discovered-update-version.injectable.ts b/src/common/application-update/discovered-update-version/discovered-update-version.injectable.ts new file mode 100644 index 0000000000..60557de211 --- /dev/null +++ b/src/common/application-update/discovered-update-version/discovered-update-version.injectable.ts @@ -0,0 +1,28 @@ +/** + * 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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable"; +import type { UpdateChannel } from "../update-channels"; +import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token"; + +const discoveredUpdateVersionInjectable = getInjectable({ + id: "discovered-update-version", + + instantiate: (di) => { + const createSyncBox = di.inject(createSyncBoxInjectable); + + return createSyncBox< + | { version: string; updateChannel: UpdateChannel } + | null + >( + "discovered-update-version", + null, + ); + }, + + injectionToken: syncBoxInjectionToken, +}); + +export default discoveredUpdateVersionInjectable; diff --git a/src/common/application-update/progress-of-update-download/progress-of-update-download.injectable.ts b/src/common/application-update/progress-of-update-download/progress-of-update-download.injectable.ts new file mode 100644 index 0000000000..26ecd1d618 --- /dev/null +++ b/src/common/application-update/progress-of-update-download/progress-of-update-download.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable"; +import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token"; + +export interface ProgressOfDownload { + percentage: number; +} + +const progressOfUpdateDownloadInjectable = getInjectable({ + id: "progress-of-update-download-state", + + instantiate: (di) => { + const createSyncBox = di.inject(createSyncBoxInjectable); + + return createSyncBox("progress-of-update-download", { percentage: 0 }); + }, + + injectionToken: syncBoxInjectionToken, +}); + +export default progressOfUpdateDownloadInjectable; diff --git a/src/common/application-update/selected-update-channel/default-update-channel.injectable.ts b/src/common/application-update/selected-update-channel/default-update-channel.injectable.ts new file mode 100644 index 0000000000..3d9101b672 --- /dev/null +++ b/src/common/application-update/selected-update-channel/default-update-channel.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 { SemVer } from "semver"; +import appVersionInjectable from "../../get-configuration-file-model/app-version/app-version.injectable"; +import type { UpdateChannelId } from "../update-channels"; +import { updateChannels } from "../update-channels"; + +const defaultUpdateChannelInjectable = getInjectable({ + id: "default-update-channel", + + instantiate: (di) => { + const appVersion = di.inject(appVersionInjectable); + + const currentReleaseChannel = new SemVer(appVersion).prerelease[0]?.toString() as UpdateChannelId; + + if (currentReleaseChannel && updateChannels[currentReleaseChannel]) { + return updateChannels[currentReleaseChannel]; + } + + return updateChannels.latest; + }, +}); + +export default defaultUpdateChannelInjectable; diff --git a/src/common/application-update/selected-update-channel/selected-update-channel.injectable.ts b/src/common/application-update/selected-update-channel/selected-update-channel.injectable.ts new file mode 100644 index 0000000000..ceb47aee5e --- /dev/null +++ b/src/common/application-update/selected-update-channel/selected-update-channel.injectable.ts @@ -0,0 +1,39 @@ +/** + * 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 { IComputedValue } from "mobx"; +import { action, computed, observable } from "mobx"; +import type { UpdateChannel, UpdateChannelId } from "../update-channels"; +import { updateChannels } from "../update-channels"; +import defaultUpdateChannelInjectable from "./default-update-channel.injectable"; + +export interface SelectedUpdateChannel { + value: IComputedValue; + setValue: (channelId?: UpdateChannelId) => void; +} + +const selectedUpdateChannelInjectable = getInjectable({ + id: "selected-update-channel", + + instantiate: (di): SelectedUpdateChannel => { + const defaultUpdateChannel = di.inject(defaultUpdateChannelInjectable); + const state = observable.box(defaultUpdateChannel); + + return { + value: computed(() => state.get()), + + setValue: action((channelId) => { + const targetUpdateChannel = + channelId && updateChannels[channelId] + ? updateChannels[channelId] + : defaultUpdateChannel; + + state.set(targetUpdateChannel); + }), + }; + }, +}); + +export default selectedUpdateChannelInjectable; diff --git a/src/common/application-update/update-channels.ts b/src/common/application-update/update-channels.ts new file mode 100644 index 0000000000..c5f7b4b8c1 --- /dev/null +++ b/src/common/application-update/update-channels.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export type UpdateChannelId = "alpha" | "beta" | "latest"; + +const latestChannel: UpdateChannel = { + id: "latest", + label: "Stable", + moreStableUpdateChannel: null, +}; + +const betaChannel: UpdateChannel = { + id: "beta", + label: "Beta", + moreStableUpdateChannel: latestChannel, +}; + +const alphaChannel: UpdateChannel = { + id: "alpha", + label: "Alpha", + moreStableUpdateChannel: betaChannel, +}; + +export const updateChannels: Record = { + latest: latestChannel, + beta: betaChannel, + alpha: alphaChannel, +}; + +export interface UpdateChannel { + readonly id: UpdateChannelId; + readonly label: string; + readonly moreStableUpdateChannel: UpdateChannel | null; +} diff --git a/src/common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable.ts b/src/common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable.ts new file mode 100644 index 0000000000..e1701d7952 --- /dev/null +++ b/src/common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable.ts @@ -0,0 +1,21 @@ +/** + * 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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable"; +import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token"; + +const updateIsBeingDownloadedInjectable = getInjectable({ + id: "update-is-being-downloaded", + + instantiate: (di) => { + const createSyncBox = di.inject(createSyncBoxInjectable); + + return createSyncBox("update-is-being-downloaded", false); + }, + + injectionToken: syncBoxInjectionToken, +}); + +export default updateIsBeingDownloadedInjectable; diff --git a/src/common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable.ts b/src/common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable.ts new file mode 100644 index 0000000000..21f1c14bec --- /dev/null +++ b/src/common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable.ts @@ -0,0 +1,21 @@ +/** + * 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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable"; +import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token"; + +const updatesAreBeingDiscoveredInjectable = getInjectable({ + id: "updates-are-being-discovered", + + instantiate: (di) => { + const createSyncBox = di.inject(createSyncBoxInjectable); + + return createSyncBox("updates-are-being-discovered", false); + }, + + injectionToken: syncBoxInjectionToken, +}); + +export default updatesAreBeingDiscoveredInjectable; diff --git a/src/common/ask-boolean/ask-boolean-answer-channel.injectable.ts b/src/common/ask-boolean/ask-boolean-answer-channel.injectable.ts new file mode 100644 index 0000000000..9901c04e30 --- /dev/null +++ b/src/common/ask-boolean/ask-boolean-answer-channel.injectable.ts @@ -0,0 +1,21 @@ +/** + * 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 { MessageChannel } from "../utils/channel/message-channel-injection-token"; +import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token"; + +export type AskBooleanAnswerChannel = MessageChannel<{ id: string; value: boolean }>; + +const askBooleanAnswerChannelInjectable = getInjectable({ + id: "ask-boolean-answer-channel", + + instantiate: (): AskBooleanAnswerChannel => ({ + id: "ask-boolean-answer", + }), + + injectionToken: messageChannelInjectionToken, +}); + +export default askBooleanAnswerChannelInjectable; diff --git a/src/common/ask-boolean/ask-boolean-question-channel.injectable.ts b/src/common/ask-boolean/ask-boolean-question-channel.injectable.ts new file mode 100644 index 0000000000..664337158f --- /dev/null +++ b/src/common/ask-boolean/ask-boolean-question-channel.injectable.ts @@ -0,0 +1,23 @@ +/** + * 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 { MessageChannel } from "../utils/channel/message-channel-injection-token"; +import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token"; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type AskBooleanQuestionParameters = { id: string; title: string; question: string }; +export type AskBooleanQuestionChannel = MessageChannel; + +const askBooleanQuestionChannelInjectable = getInjectable({ + id: "ask-boolean-question-channel", + + instantiate: (): AskBooleanQuestionChannel => ({ + id: "ask-boolean-question", + }), + + injectionToken: messageChannelInjectionToken, +}); + +export default askBooleanQuestionChannelInjectable; diff --git a/src/common/front-end-routing/app-navigation-channel.injectable.ts b/src/common/front-end-routing/app-navigation-channel.injectable.ts new file mode 100644 index 0000000000..869fbfdecd --- /dev/null +++ b/src/common/front-end-routing/app-navigation-channel.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; +import type { MessageChannel } from "../utils/channel/message-channel-injection-token"; +import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token"; + +export type AppNavigationChannel = MessageChannel; + +const appNavigationChannelInjectable = getInjectable({ + id: "app-navigation-channel", + + instantiate: (): AppNavigationChannel => ({ + id: IpcRendererNavigationEvents.NAVIGATE_IN_APP, + }), + + injectionToken: messageChannelInjectionToken, +}); + +export default appNavigationChannelInjectable; diff --git a/src/common/front-end-routing/cluster-frame-navigation-channel.injectable.ts b/src/common/front-end-routing/cluster-frame-navigation-channel.injectable.ts new file mode 100644 index 0000000000..596bd6d351 --- /dev/null +++ b/src/common/front-end-routing/cluster-frame-navigation-channel.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; +import type { MessageChannel } from "../utils/channel/message-channel-injection-token"; +import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token"; + +export type ClusterFrameNavigationChannel = MessageChannel; + +const clusterFrameNavigationChannelInjectable = getInjectable({ + id: "cluster-frame-navigation-channel", + + instantiate: (): ClusterFrameNavigationChannel => ({ + id: IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER, + }), + + injectionToken: messageChannelInjectionToken, +}); + +export default clusterFrameNavigationChannelInjectable; diff --git a/src/common/front-end-routing/navigation-ipc-channel.ts b/src/common/front-end-routing/navigation-ipc-channel.ts deleted file mode 100644 index 6094664f81..0000000000 --- a/src/common/front-end-routing/navigation-ipc-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 { createChannel } from "../ipc-channel/create-channel/create-channel"; -import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; - -export const appNavigationIpcChannel = createChannel(IpcRendererNavigationEvents.NAVIGATE_IN_APP); -export const clusterFrameNavigationIpcChannel = createChannel(IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER); diff --git a/src/common/get-configuration-file-model/app-version/app-version.injectable.ts b/src/common/get-configuration-file-model/app-version/app-version.injectable.ts index 0fe3142332..5fdfd30eba 100644 --- a/src/common/get-configuration-file-model/app-version/app-version.injectable.ts +++ b/src/common/get-configuration-file-model/app-version/app-version.injectable.ts @@ -3,12 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import packageInfo from "../../../../package.json"; +import packageJsonInjectable from "../../vars/package-json.injectable"; const appVersionInjectable = getInjectable({ id: "app-version", - instantiate: () => packageInfo.version, - causesSideEffects: true, + instantiate: (di) => di.inject(packageJsonInjectable).version, }); export default appVersionInjectable; diff --git a/src/common/ipc-channel/create-channel/create-channel.ts b/src/common/ipc-channel/create-channel/create-channel.ts deleted file mode 100644 index 6b9fe1b0d9..0000000000 --- a/src/common/ipc-channel/create-channel/create-channel.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. - */ -import type { Channel } from "../channel"; - -export const createChannel = (name: string): Channel => ({ - name, - _template: null as never, -}); diff --git a/src/common/ipc/index.ts b/src/common/ipc/index.ts index 60ae46438e..bb60ce4f6c 100644 --- a/src/common/ipc/index.ts +++ b/src/common/ipc/index.ts @@ -5,5 +5,4 @@ export * from "./ipc"; export * from "./invalid-kubeconfig"; -export * from "./update-available"; export * from "./type-enforced-ipc"; diff --git a/src/common/ipc/update-available.ts b/src/common/ipc/update-available.ts deleted file mode 100644 index ed5b18b13d..0000000000 --- a/src/common/ipc/update-available.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { UpdateInfo } from "electron-updater"; - -export const UpdateAvailableChannel = "update-available"; -export const AutoUpdateChecking = "auto-update:checking"; -export const AutoUpdateNoUpdateAvailable = "auto-update:no-update"; -export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]"; - -export type UpdateAvailableFromMain = [backChannel: string, updateInfo: UpdateInfo]; - -export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain { - if (args.length !== 2) { - return false; - } - - if (typeof args[0] !== "string") { - return false; - } - - if (typeof args[1] !== "object" || args[1] === null) { - // TODO: improve this checking - return false; - } - - return true; -} - -export type BackchannelArg = { - doUpdate: false; -} | { - doUpdate: true; - now: boolean; -}; - -export type UpdateAvailableToBackchannel = [updateDecision: BackchannelArg]; - -export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel { - if (args.length !== 1) { - return false; - } - - if (typeof args[0] !== "object" || args[0] === null) { - // TODO: improve this checking - return false; - } - - return true; -} diff --git a/src/common/root-frame-rendered-channel/root-frame-rendered-channel.injectable.ts b/src/common/root-frame-rendered-channel/root-frame-rendered-channel.injectable.ts new file mode 100644 index 0000000000..a7787c6cc4 --- /dev/null +++ b/src/common/root-frame-rendered-channel/root-frame-rendered-channel.injectable.ts @@ -0,0 +1,21 @@ +/** + * 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 { MessageChannel } from "../utils/channel/message-channel-injection-token"; +import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token"; + +export type RootFrameRenderedChannel = MessageChannel; + +const rootFrameRenderedChannelInjectable = getInjectable({ + id: "root-frame-rendered-channel", + + instantiate: (): RootFrameRenderedChannel => ({ + id: "root-frame-rendered", + }), + + injectionToken: messageChannelInjectionToken, +}); + +export default rootFrameRenderedChannelInjectable; diff --git a/src/common/user-store/preferences-helpers.ts b/src/common/user-store/preferences-helpers.ts index 7bb3aee41b..ed3fb7c249 100644 --- a/src/common/user-store/preferences-helpers.ts +++ b/src/common/user-store/preferences-helpers.ts @@ -6,14 +6,11 @@ import moment from "moment-timezone"; import path from "path"; import os from "os"; -import { getAppVersion } from "../utils"; import type { editor } from "monaco-editor"; import merge from "lodash/merge"; -import { SemVer } from "semver"; import { defaultThemeId, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars"; import type { ObservableMap } from "mobx"; import { observable } from "mobx"; -import { readonly } from "../utils/readonly"; export interface KubeconfigSyncEntry extends KubeconfigSyncValue { filePath: string; @@ -296,38 +293,6 @@ const terminalConfig: PreferenceDescription = { }, }; -export interface UpdateChannelInfo { - label: string; -} - -export const updateChannels = readonly(new Map([ - ["latest", { - label: "Stable", - }], - ["beta", { - label: "Beta", - }], - ["alpha", { - label: "Alpha", - }], -])); -export const defaultUpdateChannel = new SemVer(getAppVersion()).prerelease[0]?.toString() || "latest"; - -const updateChannel: PreferenceDescription = { - fromStore(val) { - return !val || !updateChannels.has(val) - ? defaultUpdateChannel - : val; - }, - toStore(val) { - if (!updateChannels.has(val) || val === defaultUpdateChannel) { - return undefined; - } - - return val; - }, -}; - export type ExtensionRegistryLocation = "default" | "npmrc" | "custom"; export type ExtensionRegistry = { @@ -365,7 +330,7 @@ export type UserStoreFlatModel = { export type UserPreferencesModel = { [field in keyof typeof DESCRIPTORS]: PreferencesModelType; -}; +} & { updateChannel: string }; export const DESCRIPTORS = { httpsProxy, @@ -385,6 +350,5 @@ export const DESCRIPTORS = { editorConfiguration, terminalCopyOnSelect, terminalConfig, - updateChannel, extensionRegistryUrl, }; diff --git a/src/common/user-store/user-store.injectable.ts b/src/common/user-store/user-store.injectable.ts index cd44cc60e5..3b4aba0b56 100644 --- a/src/common/user-store/user-store.injectable.ts +++ b/src/common/user-store/user-store.injectable.ts @@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import { ipcMain } from "electron"; import userStoreFileNameMigrationInjectable from "./file-name-migration.injectable"; import { UserStore } from "./user-store"; +import selectedUpdateChannelInjectable from "../application-update/selected-update-channel/selected-update-channel.injectable"; const userStoreInjectable = getInjectable({ id: "user-store", @@ -17,7 +18,9 @@ const userStoreInjectable = getInjectable({ di.inject(userStoreFileNameMigrationInjectable); } - return UserStore.createInstance(); + return UserStore.createInstance({ + selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable), + }); }, causesSideEffects: true, diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index 3cfd551fd7..b806732735 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -4,7 +4,7 @@ */ import { app } from "electron"; -import semver, { SemVer } from "semver"; +import semver from "semver"; import { action, computed, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx"; import { BaseStore } from "../base-store"; import migrations from "../../migrations/user-store"; @@ -15,15 +15,22 @@ import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils"; import { DESCRIPTORS } from "./preferences-helpers"; import type { UserPreferencesModel, StoreType } from "./preferences-helpers"; import logger from "../../main/logger"; +import type { SelectedUpdateChannel } from "../application-update/selected-update-channel/selected-update-channel.injectable"; +import type { UpdateChannelId } from "../application-update/update-channels"; export interface UserStoreModel { lastSeenAppVersion: string; preferences: UserPreferencesModel; } +interface Dependencies { + selectedUpdateChannel: SelectedUpdateChannel; +} + export class UserStore extends BaseStore /* implements UserStoreFlatModel (when strict null is enabled) */ { readonly displayName = "UserStore"; - constructor() { + + constructor(private readonly dependencies: Dependencies) { super({ configName: "lens-user-store", migrations, @@ -63,7 +70,6 @@ export class UserStore extends BaseStore /* implements UserStore @observable kubectlBinariesPath!: StoreType; @observable terminalCopyOnSelect!: StoreType; @observable terminalConfig!: StoreType; - @observable updateChannel!: StoreType; @observable extensionRegistryUrl!: StoreType; /** @@ -100,10 +106,6 @@ export class UserStore extends BaseStore /* implements UserStore return this.shell || process.env.SHELL || process.env.PTYSHELL; } - @computed get isAllowedToDowngrade() { - return new SemVer(getAppVersion()).prerelease[0] !== this.updateChannel; - } - startMainReactions() { // open at system start-up reaction(() => this.openAtLogin, openAtLogin => { @@ -175,6 +177,11 @@ export class UserStore extends BaseStore /* implements UserStore this[key] = newVal; } } + + // TODO: Switch to action-based saving instead saving stores by reaction + if (preferences?.updateChannel) { + this.dependencies.selectedUpdateChannel.setValue(preferences?.updateChannel as UpdateChannelId); + } } toJSON(): UserStoreModel { @@ -185,7 +192,12 @@ export class UserStore extends BaseStore /* implements UserStore return toJS({ lastSeenAppVersion: this.lastSeenAppVersion, - preferences, + + preferences: { + ...preferences, + + updateChannel: this.dependencies.selectedUpdateChannel.value.get().id, + }, }); } } diff --git a/src/common/utils/channel/channel-injection-token.ts b/src/common/utils/channel/channel-injection-token.ts new file mode 100644 index 0000000000..6006290f89 --- /dev/null +++ b/src/common/utils/channel/channel-injection-token.ts @@ -0,0 +1,12 @@ +/** + * 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/src/common/utils/channel/channel.test.ts b/src/common/utils/channel/channel.test.ts new file mode 100644 index 0000000000..f2748104d7 --- /dev/null +++ b/src/common/utils/channel/channel.test.ts @@ -0,0 +1,273 @@ +/** + * 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 { LensWindow } from "../../../main/start-main-application/lens-window/application-window/lens-window-injection-token"; +import { lensWindowInjectionToken } from "../../../main/start-main-application/lens-window/application-window/lens-window-injection-token"; +import type { MessageToChannel } from "./message-to-channel-injection-token"; +import { messageToChannelInjectionToken } from "./message-to-channel-injection-token"; +import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; +import createLensWindowInjectable from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable"; +import closeAllWindowsInjectable from "../../../main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable"; +import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token"; +import type { MessageChannel } from "./message-channel-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-injection-token"; +import { requestChannelListenerInjectionToken } from "./request-channel-listener-injection-token"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import { getPromiseStatus } from "../../test-utils/get-promise-status"; + +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 testMessageChannel: TestMessageChannel; + let messageListenerInWindowMock: jest.Mock; + let mainDi: DiContainer; + let messageToChannel: MessageToChannel; + + beforeEach(async () => { + const applicationBuilder = getApplicationBuilder(); + + mainDi = applicationBuilder.dis.mainDi; + const rendererDi = applicationBuilder.dis.rendererDi; + + messageListenerInWindowMock = jest.fn(); + + const testChannelListenerInTestWindowInjectable = getInjectable({ + id: "test-channel-listener-in-test-window", + + instantiate: (di) => ({ + channel: di.inject(testMessageChannelInjectable), + + handler: messageListenerInWindowMock, + }), + + injectionToken: messageChannelListenerInjectionToken, + }); + + rendererDi.register(testChannelListenerInTestWindowInjectable); + + // Notice how test channel has presence in both DIs, being from common + mainDi.register(testMessageChannelInjectable); + rendererDi.register(testMessageChannelInjectable); + + testMessageChannel = mainDi.inject(testMessageChannelInjectable); + + messageToChannel = mainDi.inject( + messageToChannelInjectionToken, + ); + + await applicationBuilder.render(); + + const closeAllWindows = mainDi.inject(closeAllWindowsInjectable); + + closeAllWindows(); + }); + + describe("given window is shown", () => { + let someWindowFake: LensWindow; + + beforeEach(async () => { + someWindowFake = createTestWindow(mainDi, "some-window"); + + await someWindowFake.show(); + }); + + 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 shown windows, when sending message, triggers listeners in all windows", async () => { + const someWindowFake = createTestWindow(mainDi, "some-window"); + const someOtherWindowFake = createTestWindow(mainDi, "some-other-window"); + + await someWindowFake.show(); + await someOtherWindowFake.show(); + + 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 testMessageChannel: TestMessageChannel; + let messageListenerInMainMock: jest.Mock; + let rendererDi: DiContainer; + let mainDi: DiContainer; + let messageToChannel: MessageToChannel; + + beforeEach(async () => { + const applicationBuilder = getApplicationBuilder(); + + mainDi = applicationBuilder.dis.mainDi; + rendererDi = applicationBuilder.dis.rendererDi; + + messageListenerInMainMock = jest.fn(); + + const testChannelListenerInMainInjectable = getInjectable({ + id: "test-channel-listener-in-main", + + instantiate: (di) => ({ + channel: di.inject(testMessageChannelInjectable), + + handler: messageListenerInMainMock, + }), + + injectionToken: messageChannelListenerInjectionToken, + }); + + mainDi.register(testChannelListenerInMainInjectable); + + // Notice how test channel has presence in both DIs, being from common + mainDi.register(testMessageChannelInjectable); + rendererDi.register(testMessageChannelInjectable); + + testMessageChannel = rendererDi.inject(testMessageChannelInjectable); + + messageToChannel = rendererDi.inject( + messageToChannelInjectionToken, + ); + + await applicationBuilder.render(); + }); + + 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 testRequestChannel: TestRequestChannel; + let requestListenerInMainMock: AsyncFnMock<(arg: string) => string>; + let rendererDi: DiContainer; + let mainDi: DiContainer; + let requestFromChannel: RequestFromChannel; + + beforeEach(async () => { + const applicationBuilder = getApplicationBuilder(); + + mainDi = applicationBuilder.dis.mainDi; + rendererDi = applicationBuilder.dis.rendererDi; + + requestListenerInMainMock = asyncFn(); + + const testChannelListenerInMainInjectable = getInjectable({ + id: "test-channel-listener-in-main", + + instantiate: (di) => ({ + channel: di.inject(testRequestChannelInjectable), + + handler: requestListenerInMainMock, + }), + + injectionToken: requestChannelListenerInjectionToken, + }); + + mainDi.register(testChannelListenerInMainInjectable); + + // Notice how test channel has presence in both DIs, being from common + mainDi.register(testRequestChannelInjectable); + rendererDi.register(testRequestChannelInjectable); + + testRequestChannel = rendererDi.inject(testRequestChannelInjectable); + + requestFromChannel = rendererDi.inject( + requestFromChannelInjectionToken, + ); + + await applicationBuilder.render(); + }); + + 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"); + }); + }); + }); +}); + +const testMessageChannelInjectable = getInjectable({ + id: "some-message-test-channel", + + instantiate: (): TestMessageChannel => ({ + id: "some-message-channel-id", + }), +}); + +const testRequestChannelInjectable = getInjectable({ + id: "some-request-test-channel", + + instantiate: (): TestRequestChannel => ({ + id: "some-request-channel-id", + }), +}); + +const createTestWindow = (di: DiContainer, id: string) => { + const testWindowInjectable = getInjectable({ + id, + + instantiate: (di) => { + const createLensWindow = di.inject(createLensWindowInjectable); + + return createLensWindow({ + id, + title: "Some test window", + defaultHeight: 42, + defaultWidth: 42, + getContentSource: () => ({ url: "some-content-url" }), + resizable: true, + windowFrameUtilitiesAreShown: false, + centered: false, + }); + }, + + injectionToken: lensWindowInjectionToken, + }); + + di.register(testWindowInjectable); + + return di.inject(testWindowInjectable); +}; diff --git a/src/common/utils/channel/enlist-message-channel-listener-injection-token.ts b/src/common/utils/channel/enlist-message-channel-listener-injection-token.ts new file mode 100644 index 0000000000..fa6983e130 --- /dev/null +++ b/src/common/utils/channel/enlist-message-channel-listener-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable"; +import type { MessageChannel } from "./message-channel-injection-token"; +import type { MessageChannelListener } from "./message-channel-listener-injection-token"; + +export type EnlistMessageChannelListener = < + TChannel extends MessageChannel, +>(listener: MessageChannelListener) => () => void; + +export const enlistMessageChannelListenerInjectionToken = + getInjectionToken({ + id: "enlist-message-channel-listener", + }); diff --git a/src/common/utils/channel/enlist-request-channel-listener-injection-token.ts b/src/common/utils/channel/enlist-request-channel-listener-injection-token.ts new file mode 100644 index 0000000000..f87082c466 --- /dev/null +++ b/src/common/utils/channel/enlist-request-channel-listener-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable"; +import type { RequestChannel } from "./request-channel-injection-token"; +import type { RequestChannelListener } from "./request-channel-listener-injection-token"; + +export type EnlistRequestChannelListener = < + TChannel extends RequestChannel, +>(listener: RequestChannelListener) => () => void; + +export const enlistRequestChannelListenerInjectionToken = + getInjectionToken({ + id: "enlist-request-channel-listener", + }); diff --git a/src/common/utils/channel/listening-of-channels.injectable.ts b/src/common/utils/channel/listening-of-channels.injectable.ts new file mode 100644 index 0000000000..30fee42fb9 --- /dev/null +++ b/src/common/utils/channel/listening-of-channels.injectable.ts @@ -0,0 +1,32 @@ +/** + * 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 { disposer } from "../index"; +import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token"; +import { requestChannelListenerInjectionToken } from "./request-channel-listener-injection-token"; +import { enlistMessageChannelListenerInjectionToken } from "./enlist-message-channel-listener-injection-token"; +import { enlistRequestChannelListenerInjectionToken } from "./enlist-request-channel-listener-injection-token"; + +const listeningOfChannelsInjectable = getInjectable({ + id: "listening-of-channels", + + instantiate: (di) => { + const enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken); + const enlistRequestChannelListener = di.inject(enlistRequestChannelListenerInjectionToken); + const messageChannelListeners = di.injectMany(messageChannelListenerInjectionToken); + const requestChannelListeners = di.injectMany(requestChannelListenerInjectionToken); + + return getStartableStoppable("listening-of-channels", () => { + const messageChannelDisposers = messageChannelListeners.map(enlistMessageChannelListener); + const requestChannelDisposers = requestChannelListeners.map(enlistRequestChannelListener); + + return disposer(...messageChannelDisposers, ...requestChannelDisposers); + }); + }, +}); + + +export default listeningOfChannelsInjectable; diff --git a/src/common/utils/channel/message-channel-injection-token.ts b/src/common/utils/channel/message-channel-injection-token.ts new file mode 100644 index 0000000000..3141acedf3 --- /dev/null +++ b/src/common/utils/channel/message-channel-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable"; +import type { JsonValue } from "type-fest"; + +export interface MessageChannel { + id: string; + _messageSignature?: Message; +} + +export const messageChannelInjectionToken = getInjectionToken>({ + id: "message-channel", +}); diff --git a/src/common/utils/channel/message-channel-listener-injection-token.ts b/src/common/utils/channel/message-channel-listener-injection-token.ts new file mode 100644 index 0000000000..8879e19013 --- /dev/null +++ b/src/common/utils/channel/message-channel-listener-injection-token.ts @@ -0,0 +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 { SetRequired } from "type-fest"; +import type { MessageChannel } from "./message-channel-injection-token"; + +export interface MessageChannelListener> { + channel: TChannel; + handler: (value: SetRequired["_messageSignature"]) => void; +} + +export const messageChannelListenerInjectionToken = getInjectionToken>>( + { + id: "message-channel-listener", + }, +); diff --git a/src/common/utils/channel/message-to-channel-injection-token.ts b/src/common/utils/channel/message-to-channel-injection-token.ts new file mode 100644 index 0000000000..8c5f03b9ee --- /dev/null +++ b/src/common/utils/channel/message-to-channel-injection-token.ts @@ -0,0 +1,23 @@ +/** + * 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 { SetRequired } from "type-fest"; +import type { MessageChannel } from "./message-channel-injection-token"; + +export interface MessageToChannel { + , TMessage extends void>( + channel: TChannel, + ): void; + + >( + channel: TChannel, + message: SetRequired["_messageSignature"], + ): void; +} + +export const messageToChannelInjectionToken = + getInjectionToken({ + id: "message-to-message-channel", + }); diff --git a/src/common/utils/channel/request-channel-injection-token.ts b/src/common/utils/channel/request-channel-injection-token.ts new file mode 100644 index 0000000000..67044db878 --- /dev/null +++ b/src/common/utils/channel/request-channel-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable"; +import type { JsonValue } from "type-fest"; + +export interface RequestChannel< + Request extends JsonValue | void = void, + Response extends JsonValue | void = void, +> { + id: string; + _requestSignature?: Request; + _responseSignature?: Response; +} + +export const requestChannelInjectionToken = getInjectionToken>({ + id: "request-channel", +}); diff --git a/src/common/utils/channel/request-channel-listener-injection-token.ts b/src/common/utils/channel/request-channel-listener-injection-token.ts new file mode 100644 index 0000000000..690b96d9dc --- /dev/null +++ b/src/common/utils/channel/request-channel-listener-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable"; +import type { SetRequired } from "type-fest"; +import type { RequestChannel } from "./request-channel-injection-token"; + +export interface RequestChannelListener> { + channel: TChannel; + + handler: ( + request: SetRequired["_requestSignature"] + ) => + | SetRequired["_responseSignature"] + | Promise< + SetRequired["_responseSignature"] + >; +} + +export const requestChannelListenerInjectionToken = getInjectionToken>>( + { + id: "request-channel-listener", + }, +); diff --git a/src/common/utils/channel/request-from-channel-injection-token.ts b/src/common/utils/channel/request-from-channel-injection-token.ts new file mode 100644 index 0000000000..5f4492543f --- /dev/null +++ b/src/common/utils/channel/request-from-channel-injection-token.ts @@ -0,0 +1,21 @@ +/** + * 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 { SetRequired } from "type-fest"; +import type { RequestChannel } from "./request-channel-injection-token"; + +export type RequestFromChannel = < + TChannel extends RequestChannel, +>( + channel: TChannel, + ...request: TChannel["_requestSignature"] extends void + ? [] + : [TChannel["_requestSignature"]] +) => Promise["_responseSignature"]>; + +export const requestFromChannelInjectionToken = + getInjectionToken({ + id: "request-from-request-channel", + }); diff --git a/src/common/utils/get-random-id.injectable.ts b/src/common/utils/get-random-id.injectable.ts new file mode 100644 index 0000000000..3b96c50633 --- /dev/null +++ b/src/common/utils/get-random-id.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 { v4 as getRandomId } from "uuid"; + +const getRandomIdInjectable = getInjectable({ + id: "get-random-id", + instantiate: () => getRandomId, + causesSideEffects: true, +}); + +export default getRandomIdInjectable; diff --git a/src/common/utils/is-promise/is-promise.test.ts b/src/common/utils/is-promise/is-promise.test.ts new file mode 100644 index 0000000000..565f272ed6 --- /dev/null +++ b/src/common/utils/is-promise/is-promise.test.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { isPromise } from "./is-promise"; + +describe("isPromise", () => { + it("given promise, returns true", () => { + const actual = isPromise(new Promise(() => {})); + + expect(actual).toBe(true); + }); + + it("given non-promise, returns false", () => { + const actual = isPromise({}); + + expect(actual).toBe(false); + }); + + it("given thenable, returns false", () => { + const actual = isPromise({ then: () => {} }); + + expect(actual).toBe(false); + }); + + it("given nothing, returns false", () => { + const actual = isPromise(undefined); + + expect(actual).toBe(false); + }); +}); diff --git a/src/common/ipc-channel/channel.ts b/src/common/utils/is-promise/is-promise.ts similarity index 56% rename from src/common/ipc-channel/channel.ts rename to src/common/utils/is-promise/is-promise.ts index 2153134fff..6261f569cd 100644 --- a/src/common/ipc-channel/channel.ts +++ b/src/common/utils/is-promise/is-promise.ts @@ -2,7 +2,6 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -export interface Channel { - name: string; - _template: TInstance; +export function isPromise(reference: any): reference is Promise { + return reference?.constructor === Promise; } diff --git a/src/common/utils/sync-box/create-sync-box.injectable.ts b/src/common/utils/sync-box/create-sync-box.injectable.ts new file mode 100644 index 0000000000..2cf3de6a69 --- /dev/null +++ b/src/common/utils/sync-box/create-sync-box.injectable.ts @@ -0,0 +1,41 @@ +/** + * 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 { computed } from "mobx"; +import syncBoxChannelInjectable from "./sync-box-channel.injectable"; +import { messageToChannelInjectionToken } from "../channel/message-to-channel-injection-token"; +import syncBoxStateInjectable from "./sync-box-state.injectable"; +import type { SyncBox } from "./sync-box-injection-token"; + +const createSyncBoxInjectable = getInjectable({ + id: "create-sync-box", + + instantiate: (di) => { + const syncBoxChannel = di.inject(syncBoxChannelInjectable); + const messageToChannel = di.inject(messageToChannelInjectionToken); + const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id); + + return (id: string, initialValue: TData): SyncBox => { + const state = getSyncBoxState(id); + + state.set(initialValue); + + return { + id, + + value: computed(() => state.get()), + + set: (value) => { + state.set(value); + + messageToChannel(syncBoxChannel, { id, value }); + }, + }; + }; + }, +}); + +export default createSyncBoxInjectable; + diff --git a/src/common/utils/sync-box/sync-box-channel-listener.injectable.ts b/src/common/utils/sync-box/sync-box-channel-listener.injectable.ts new file mode 100644 index 0000000000..b603c85997 --- /dev/null +++ b/src/common/utils/sync-box/sync-box-channel-listener.injectable.ts @@ -0,0 +1,35 @@ +/** + * 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 { SyncBoxChannel } from "./sync-box-channel.injectable"; +import syncBoxChannelInjectable from "./sync-box-channel.injectable"; +import syncBoxStateInjectable from "./sync-box-state.injectable"; +import type { MessageChannelListener } from "../channel/message-channel-listener-injection-token"; +import { messageChannelListenerInjectionToken } from "../channel/message-channel-listener-injection-token"; + +const syncBoxChannelListenerInjectable = getInjectable({ + id: "sync-box-channel-listener", + + instantiate: (di): MessageChannelListener => { + const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id); + const channel = di.inject(syncBoxChannelInjectable); + + return { + channel, + + handler: ({ id, value }) => { + const target = getSyncBoxState(id); + + if (target) { + target.set(value); + } + }, + }; + }, + + injectionToken: messageChannelListenerInjectionToken, +}); + +export default syncBoxChannelListenerInjectable; diff --git a/src/common/utils/sync-box/sync-box-channel.injectable.ts b/src/common/utils/sync-box/sync-box-channel.injectable.ts new file mode 100644 index 0000000000..9389a99867 --- /dev/null +++ b/src/common/utils/sync-box/sync-box-channel.injectable.ts @@ -0,0 +1,21 @@ +/** + * 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 { MessageChannel } from "../channel/message-channel-injection-token"; +import { messageChannelInjectionToken } from "../channel/message-channel-injection-token"; + +export type SyncBoxChannel = MessageChannel<{ id: string; value: any }>; + +const syncBoxChannelInjectable = getInjectable({ + id: "sync-box-channel", + + instantiate: (): SyncBoxChannel => ({ + id: "sync-box-channel", + }), + + injectionToken: messageChannelInjectionToken, +}); + +export default syncBoxChannelInjectable; diff --git a/src/common/utils/sync-box/sync-box-initial-value-channel.injectable.ts b/src/common/utils/sync-box/sync-box-initial-value-channel.injectable.ts new file mode 100644 index 0000000000..89374c3565 --- /dev/null +++ b/src/common/utils/sync-box/sync-box-initial-value-channel.injectable.ts @@ -0,0 +1,24 @@ +/** + * 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 { RequestChannel } from "../channel/request-channel-injection-token"; +import { requestChannelInjectionToken } from "../channel/request-channel-injection-token"; + +export type SyncBoxInitialValueChannel = RequestChannel< + void, + { id: string; value: any }[] +>; + +const syncBoxInitialValueChannelInjectable = getInjectable({ + id: "sync-box-initial-value-channel", + + instantiate: (): SyncBoxInitialValueChannel => ({ + id: "sync-box-initial-value-channel", + }), + + injectionToken: requestChannelInjectionToken, +}); + +export default syncBoxInitialValueChannelInjectable; diff --git a/src/common/utils/sync-box/sync-box-injection-token.ts b/src/common/utils/sync-box/sync-box-injection-token.ts new file mode 100644 index 0000000000..d35c7d5367 --- /dev/null +++ b/src/common/utils/sync-box/sync-box-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable"; +import type { IComputedValue } from "mobx"; +import type { JsonValue } from "type-fest"; + +export interface SyncBox { + id: string; + value: IComputedValue; + set: (value: TValue) => void; +} + +export const syncBoxInjectionToken = getInjectionToken>({ + id: "sync-box", +}); diff --git a/src/common/utils/sync-box/sync-box-state.injectable.ts b/src/common/utils/sync-box/sync-box-state.injectable.ts new file mode 100644 index 0000000000..e695833da4 --- /dev/null +++ b/src/common/utils/sync-box/sync-box-state.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { observable } from "mobx"; + +const syncBoxStateInjectable = getInjectable({ + id: "sync-box-state", + + instantiate: () => observable.box(), + + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, id: string) => id, + }), +}); + +export default syncBoxStateInjectable; diff --git a/src/common/utils/sync-box/sync-box.test.ts b/src/common/utils/sync-box/sync-box.test.ts new file mode 100644 index 0000000000..2dccbd87a5 --- /dev/null +++ b/src/common/utils/sync-box/sync-box.test.ts @@ -0,0 +1,179 @@ +/** + * 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 { observe, runInAction } from "mobx"; +import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; +import createSyncBoxInjectable from "./create-sync-box.injectable"; +import { flushPromises } from "../../test-utils/flush-promises"; +import type { SyncBox } from "./sync-box-injection-token"; + +describe("sync-box", () => { + let applicationBuilder: ApplicationBuilder; + + beforeEach(() => { + applicationBuilder = getApplicationBuilder(); + + applicationBuilder.dis.mainDi.register(someInjectable); + applicationBuilder.dis.rendererDi.register(someInjectable); + }); + + // TODO: Separate starting for main application and starting of window in application builder + xdescribe("given application is started, when value is set in main", () => { + let valueInMain: string; + let syncBoxInMain: SyncBox; + + beforeEach(async () => { + syncBoxInMain = applicationBuilder.dis.mainDi.inject(someInjectable); + + // await applicationBuilder.start(); + + observe(syncBoxInMain.value, ({ newValue }) => { + valueInMain = newValue as string; + }, true); + + runInAction(() => { + syncBoxInMain.set("some-value-from-main"); + }); + }); + + it("knows value in main", () => { + expect(valueInMain).toBe("some-value-from-main"); + }); + + describe("when window starts", () => { + let valueInRenderer: string; + let syncBoxInRenderer: SyncBox; + + beforeEach(() => { + // applicationBuilder.renderWindow() + + syncBoxInRenderer = applicationBuilder.dis.rendererDi.inject(someInjectable); + + observe(syncBoxInRenderer.value, ({ newValue }) => { + valueInRenderer = newValue as string; + }, true); + }); + + it("does not have the initial value yet", () => { + expect(valueInRenderer).toBe(undefined); + }); + + describe("when getting initial value resolves", () => { + beforeEach(async () => { + await flushPromises(); + }); + + it("has value in renderer", () => { + expect(valueInRenderer).toBe("some-value-from-main"); + }); + + describe("when value is set from renderer", () => { + beforeEach(() => { + runInAction(() => { + syncBoxInRenderer.set("some-value-from-renderer"); + }); + }); + + it("has value in main", () => { + expect(valueInMain).toBe("some-value-from-renderer"); + }); + + it("has value in renderer", () => { + expect(valueInRenderer).toBe("some-value-from-renderer"); + }); + }); + }); + + describe("when value is set from renderer before getting initial value from main resolves", () => { + beforeEach(() => { + runInAction(() => { + syncBoxInRenderer.set("some-value-from-renderer"); + }); + }); + + it("has value in main", () => { + expect(valueInMain).toBe("some-value-from-renderer"); + }); + + it("has value in renderer", () => { + expect(valueInRenderer).toBe("some-value-from-renderer"); + }); + }); + }); + }); + + describe("when application starts with a window", () => { + let valueInRenderer: string; + let valueInMain: string; + let syncBoxInMain: SyncBox; + let syncBoxInRenderer: SyncBox; + + beforeEach(async () => { + syncBoxInMain = applicationBuilder.dis.mainDi.inject(someInjectable); + syncBoxInRenderer = applicationBuilder.dis.rendererDi.inject(someInjectable); + + await applicationBuilder.render(); + + observe(syncBoxInRenderer.value, ({ newValue }) => { + valueInRenderer = newValue as string; + }, true); + + observe(syncBoxInMain.value, ({ newValue }) => { + valueInMain = newValue as string; + }, true); + }); + + it("knows initial value in main", () => { + expect(valueInMain).toBe("some-initial-value"); + }); + + it("knows initial value in renderer", () => { + expect(valueInRenderer).toBe("some-initial-value"); + }); + + describe("when value is set from main", () => { + beforeEach(() => { + runInAction(() => { + syncBoxInMain.set("some-value-from-main"); + }); + }); + + it("has value in main", () => { + expect(valueInMain).toBe("some-value-from-main"); + }); + + it("has value in renderer", () => { + expect(valueInRenderer).toBe("some-value-from-main"); + }); + + describe("when value is set from renderer", () => { + beforeEach(() => { + runInAction(() => { + syncBoxInRenderer.set("some-value-from-renderer"); + }); + }); + + it("has value in main", () => { + expect(valueInMain).toBe("some-value-from-renderer"); + }); + + it("has value in renderer", () => { + expect(valueInRenderer).toBe("some-value-from-renderer"); + }); + }); + }); + }); +}); + +const someInjectable = getInjectable({ + id: "some-injectable", + + instantiate: (di) => { + const createSyncBox = di.inject(createSyncBoxInjectable); + + return createSyncBox("some-sync-box", "some-initial-value"); + }, +}); diff --git a/src/common/utils/tentative-parse-json.ts b/src/common/utils/tentative-parse-json.ts new file mode 100644 index 0000000000..a0cb089a74 --- /dev/null +++ b/src/common/utils/tentative-parse-json.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { pipeline } from "@ogre-tools/fp"; +import { defaultTo } from "lodash/fp"; +import { withErrorSuppression } from "./with-error-suppression/with-error-suppression"; + +export const tentativeParseJson = (toBeParsed: any) => pipeline( + toBeParsed, + withErrorSuppression(JSON.parse), + defaultTo(toBeParsed), +); + + diff --git a/src/common/utils/tentative-stringify-json.ts b/src/common/utils/tentative-stringify-json.ts new file mode 100644 index 0000000000..dc7206be7c --- /dev/null +++ b/src/common/utils/tentative-stringify-json.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { pipeline } from "@ogre-tools/fp"; +import { defaultTo } from "lodash/fp"; +import { withErrorSuppression } from "./with-error-suppression/with-error-suppression"; + +export const tentativeStringifyJson = (toBeParsed: any) => pipeline( + toBeParsed, + withErrorSuppression(JSON.stringify), + defaultTo(toBeParsed), +); + + diff --git a/src/common/utils/with-error-logging/with-error-logging.injectable.ts b/src/common/utils/with-error-logging/with-error-logging.injectable.ts new file mode 100644 index 0000000000..12b48c6204 --- /dev/null +++ b/src/common/utils/with-error-logging/with-error-logging.injectable.ts @@ -0,0 +1,47 @@ +/** + * 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 loggerInjectable from "../../logger.injectable"; +import { isPromise } from "../is-promise/is-promise"; + +export type WithErrorLoggingFor = ( + getErrorMessage: (error: unknown) => string +) => any>( + toBeDecorated: T +) => (...args: Parameters) => ReturnType; + +const withErrorLoggingInjectable = getInjectable({ + id: "with-error-logging", + + instantiate: (di): WithErrorLoggingFor => { + const logger = di.inject(loggerInjectable); + + return (getErrorMessage) => + (toBeDecorated) => + (...args) => { + try { + const returnValue = toBeDecorated(...args); + + if (isPromise(returnValue)) { + returnValue.catch((e) => { + const errorMessage = getErrorMessage(e); + + logger.error(errorMessage, e); + }); + } + + return returnValue; + } catch (e) { + const errorMessage = getErrorMessage(e); + + logger.error(errorMessage, e); + + throw e; + } + }; + }, +}); + +export default withErrorLoggingInjectable; diff --git a/src/common/utils/with-error-logging/with-error-logging.test.ts b/src/common/utils/with-error-logging/with-error-logging.test.ts new file mode 100644 index 0000000000..533374d9ad --- /dev/null +++ b/src/common/utils/with-error-logging/with-error-logging.test.ts @@ -0,0 +1,243 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting"; +import loggerInjectable from "../../logger.injectable"; +import type { Logger } from "../../logger"; +import withErrorLoggingInjectable from "./with-error-logging.injectable"; +import { pipeline } from "@ogre-tools/fp"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import { getPromiseStatus } from "../../test-utils/get-promise-status"; + +describe("with-error-logging", () => { + describe("given decorated sync function", () => { + let loggerStub: Logger; + let toBeDecorated: jest.Mock; + let decorated: (a: string, b: string) => number | undefined; + + beforeEach(() => { + const di = getDiForUnitTesting(); + + loggerStub = { + error: jest.fn(), + } as unknown as Logger; + + di.override(loggerInjectable, () => loggerStub); + + const withErrorLoggingFor = di.inject(withErrorLoggingInjectable); + + toBeDecorated = jest.fn(); + + decorated = pipeline( + toBeDecorated, + withErrorLoggingFor((error: any) => `some-error-message-for-${error.message}`), + ); + }); + + describe("when function does not throw and returns value", () => { + let returnValue: number | undefined; + + beforeEach(() => { + // eslint-disable-next-line unused-imports/no-unused-vars-ts + toBeDecorated.mockImplementation((_, __) => 42); + + returnValue = decorated("some-parameter", "some-other-parameter"); + }); + + it("passes arguments to decorated function", () => { + expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter"); + }); + + it("does not log error", () => { + expect(loggerStub.error).not.toHaveBeenCalled(); + }); + + it("returns the value", () => { + expect(returnValue).toBe(42); + }); + }); + + describe("when function does not throw and returns no value", () => { + let returnValue: number | undefined; + + beforeEach(() => { + // eslint-disable-next-line unused-imports/no-unused-vars-ts + toBeDecorated.mockImplementation((_, __) => undefined); + + returnValue = decorated("some-parameter", "some-other-parameter"); + }); + + it("passes arguments to decorated function", () => { + expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter"); + }); + + it("does not log error", () => { + expect(loggerStub.error).not.toHaveBeenCalled(); + }); + + it("returns nothing", () => { + expect(returnValue).toBeUndefined(); + }); + }); + + describe("when function throws", () => { + let error: Error; + + beforeEach(() => { + // eslint-disable-next-line unused-imports/no-unused-vars-ts + toBeDecorated.mockImplementation((_, __) => { + throw new Error("some-error"); + }); + + try { + decorated("some-parameter", "some-other-parameter"); + } catch (e: any) { + error = e; + } + }); + + it("passes arguments to decorated function", () => { + expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter"); + }); + + it("logs the error", () => { + expect(loggerStub.error).toHaveBeenCalledWith("some-error-message-for-some-error", error); + }); + + it("throws", () => { + expect(error.message).toBe("some-error"); + }); + }); + }); + + describe("given decorated async function", () => { + let loggerStub: Logger; + let decorated: (a: string, b: string) => Promise; + let toBeDecorated: AsyncFnMock; + + beforeEach(() => { + const di = getDiForUnitTesting(); + + loggerStub = { + error: jest.fn(), + } as unknown as Logger; + + di.override(loggerInjectable, () => loggerStub); + + const withErrorLoggingFor = di.inject(withErrorLoggingInjectable); + + toBeDecorated = asyncFn(); + + decorated = pipeline( + toBeDecorated, + + withErrorLoggingFor( + (error: any) => + `some-error-message-for-${error.message || error.someProperty}`, + ), + ); + }); + + describe("when called", () => { + let returnValuePromise: Promise; + + beforeEach(() => { + returnValuePromise = decorated("some-parameter", "some-other-parameter"); + }); + + it("passes arguments to decorated function", () => { + expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter"); + }); + + it("does not log error yet", () => { + expect(loggerStub.error).not.toHaveBeenCalled(); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(returnValuePromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + describe("when call rejects with error instance", () => { + let error: Error; + + beforeEach(async () => { + try { + await toBeDecorated.reject(new Error("some-error")); + await returnValuePromise; + } catch (e) { + error = e as Error; + } + }); + + it("logs the error", () => { + expect(loggerStub.error).toHaveBeenCalledWith("some-error-message-for-some-error", error); + }); + + it("rejects", () => { + return expect(() => returnValuePromise).rejects.toThrow("some-error"); + }); + }); + + describe("when call rejects with something else than error instance", () => { + let error: unknown; + + beforeEach(async () => { + try { + await toBeDecorated.reject({ someProperty: "some-rejection" }); + await returnValuePromise; + } catch (e) { + error = e; + } + }); + + it("logs the rejection", () => { + expect(loggerStub.error).toHaveBeenCalledWith( + "some-error-message-for-some-rejection", + error, + ); + }); + + it("rejects", () => { + return expect(() => returnValuePromise).rejects.toEqual({ someProperty: "some-rejection" }); + }); + }); + + describe("when call resolves with value", () => { + beforeEach(async () => { + await toBeDecorated.resolve(42); + }); + + it("does not log error", () => { + expect(loggerStub.error).not.toHaveBeenCalled(); + }); + + it("resolves with the value", async () => { + const returnValue = await returnValuePromise; + + expect(returnValue).toBe(42); + }); + }); + + describe("when call resolves without value", () => { + beforeEach(async () => { + await toBeDecorated.resolve(undefined); + }); + + it("does not log error", () => { + expect(loggerStub.error).not.toHaveBeenCalled(); + }); + + it("resolves without value", async () => { + const returnValue = await returnValuePromise; + + expect(returnValue).toBeUndefined(); + }); + }); + }); + }); +}); diff --git a/src/common/utils/with-error-suppression/with-error-suppression.test.ts b/src/common/utils/with-error-suppression/with-error-suppression.test.ts new file mode 100644 index 0000000000..db4909fd55 --- /dev/null +++ b/src/common/utils/with-error-suppression/with-error-suppression.test.ts @@ -0,0 +1,104 @@ +/** + * 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 { getPromiseStatus } from "../../test-utils/get-promise-status"; +import { withErrorSuppression } from "./with-error-suppression"; + +describe("with-error-suppression", () => { + describe("given decorated sync function", () => { + let toBeDecorated: jest.Mock; + let decorated: (a: string, b: string) => void; + + beforeEach(() => { + toBeDecorated = jest.fn(); + + decorated = withErrorSuppression(toBeDecorated); + }); + + describe("when function does not throw", () => { + let returnValue: void; + + beforeEach(() => { + returnValue = decorated("some-parameter", "some-other-parameter"); + }); + + it("passes arguments to decorated function", () => { + expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter"); + }); + + it("returns nothing", () => { + expect(returnValue).toBeUndefined(); + }); + }); + + describe("when function throws", () => { + let returnValue: void; + + beforeEach(() => { + // eslint-disable-next-line unused-imports/no-unused-vars-ts + toBeDecorated.mockImplementation((_, __) => { + throw new Error("some-error"); + }); + + returnValue = decorated("some-parameter", "some-other-parameter"); + }); + + it("passes arguments to decorated function", () => { + expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter"); + }); + + it("returns nothing", () => { + expect(returnValue).toBeUndefined(); + }); + }); + }); + + describe("given decorated async function", () => { + let decorated: (a: string, b: string) => Promise | Promise; + let toBeDecorated: AsyncFnMock<(a: string, b: string) => number>; + + beforeEach(() => { + toBeDecorated = asyncFn(); + + decorated = withErrorSuppression(toBeDecorated); + }); + + describe("when called", () => { + let returnValuePromise: Promise | Promise; + + beforeEach(() => { + returnValuePromise = decorated("some-parameter", "some-other-parameter"); + }); + + it("passes arguments to decorated function", () => { + expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter"); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(returnValuePromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("when call rejects, resolves with nothing", async () => { + await toBeDecorated.reject(new Error("some-error")); + + const returnValue = await returnValuePromise; + + expect(returnValue).toBeUndefined(); + }); + + it("when call resolves, resolves with the value", async () => { + await toBeDecorated.resolve(42); + + const returnValue = await returnValuePromise; + + expect(returnValue).toBe(42); + }); + }); + }); +}); diff --git a/src/common/utils/with-error-suppression/with-error-suppression.ts b/src/common/utils/with-error-suppression/with-error-suppression.ts new file mode 100644 index 0000000000..657ed13c16 --- /dev/null +++ b/src/common/utils/with-error-suppression/with-error-suppression.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { noop } from "lodash/fp"; + +export function withErrorSuppression Promise>(toBeDecorated: TDecorated): (...args: Parameters) => ReturnType | Promise; +export function withErrorSuppression any>(toBeDecorated: TDecorated): (...args: Parameters) => ReturnType | void; + +export function withErrorSuppression(toBeDecorated: any) { + return (...args: any[]) => { + try { + const returnValue = toBeDecorated(...args); + + if (isPromise(returnValue)) { + return returnValue.catch(noop); + } + + return returnValue; + } catch (e) { + return undefined; + } + }; +} + +function isPromise(reference: any): reference is Promise { + return !!reference?.then; +} diff --git a/src/common/utils/with-orphan-promise/with-orphan-promise.injectable.ts b/src/common/utils/with-orphan-promise/with-orphan-promise.injectable.ts new file mode 100644 index 0000000000..42e6cb9a61 --- /dev/null +++ b/src/common/utils/with-orphan-promise/with-orphan-promise.injectable.ts @@ -0,0 +1,29 @@ +/** + * 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 withErrorLoggingInjectable from "../with-error-logging/with-error-logging.injectable"; +import { withErrorSuppression } from "../with-error-suppression/with-error-suppression"; +import { pipeline } from "@ogre-tools/fp"; + +const withOrphanPromiseInjectable = getInjectable({ + id: "with-orphan-promise", + + instantiate: (di) => { + const withErrorLoggingFor = di.inject(withErrorLoggingInjectable); + + return Promise>(toBeDecorated: T) => + (...args: Parameters): void => { + const decorated = pipeline( + toBeDecorated, + withErrorLoggingFor(() => "Orphan promise rejection encountered"), + withErrorSuppression, + ); + + decorated(...args); + }; + }, +}); + +export default withOrphanPromiseInjectable; diff --git a/src/common/utils/with-orphan-promise/with-orphan-promise.test.ts b/src/common/utils/with-orphan-promise/with-orphan-promise.test.ts new file mode 100644 index 0000000000..cea88b2352 --- /dev/null +++ b/src/common/utils/with-orphan-promise/with-orphan-promise.test.ts @@ -0,0 +1,59 @@ +/** + * 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 { getDiForUnitTesting } from "../../../main/getDiForUnitTesting"; +import loggerInjectable from "../../logger.injectable"; +import type { Logger } from "../../logger"; +import withOrphanPromiseInjectable from "./with-orphan-promise.injectable"; + +describe("with orphan promise, when called", () => { + let toBeDecorated: AsyncFnMock<(arg1: string, arg2: string) => Promise>; + let actual: void; + let loggerStub: Logger; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + loggerStub = { error: jest.fn() } as unknown as Logger; + + di.override(loggerInjectable, () => loggerStub); + + const withOrphanPromise = di.inject(withOrphanPromiseInjectable); + + toBeDecorated = asyncFn(); + + const decorated = withOrphanPromise(toBeDecorated); + + actual = decorated("some-argument", "some-other-argument"); + }); + + it("calls decorated with arguments", () => { + expect(toBeDecorated).toHaveBeenCalledWith("some-argument", "some-other-argument"); + }); + + it("given promise returned by decorated has not been fulfilled yet, already returns nothing", () => { + expect(actual).toBeUndefined(); + }); + + it("when decorated function resolves, nothing happens", async () => { + await toBeDecorated.resolve("irrelevant"); + // Note: there is no expect, test is here only for documentation. + }); + + describe("when decorated function rejects", () => { + beforeEach(async () => { + await toBeDecorated.reject("some-error"); + }); + + it("logs the rejection", () => { + expect(loggerStub.error).toHaveBeenCalledWith("Orphan promise rejection encountered", "some-error"); + }); + + it("nothing else happens", () => { + // Note: there is no expect, test is here only for documentation. + }); + }); +}); diff --git a/src/common/vars.ts b/src/common/vars.ts index 1d47eac9d8..e11e49a7a2 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -43,8 +43,6 @@ export const isProduction = process.env.NODE_ENV === "production"; */ export const isDevelopment = !isTestEnv && !isProduction; -export const isPublishConfigured = Object.keys(packageInfo.build).includes("publish"); - export const productName = packageInfo.productName; /** diff --git a/src/common/vars/package-json.injectable.ts b/src/common/vars/package-json.injectable.ts new file mode 100644 index 0000000000..fa132be518 --- /dev/null +++ b/src/common/vars/package-json.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 packageJson from "../../../package.json"; + +const packageJsonInjectable = getInjectable({ + id: "package-json", + instantiate: () => packageJson, + causesSideEffects: true, +}); + +export default packageJsonInjectable; diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index 298c8f351b..16cebb813a 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -46,7 +46,6 @@ import { mock } from "jest-mock-extended"; import { waitUntilUsed } from "tcp-port-used"; import type { Readable } from "stream"; import { EventEmitter } from "stream"; -import { UserStore } from "../../common/user-store"; import { Console } from "console"; import { stdout, stderr } from "process"; import mockFs from "mock-fs"; @@ -120,12 +119,9 @@ describe("kube auth proxy tests", () => { createCluster = di.inject(createClusterInjectionToken); createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable); - - UserStore.createInstance(); }); afterEach(() => { - UserStore.resetInstance(); mockFs.restore(); }); diff --git a/src/main/app-paths/app-name/app-name.injectable.ts b/src/main/app-paths/app-name/app-name.injectable.ts index f4af95cf83..0a1db468d8 100644 --- a/src/main/app-paths/app-name/app-name.injectable.ts +++ b/src/main/app-paths/app-name/app-name.injectable.ts @@ -3,16 +3,17 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import packageInfo from "../../../../package.json"; import isDevelopmentInjectable from "../../../common/vars/is-development.injectable"; +import productNameInjectable from "./product-name.injectable"; const appNameInjectable = getInjectable({ id: "app-name", instantiate: (di) => { const isDevelopment = di.inject(isDevelopmentInjectable); + const productName = di.inject(productNameInjectable); - return `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`; + return `${productName}${isDevelopment ? "Dev" : ""}`; }, causesSideEffects: true, diff --git a/src/main/app-paths/app-name/product-name.injectable.ts b/src/main/app-paths/app-name/product-name.injectable.ts new file mode 100644 index 0000000000..8c5c53bfba --- /dev/null +++ b/src/main/app-paths/app-name/product-name.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 packageInfo from "../../../../package.json"; + +const productNameInjectable = getInjectable({ + id: "product-name", + instantiate: () => packageInfo.productName, + causesSideEffects: true, +}); + +export default productNameInjectable; diff --git a/src/main/app-paths/app-paths-request-channel-listener.injectable.ts b/src/main/app-paths/app-paths-request-channel-listener.injectable.ts new file mode 100644 index 0000000000..3bd0c95bf7 --- /dev/null +++ b/src/main/app-paths/app-paths-request-channel-listener.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 type { RequestChannelListener } from "../../common/utils/channel/request-channel-listener-injection-token"; +import { requestChannelListenerInjectionToken } from "../../common/utils/channel/request-channel-listener-injection-token"; +import type { AppPathsChannel } from "../../common/app-paths/app-paths-channel.injectable"; +import appPathsChannelInjectable from "../../common/app-paths/app-paths-channel.injectable"; +import appPathsInjectable from "../../common/app-paths/app-paths.injectable"; + +const appPathsRequestChannelListenerInjectable = getInjectable({ + id: "app-paths-request-channel-listener", + + instantiate: (di): RequestChannelListener => { + const channel = di.inject(appPathsChannelInjectable); + const appPaths = di.inject(appPathsInjectable); + + return { + channel, + handler: () => appPaths, + }; + }, + injectionToken: requestChannelListenerInjectionToken, +}); + +export default appPathsRequestChannelListenerInjectable; diff --git a/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts b/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts index 6cb937f45d..8e28c806d7 100644 --- a/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts +++ b/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts @@ -6,7 +6,6 @@ import electronAppInjectable from "../../electron-app/electron-app.injectable"; import getElectronAppPathInjectable from "./get-electron-app-path.injectable"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import type { App } from "electron"; -import registerChannelInjectable from "../register-channel/register-channel.injectable"; import joinPathsInjectable from "../../../common/path/join-paths.injectable"; import { joinPathsFake } from "../../../common/test-utils/join-paths-fake"; @@ -32,7 +31,6 @@ describe("get-electron-app-path", () => { } as App; di.override(electronAppInjectable, () => appStub); - di.override(registerChannelInjectable, () => () => undefined); di.override(joinPathsInjectable, () => joinPathsFake); getElectronAppPath = di.inject(getElectronAppPathInjectable) as (name: string) => string; diff --git a/src/main/app-paths/register-channel/register-channel.injectable.ts b/src/main/app-paths/register-channel/register-channel.injectable.ts deleted file mode 100644 index d0b517cf25..0000000000 --- a/src/main/app-paths/register-channel/register-channel.injectable.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 { getInjectable } from "@ogre-tools/injectable"; -import ipcMainInjectable from "./ipc-main/ipc-main.injectable"; -import { registerChannel } from "./register-channel"; - -const registerChannelInjectable = getInjectable({ - id: "register-channel", - - instantiate: (di) => registerChannel({ - ipcMain: di.inject(ipcMainInjectable), - }), -}); - -export default registerChannelInjectable; diff --git a/src/main/app-paths/register-channel/register-channel.ts b/src/main/app-paths/register-channel/register-channel.ts deleted file mode 100644 index 73f3e13243..0000000000 --- a/src/main/app-paths/register-channel/register-channel.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { IpcMain } from "electron"; -import type { Channel } from "../../../common/ipc-channel/channel"; - -interface Dependencies { - ipcMain: IpcMain; -} - -export const registerChannel = - ({ ipcMain }: Dependencies) => - , TInstance>( - channel: TChannel, - getValue: () => TInstance, - ) => - ipcMain.handle(channel.name, getValue); diff --git a/src/main/app-paths/setup-app-paths.injectable.ts b/src/main/app-paths/setup-app-paths.injectable.ts index 9a4283f063..816c58db8b 100644 --- a/src/main/app-paths/setup-app-paths.injectable.ts +++ b/src/main/app-paths/setup-app-paths.injectable.ts @@ -12,8 +12,6 @@ import appPathsStateInjectable from "../../common/app-paths/app-paths-state.inje import { pathNames } from "../../common/app-paths/app-path-names"; import { fromPairs, map } from "lodash/fp"; import { pipeline } from "@ogre-tools/fp"; -import { appPathsIpcChannel } from "../../common/app-paths/app-path-injection-token"; -import registerChannelInjectable from "./register-channel/register-channel.injectable"; import joinPathsInjectable from "../../common/path/join-paths.injectable"; import { beforeElectronIsReadyInjectionToken } from "../start-main-application/runnable-tokens/before-electron-is-ready-injection-token"; @@ -25,7 +23,6 @@ const setupAppPathsInjectable = getInjectable({ const appName = di.inject(appNameInjectable); const getAppPath = di.inject(getElectronAppPathInjectable); const appPathsState = di.inject(appPathsStateInjectable); - const registerChannel = di.inject(registerChannelInjectable); const directoryForIntegrationTesting = di.inject(directoryForIntegrationTestingInjectable); const joinPaths = di.inject(joinPathsInjectable); @@ -46,8 +43,6 @@ const setupAppPathsInjectable = getInjectable({ ) as AppPaths; appPathsState.set(appPaths); - - registerChannel(appPathsIpcChannel, () => appPaths); }, }; }, diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts deleted file mode 100644 index 77daf6b4b6..0000000000 --- a/src/main/app-updater.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { UpdateInfo } from "electron-updater"; -import { autoUpdater } from "electron-updater"; -import logger from "./logger"; -import { isPublishConfigured, isTestEnv } from "../common/vars"; -import { delay } from "../common/utils"; -import type { UpdateAvailableToBackchannel } from "../common/ipc"; -import { areArgsUpdateAvailableToBackchannel, AutoUpdateChecking, AutoUpdateLogPrefix, AutoUpdateNoUpdateAvailable, broadcastMessage, onceCorrect, UpdateAvailableChannel } from "../common/ipc"; -import { once } from "lodash"; -import { ipcMain } from "electron"; -import { nextUpdateChannel } from "./utils/update-channel"; -import { UserStore } from "../common/user-store"; - -let installVersion: undefined | string; - -export function isAutoUpdateEnabled() { - return autoUpdater.isUpdaterActive() && isPublishConfigured; -} - -function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) { - if (arg.doUpdate) { - if (arg.now) { - logger.info(`${AutoUpdateLogPrefix}: User chose to update now`); - autoUpdater.quitAndInstall(true, true); - } else { - logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`); - } - } else { - logger.info(`${AutoUpdateLogPrefix}: User chose not to update, will update on quit anyway`); - } -} - -autoUpdater.logger = { - info: message => logger.info(`[AUTO-UPDATE]: electron-updater: %s`, message), - warn: message => logger.warn(`[AUTO-UPDATE]: electron-updater: %s`, message), - error: message => logger.error(`[AUTO-UPDATE]: electron-updater: %s`, message), - debug: message => logger.debug(`[AUTO-UPDATE]: electron-updater: %s`, message), -}; - -interface Dependencies { - isAutoUpdateEnabled: () => boolean; -} - -/** - * starts the automatic update checking - * @param interval milliseconds between interval to check on, defaults to 2h - */ -export const startUpdateChecking = ({ isAutoUpdateEnabled } : Dependencies) => once(function (interval = 1000 * 60 * 60 * 2): void { - if (!isAutoUpdateEnabled() || isTestEnv) { - return; - } - - const userStore = UserStore.getInstance(); - - autoUpdater.autoDownload = false; - autoUpdater.autoInstallOnAppQuit = true; - autoUpdater.channel = userStore.updateChannel; - autoUpdater.allowDowngrade = userStore.isAllowedToDowngrade; - - autoUpdater - .on("update-available", (info: UpdateInfo) => { - if (installVersion === info.version) { - // same version, don't broadcast - return; - } - - installVersion = info.version; - - autoUpdater.downloadUpdate() - .catch(error => logger.error(`${AutoUpdateLogPrefix}: failed to download update`, { error: String(error) })); - }) - .on("update-downloaded", (info: UpdateInfo) => { - try { - const backchannel = `auto-update:${info.version}`; - - ipcMain.removeAllListeners(backchannel); // only one handler should be present - - // make sure that the handler is in place before broadcasting (prevent race-condition) - onceCorrect({ - source: ipcMain, - channel: backchannel, - listener: handleAutoUpdateBackChannel, - verifier: areArgsUpdateAvailableToBackchannel, - }); - logger.info(`${AutoUpdateLogPrefix}: broadcasting update available`, { backchannel, version: info.version }); - broadcastMessage(UpdateAvailableChannel, backchannel, info); - } catch (error) { - logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error }); - installVersion = undefined; - } - }) - .on("update-not-available", () => { - const nextChannel = nextUpdateChannel(userStore.updateChannel, autoUpdater.channel); - - logger.info(`${AutoUpdateLogPrefix}: update not available from ${autoUpdater.channel}, will check ${nextChannel} channel next`); - - if (nextChannel !== autoUpdater.channel) { - autoUpdater.channel = nextChannel; - autoUpdater.checkForUpdates() - .catch(error => logger.error(`${AutoUpdateLogPrefix}: failed with an error`, error)); - } else { - broadcastMessage(AutoUpdateNoUpdateAvailable); - } - }); - - async function helper() { - while (true) { - await checkForUpdates(); - await delay(interval); - } - } - - helper(); -}); - -export async function checkForUpdates(): Promise { - const userStore = UserStore.getInstance(); - - try { - logger.info(`📡 Checking for app updates`); - - autoUpdater.channel = userStore.updateChannel; - autoUpdater.allowDowngrade = userStore.isAllowedToDowngrade; - broadcastMessage(AutoUpdateChecking); - await autoUpdater.checkForUpdates(); - } catch (error) { - logger.error(`${AutoUpdateLogPrefix}: failed with an error`, error); - } -} diff --git a/src/main/application-update/check-for-platform-updates/check-for-platform-updates.injectable.ts b/src/main/application-update/check-for-platform-updates/check-for-platform-updates.injectable.ts new file mode 100644 index 0000000000..286999c484 --- /dev/null +++ b/src/main/application-update/check-for-platform-updates/check-for-platform-updates.injectable.ts @@ -0,0 +1,60 @@ +/** + * 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 electronUpdaterInjectable from "../../electron-app/features/electron-updater.injectable"; +import type { UpdateChannel } from "../../../common/application-update/update-channels"; +import loggerInjectable from "../../../common/logger.injectable"; +import type { UpdateCheckResult } from "electron-updater"; + +export type CheckForUpdatesResult = { + updateWasDiscovered: false; +} | { + updateWasDiscovered: true; + version: string; +}; + +export type CheckForPlatformUpdates = (updateChannel: UpdateChannel, opts: { allowDowngrade: boolean }) => Promise; + +const checkForPlatformUpdatesInjectable = getInjectable({ + id: "check-for-platform-updates", + + instantiate: (di): CheckForPlatformUpdates => { + const electronUpdater = di.inject(electronUpdaterInjectable); + const logger = di.inject(loggerInjectable); + + return async (updateChannel, { allowDowngrade }) => { + electronUpdater.channel = updateChannel.id; + electronUpdater.autoDownload = false; + electronUpdater.allowDowngrade = allowDowngrade; + + let result: UpdateCheckResult; + + try { + result = await electronUpdater.checkForUpdates(); + } catch (error) { + logger.error("[UPDATE-APP/CHECK-FOR-UPDATES]", error); + + return { + updateWasDiscovered: false, + }; + } + + const { updateInfo, cancellationToken } = result; + + if (!cancellationToken) { + return { + updateWasDiscovered: false, + }; + } + + return { + updateWasDiscovered: true, + version: updateInfo.version, + }; + }; + }, +}); + +export default checkForPlatformUpdatesInjectable; diff --git a/src/main/application-update/check-for-platform-updates/check-for-platform-updates.test.ts b/src/main/application-update/check-for-platform-updates/check-for-platform-updates.test.ts new file mode 100644 index 0000000000..b826a1a5a7 --- /dev/null +++ b/src/main/application-update/check-for-platform-updates/check-for-platform-updates.test.ts @@ -0,0 +1,128 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import electronUpdaterInjectable from "../../electron-app/features/electron-updater.injectable"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { AppUpdater, UpdateCheckResult } from "electron-updater"; +import type { CheckForPlatformUpdates } from "./check-for-platform-updates.injectable"; +import checkForPlatformUpdatesInjectable from "./check-for-platform-updates.injectable"; +import type { UpdateChannel, UpdateChannelId } from "../../../common/application-update/update-channels"; +import { getPromiseStatus } from "../../../common/test-utils/get-promise-status"; +import loggerInjectable from "../../../common/logger.injectable"; +import type { Logger } from "../../../common/logger"; + +describe("check-for-platform-updates", () => { + let checkForPlatformUpdates: CheckForPlatformUpdates; + let electronUpdaterFake: AppUpdater; + let checkForUpdatesMock: AsyncFnMock<() => UpdateCheckResult>; + let logErrorMock: jest.Mock; + + beforeEach(() => { + const di = getDiForUnitTesting(); + + checkForUpdatesMock = asyncFn(); + + electronUpdaterFake = { + channel: undefined, + autoDownload: undefined, + allowDowngrade: undefined, + + checkForUpdates: checkForUpdatesMock, + } as unknown as AppUpdater; + + di.override(electronUpdaterInjectable, () => electronUpdaterFake); + + logErrorMock = jest.fn(); + + di.override(loggerInjectable, () => ({ error: logErrorMock }) as unknown as Logger); + + checkForPlatformUpdates = di.inject(checkForPlatformUpdatesInjectable); + }); + + describe("when called", () => { + let actualPromise: Promise; + + beforeEach(() => { + const testUpdateChannel: UpdateChannel = { + id: "some-update-channel" as UpdateChannelId, + label: "Some update channel", + moreStableUpdateChannel: null, + }; + + actualPromise = checkForPlatformUpdates(testUpdateChannel, { allowDowngrade: true }); + }); + + it("sets update channel", () => { + expect(electronUpdaterFake.channel).toBe("some-update-channel"); + }); + + it("sets flag for allowing downgrade", () => { + expect(electronUpdaterFake.allowDowngrade).toBe(true); + }); + + it("disables auto downloading for being controlled", () => { + expect(electronUpdaterFake.autoDownload).toBe(false); + }); + + it("checks for updates", () => { + expect(checkForUpdatesMock).toHaveBeenCalled(); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("when checking for updates resolves with update, resolves with the discovered update", async () => { + await checkForUpdatesMock.resolve({ + updateInfo: { + version: "some-version", + }, + + cancellationToken: "some-cancellation-token", + } as unknown as UpdateCheckResult); + + const actual = await actualPromise; + + expect(actual).toEqual({ updateWasDiscovered: true, version: "some-version" }); + }); + + it("when checking for updates resolves without update, resolves with update not being discovered", async () => { + await checkForUpdatesMock.resolve({ + updateInfo: { + version: "some-version-that-matches-to-current-installed-version", + }, + + cancellationToken: null, + } as unknown as UpdateCheckResult); + + const actual = await actualPromise; + + expect(actual).toEqual({ updateWasDiscovered: false }); + }); + + describe("when checking for updates rejects", () => { + let errorStub: Error; + + beforeEach(() => { + errorStub = new Error("Some error"); + + checkForUpdatesMock.reject(errorStub); + }); + + it("logs errors", () => { + expect(logErrorMock).toHaveBeenCalledWith("[UPDATE-APP/CHECK-FOR-UPDATES]", errorStub); + }); + + it("resolves with update not being discovered", async () => { + const actual = await actualPromise; + + expect(actual).toEqual({ updateWasDiscovered: false }); + }); + }); + }); +}); diff --git a/src/main/application-update/check-for-updates-tray-item.injectable.ts b/src/main/application-update/check-for-updates-tray-item.injectable.ts new file mode 100644 index 0000000000..5ff4be731a --- /dev/null +++ b/src/main/application-update/check-for-updates-tray-item.injectable.ts @@ -0,0 +1,79 @@ +/** + * 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 { computed } from "mobx"; +import updatingIsEnabledInjectable from "./updating-is-enabled.injectable"; +import { trayMenuItemInjectionToken } from "../tray/tray-menu-item/tray-menu-item-injection-token"; +import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable"; +import discoveredUpdateVersionInjectable from "../../common/application-update/discovered-update-version/discovered-update-version.injectable"; +import updateIsBeingDownloadedInjectable from "../../common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable"; +import updatesAreBeingDiscoveredInjectable from "../../common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable"; +import progressOfUpdateDownloadInjectable from "../../common/application-update/progress-of-update-download/progress-of-update-download.injectable"; +import assert from "assert"; +import processCheckingForUpdatesInjectable from "./check-for-updates/process-checking-for-updates.injectable"; +import { withErrorSuppression } from "../../common/utils/with-error-suppression/with-error-suppression"; +import { pipeline } from "@ogre-tools/fp"; +import withErrorLoggingInjectable from "../../common/utils/with-error-logging/with-error-logging.injectable"; + +const checkForUpdatesTrayItemInjectable = getInjectable({ + id: "check-for-updates-tray-item", + + instantiate: (di) => { + const showApplicationWindow = di.inject(showApplicationWindowInjectable); + const updatingIsEnabled = di.inject(updatingIsEnabledInjectable); + const progressOfUpdateDownload = di.inject(progressOfUpdateDownloadInjectable); + const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable); + const downloadingUpdateState = di.inject(updateIsBeingDownloadedInjectable); + const checkingForUpdatesState = di.inject(updatesAreBeingDiscoveredInjectable); + const processCheckingForUpdates = di.inject(processCheckingForUpdatesInjectable); + const withErrorLoggingFor = di.inject(withErrorLoggingInjectable); + + return { + id: "check-for-updates", + parentId: null, + orderNumber: 30, + + label: computed(() => { + if (downloadingUpdateState.value.get()) { + const discoveredVersion = discoveredVersionState.value.get(); + + assert(discoveredVersion); + + const roundedPercentage = Math.round(progressOfUpdateDownload.value.get().percentage); + + return `Downloading update ${discoveredVersion.version} (${roundedPercentage}%)...`; + } + + if (checkingForUpdatesState.value.get()) { + return "Checking for updates..."; + } + + return "Check for updates"; + }), + + enabled: computed(() => !checkingForUpdatesState.value.get() && !downloadingUpdateState.value.get()), + + visible: computed(() => updatingIsEnabled), + + click: pipeline( + async () => { + await processCheckingForUpdates(); + + await showApplicationWindow(); + }, + + withErrorLoggingFor(() => "[TRAY]: Checking for updates failed."), + + // TODO: Find out how to improve typing so that instead of + // x => withErrorSuppression(x) there could only be withErrorSuppression + (x) => withErrorSuppression(x), + ), + }; + }, + + injectionToken: trayMenuItemInjectionToken, +}); + +export default checkForUpdatesTrayItemInjectable; diff --git a/src/main/application-update/check-for-updates/broadcast-change-in-updating-status.injectable.ts b/src/main/application-update/check-for-updates/broadcast-change-in-updating-status.injectable.ts new file mode 100644 index 0000000000..7e9257e966 --- /dev/null +++ b/src/main/application-update/check-for-updates/broadcast-change-in-updating-status.injectable.ts @@ -0,0 +1,23 @@ +/** + * 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 { ApplicationUpdateStatusChannelMessage } from "../../../common/application-update/application-update-status-channel.injectable"; +import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; +import applicationUpdateStatusChannelInjectable from "../../../common/application-update/application-update-status-channel.injectable"; + +const broadcastChangeInUpdatingStatusInjectable = getInjectable({ + id: "broadcast-change-in-updating-status", + + instantiate: (di) => { + const messageToChannel = di.inject(messageToChannelInjectionToken); + const applicationUpdateStatusChannel = di.inject(applicationUpdateStatusChannelInjectable); + + return (data: ApplicationUpdateStatusChannelMessage) => { + messageToChannel(applicationUpdateStatusChannel, data); + }; + }, +}); + +export default broadcastChangeInUpdatingStatusInjectable; diff --git a/src/main/application-update/check-for-updates/check-for-updates-starting-from-channel.injectable.ts b/src/main/application-update/check-for-updates/check-for-updates-starting-from-channel.injectable.ts new file mode 100644 index 0000000000..caf695ff80 --- /dev/null +++ b/src/main/application-update/check-for-updates/check-for-updates-starting-from-channel.injectable.ts @@ -0,0 +1,54 @@ +/** + * 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 { UpdateChannel } from "../../../common/application-update/update-channels"; +import checkForPlatformUpdatesInjectable from "../check-for-platform-updates/check-for-platform-updates.injectable"; +import updateCanBeDowngradedInjectable from "./update-can-be-downgraded.injectable"; + +export type CheckForUpdatesFromChannelResult = { + updateWasDiscovered: false; +} | { + updateWasDiscovered: true; + version: string; + actualUpdateChannel: UpdateChannel; +}; + +const checkForUpdatesStartingFromChannelInjectable = getInjectable({ + id: "check-for-updates-starting-from-channel", + + instantiate: (di) => { + const checkForPlatformUpdates = di.inject( + checkForPlatformUpdatesInjectable, + ); + + const updateCanBeDowngraded = di.inject(updateCanBeDowngradedInjectable); + + const _recursiveCheck = async ( + updateChannel: UpdateChannel, + ): Promise => { + const result = await checkForPlatformUpdates(updateChannel, { + allowDowngrade: updateCanBeDowngraded.get(), + }); + + if (result.updateWasDiscovered) { + return { + updateWasDiscovered: true, + version: result.version, + actualUpdateChannel: updateChannel, + }; + } + + if (updateChannel.moreStableUpdateChannel) { + return await _recursiveCheck(updateChannel.moreStableUpdateChannel); + } + + return { updateWasDiscovered: false }; + }; + + return _recursiveCheck; + }, +}); + +export default checkForUpdatesStartingFromChannelInjectable; diff --git a/src/main/application-update/check-for-updates/process-checking-for-updates.injectable.ts b/src/main/application-update/check-for-updates/process-checking-for-updates.injectable.ts new file mode 100644 index 0000000000..2688d00d4a --- /dev/null +++ b/src/main/application-update/check-for-updates/process-checking-for-updates.injectable.ts @@ -0,0 +1,93 @@ +/** + * 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 selectedUpdateChannelInjectable from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable"; +import updatesAreBeingDiscoveredInjectable from "../../../common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable"; +import discoveredUpdateVersionInjectable from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable"; +import { runInAction } from "mobx"; +import askBooleanInjectable from "../../ask-boolean/ask-boolean.injectable"; +import quitAndInstallUpdateInjectable from "../../electron-app/features/quit-and-install-update.injectable"; +import downloadUpdateInjectable from "../download-update/download-update.injectable"; +import broadcastChangeInUpdatingStatusInjectable from "./broadcast-change-in-updating-status.injectable"; +import checkForUpdatesStartingFromChannelInjectable from "./check-for-updates-starting-from-channel.injectable"; +import withOrphanPromiseInjectable from "../../../common/utils/with-orphan-promise/with-orphan-promise.injectable"; + +const processCheckingForUpdatesInjectable = getInjectable({ + id: "process-checking-for-updates", + + instantiate: (di) => { + const askBoolean = di.inject(askBooleanInjectable); + const quitAndInstallUpdate = di.inject(quitAndInstallUpdateInjectable); + const downloadUpdate = di.inject(downloadUpdateInjectable); + const selectedUpdateChannel = di.inject(selectedUpdateChannelInjectable); + const broadcastChangeInUpdatingStatus = di.inject(broadcastChangeInUpdatingStatusInjectable); + const checkingForUpdatesState = di.inject(updatesAreBeingDiscoveredInjectable); + const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable); + const checkForUpdatesStartingFromChannel = di.inject(checkForUpdatesStartingFromChannelInjectable); + const withOrphanPromise = di.inject(withOrphanPromiseInjectable); + + return async () => { + broadcastChangeInUpdatingStatus({ eventId: "checking-for-updates" }); + + runInAction(() => { + checkingForUpdatesState.set(true); + }); + + const result = await checkForUpdatesStartingFromChannel(selectedUpdateChannel.value.get()); + + if (!result.updateWasDiscovered) { + broadcastChangeInUpdatingStatus({ eventId: "no-updates-available" }); + + runInAction(() => { + discoveredVersionState.set(null); + checkingForUpdatesState.set(false); + }); + + return; + } + + const { version, actualUpdateChannel } = result; + + broadcastChangeInUpdatingStatus({ + eventId: "download-for-update-started", + version, + }); + + runInAction(() => { + discoveredVersionState.set({ + version, + updateChannel: actualUpdateChannel, + }); + + checkingForUpdatesState.set(false); + }); + + withOrphanPromise(async () => { + const { downloadWasSuccessful } = await downloadUpdate(); + + if (!downloadWasSuccessful) { + broadcastChangeInUpdatingStatus({ + eventId: "download-for-update-failed", + }); + + return; + } + + const userWantsToInstallUpdate = await askBoolean({ + title: "Update Available", + + question: `Version ${version} of Lens IDE is available and ready to be installed. Would you like to update now?\n\n` + + `Lens should restart automatically, if it doesn't please restart manually. Installed extensions might require updating.`, + }); + + if (userWantsToInstallUpdate) { + quitAndInstallUpdate(); + } + })(); + }; + }, +}); + +export default processCheckingForUpdatesInjectable; diff --git a/src/main/application-update/check-for-updates/update-can-be-downgraded.injectable.ts b/src/main/application-update/check-for-updates/update-can-be-downgraded.injectable.ts new file mode 100644 index 0000000000..72ea5c4023 --- /dev/null +++ b/src/main/application-update/check-for-updates/update-can-be-downgraded.injectable.ts @@ -0,0 +1,29 @@ +/** + * 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 { computed } from "mobx"; +import selectedUpdateChannelInjectable from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable"; +import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable"; +import { SemVer } from "semver"; + +const updateCanBeDowngradedInjectable = getInjectable({ + id: "update-can-be-downgraded", + + instantiate: (di) => { + const selectedUpdateChannel = di.inject(selectedUpdateChannelInjectable); + const appVersion = di.inject(appVersionInjectable); + + return computed(() => { + const semVer = new SemVer(appVersion); + + return ( + semVer.prerelease[0] !== + selectedUpdateChannel.value.get().id + ); + }); + }, +}); + +export default updateCanBeDowngradedInjectable; diff --git a/src/main/application-update/download-platform-update/download-platform-update.injectable.ts b/src/main/application-update/download-platform-update/download-platform-update.injectable.ts new file mode 100644 index 0000000000..374efd2caf --- /dev/null +++ b/src/main/application-update/download-platform-update/download-platform-update.injectable.ts @@ -0,0 +1,45 @@ +/** + * 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 electronUpdaterInjectable from "../../electron-app/features/electron-updater.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import type { ProgressInfo } from "electron-updater"; +import type { ProgressOfDownload } from "../../../common/application-update/progress-of-update-download/progress-of-update-download.injectable"; + +export type DownloadPlatformUpdate = ( + onDownloadProgress: (arg: ProgressOfDownload) => void +) => Promise<{ downloadWasSuccessful: boolean }>; + +const downloadPlatformUpdateInjectable = getInjectable({ + id: "download-platform-update", + + instantiate: (di): DownloadPlatformUpdate => { + const electronUpdater = di.inject(electronUpdaterInjectable); + const logger = di.inject(loggerInjectable); + + return async (onDownloadProgress) => { + onDownloadProgress({ percentage: 0 }); + + const updateDownloadProgress = ({ percent: percentage }: ProgressInfo) => + onDownloadProgress({ percentage }); + + electronUpdater.on("download-progress", updateDownloadProgress); + + try { + await electronUpdater.downloadUpdate(); + } catch(error) { + logger.error("[UPDATE-APP/DOWNLOAD]", error); + + return { downloadWasSuccessful: false }; + } finally { + electronUpdater.off("download-progress", updateDownloadProgress); + } + + return { downloadWasSuccessful: true }; + }; + }, +}); + +export default downloadPlatformUpdateInjectable; diff --git a/src/main/application-update/download-platform-update/download-platform-update.test.ts b/src/main/application-update/download-platform-update/download-platform-update.test.ts new file mode 100644 index 0000000000..04507be980 --- /dev/null +++ b/src/main/application-update/download-platform-update/download-platform-update.test.ts @@ -0,0 +1,155 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import electronUpdaterInjectable from "../../electron-app/features/electron-updater.injectable"; +import type { DownloadPlatformUpdate } from "./download-platform-update.injectable"; +import downloadPlatformUpdateInjectable from "./download-platform-update.injectable"; +import type { AppUpdater } from "electron-updater"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import { getPromiseStatus } from "../../../common/test-utils/get-promise-status"; +import type { DiContainer } from "@ogre-tools/injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import type { Logger } from "../../../common/logger"; + +describe("download-platform-update", () => { + let downloadPlatformUpdate: DownloadPlatformUpdate; + let downloadUpdateMock: AsyncFnMock<() => void>; + let electronUpdaterFake: AppUpdater; + let electronUpdaterOnMock: jest.Mock; + let electronUpdaterOffMock: jest.Mock; + let di: DiContainer; + let logErrorMock: jest.Mock; + + beforeEach(() => { + di = getDiForUnitTesting(); + + downloadUpdateMock = asyncFn(); + electronUpdaterOnMock = jest.fn(); + electronUpdaterOffMock = jest.fn(); + + electronUpdaterFake = { + channel: undefined, + autoDownload: undefined, + + on: electronUpdaterOnMock, + off: electronUpdaterOffMock, + + downloadUpdate: downloadUpdateMock, + } as unknown as AppUpdater; + + di.override(electronUpdaterInjectable, () => electronUpdaterFake); + + logErrorMock = jest.fn(); + di.override(loggerInjectable, () => ({ error: logErrorMock }) as unknown as Logger); + + downloadPlatformUpdate = di.inject(downloadPlatformUpdateInjectable); + }); + + describe("when called", () => { + let actualPromise: Promise<{ downloadWasSuccessful: boolean }>; + let onDownloadProgressMock: jest.Mock; + + beforeEach(() => { + onDownloadProgressMock = jest.fn(); + + actualPromise = downloadPlatformUpdate(onDownloadProgressMock); + }); + + it("calls for downloading of update", () => { + expect(downloadUpdateMock).toHaveBeenCalled(); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("starts progress of download from 0", () => { + expect(onDownloadProgressMock).toHaveBeenCalledWith({ percentage: 0 }); + }); + + describe("when downloading progresses", () => { + beforeEach(() => { + onDownloadProgressMock.mockClear(); + + const [, callback] = electronUpdaterOnMock.mock.calls.find( + ([event]) => event === "download-progress", + ); + + callback({ + percent: 42, + total: 0, + delta: 0, + transferred: 0, + bytesPerSecond: 0, + }); + }); + + it("updates progress of the download", () => { + expect(onDownloadProgressMock).toHaveBeenCalledWith({ percentage: 42 }); + }); + + describe("when downloading resolves", () => { + beforeEach(async () => { + onDownloadProgressMock.mockClear(); + + await downloadUpdateMock.resolve(); + }); + + it("resolves with success", async () => { + const actual = await actualPromise; + + expect(actual).toEqual({ downloadWasSuccessful: true }); + }); + + it("does not reset progress of download yet", () => { + expect(onDownloadProgressMock).not.toHaveBeenCalled(); + }); + + it("stops watching for download progress", () => { + expect(electronUpdaterOffMock).toHaveBeenCalledWith( + "download-progress", + expect.any(Function), + ); + }); + + it("when starting download again, resets progress of download", () => { + downloadPlatformUpdate(onDownloadProgressMock); + + expect(onDownloadProgressMock).toHaveBeenCalledWith({ percentage: 0 }); + }); + }); + + describe("when downloading rejects", () => { + let errorStub: Error; + + beforeEach(() => { + errorStub = new Error("Some error"); + + downloadUpdateMock.reject(errorStub); + }); + + it("logs error", () => { + expect(logErrorMock).toHaveBeenCalledWith("[UPDATE-APP/DOWNLOAD]", errorStub); + }); + + it("stops watching for download progress", () => { + expect(electronUpdaterOffMock).toHaveBeenCalledWith( + "download-progress", + expect.any(Function), + ); + }); + + it("resolves with failure", async () => { + const actual = await actualPromise; + + expect(actual).toEqual({ downloadWasSuccessful: false }); + }); + }); + }); + }); +}); diff --git a/src/main/application-update/download-update/download-update.injectable.ts b/src/main/application-update/download-update/download-update.injectable.ts new file mode 100644 index 0000000000..d0ac141b9c --- /dev/null +++ b/src/main/application-update/download-update/download-update.injectable.ts @@ -0,0 +1,49 @@ +/** + * 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 downloadPlatformUpdateInjectable from "../download-platform-update/download-platform-update.injectable"; +import updateIsBeingDownloadedInjectable from "../../../common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable"; +import discoveredUpdateVersionInjectable from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable"; +import { action, runInAction } from "mobx"; +import type { ProgressOfDownload } from "../../../common/application-update/progress-of-update-download/progress-of-update-download.injectable"; +import progressOfUpdateDownloadInjectable from "../../../common/application-update/progress-of-update-download/progress-of-update-download.injectable"; + +const downloadUpdateInjectable = getInjectable({ + id: "download-update", + + instantiate: (di) => { + const downloadPlatformUpdate = di.inject(downloadPlatformUpdateInjectable); + const downloadingUpdateState = di.inject(updateIsBeingDownloadedInjectable); + const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable); + const progressOfUpdateDownload = di.inject(progressOfUpdateDownloadInjectable); + + const updateDownloadProgress = action((progressOfDownload: ProgressOfDownload) => { + progressOfUpdateDownload.set(progressOfDownload); + }); + + return async () => { + runInAction(() => { + progressOfUpdateDownload.set({ percentage: 0 }); + downloadingUpdateState.set(true); + }); + + const { downloadWasSuccessful } = await downloadPlatformUpdate( + updateDownloadProgress, + ); + + runInAction(() => { + if (!downloadWasSuccessful) { + discoveredVersionState.set(null); + } + + downloadingUpdateState.set(false); + }); + + return { downloadWasSuccessful }; + }; + }, +}); + +export default downloadUpdateInjectable; diff --git a/src/main/application-update/install-application-update-tray-item.injectable.ts b/src/main/application-update/install-application-update-tray-item.injectable.ts new file mode 100644 index 0000000000..2f938964f8 --- /dev/null +++ b/src/main/application-update/install-application-update-tray-item.injectable.ts @@ -0,0 +1,56 @@ +/** + * 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 { computed } from "mobx"; +import { trayMenuItemInjectionToken } from "../tray/tray-menu-item/tray-menu-item-injection-token"; +import quitAndInstallUpdateInjectable from "../electron-app/features/quit-and-install-update.injectable"; +import discoveredUpdateVersionInjectable from "../../common/application-update/discovered-update-version/discovered-update-version.injectable"; +import updateIsBeingDownloadedInjectable from "../../common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable"; +import { withErrorSuppression } from "../../common/utils/with-error-suppression/with-error-suppression"; +import { pipeline } from "@ogre-tools/fp"; +import withErrorLoggingInjectable from "../../common/utils/with-error-logging/with-error-logging.injectable"; + +const installApplicationUpdateTrayItemInjectable = getInjectable({ + id: "install-update-tray-item", + + instantiate: (di) => { + const quitAndInstallUpdate = di.inject(quitAndInstallUpdateInjectable); + const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable); + const downloadingUpdateState = di.inject(updateIsBeingDownloadedInjectable); + const withErrorLoggingFor = di.inject(withErrorLoggingInjectable); + + return { + id: "install-update", + parentId: null, + orderNumber: 50, + + label: computed(() => { + const versionToBeInstalled = discoveredVersionState.value.get()?.version; + + return `Install update ${versionToBeInstalled}`; + }), + + enabled: computed(() => true), + + visible: computed( + () => !!discoveredVersionState.value.get() && !downloadingUpdateState.value.get(), + ), + + click: pipeline( + quitAndInstallUpdate, + + withErrorLoggingFor(() => "[TRAY]: Update installation failed."), + + // TODO: Find out how to improve typing so that instead of + // x => withErrorSuppression(x) there could only be withErrorSuppression + (x) => withErrorSuppression(x), + ), + }; + }, + + injectionToken: trayMenuItemInjectionToken, +}); + +export default installApplicationUpdateTrayItemInjectable; diff --git a/src/main/application-update/periodical-check-for-updates/periodical-check-for-updates.injectable.ts b/src/main/application-update/periodical-check-for-updates/periodical-check-for-updates.injectable.ts new file mode 100644 index 0000000000..394383ee65 --- /dev/null +++ b/src/main/application-update/periodical-check-for-updates/periodical-check-for-updates.injectable.ts @@ -0,0 +1,35 @@ +/** + * 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 "../../../common/utils/get-startable-stoppable"; +import processCheckingForUpdatesInjectable from "../check-for-updates/process-checking-for-updates.injectable"; + +const periodicalCheckForUpdatesInjectable = getInjectable({ + id: "periodical-check-for-updates", + + instantiate: (di) => { + const processCheckingForUpdates = di.inject(processCheckingForUpdatesInjectable); + + return getStartableStoppable("periodical-check-for-updates", () => { + const TWO_HOURS = 1000 * 60 * 60 * 2; + + // Note: intentional orphan promise to make checking for updates happen in the background + processCheckingForUpdates(); + + const intervalId = setInterval(() => { + // Note: intentional orphan promise to make checking for updates happen in the background + processCheckingForUpdates(); + }, TWO_HOURS); + + return () => { + clearInterval(intervalId); + }; + }); + }, + + causesSideEffects: true, +}); + +export default periodicalCheckForUpdatesInjectable; diff --git a/src/main/application-update/periodical-check-for-updates/start-checking-for-updates.injectable.ts b/src/main/application-update/periodical-check-for-updates/start-checking-for-updates.injectable.ts new file mode 100644 index 0000000000..9a9b9cf206 --- /dev/null +++ b/src/main/application-update/periodical-check-for-updates/start-checking-for-updates.injectable.ts @@ -0,0 +1,29 @@ +/** + * 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 periodicalCheckForUpdatesInjectable from "./periodical-check-for-updates.injectable"; +import { afterRootFrameIsReadyInjectionToken } from "../../start-main-application/runnable-tokens/after-root-frame-is-ready-injection-token"; +import updatingIsEnabledInjectable from "../updating-is-enabled.injectable"; + +const startCheckingForUpdatesInjectable = getInjectable({ + id: "start-checking-for-updates", + + instantiate: (di) => { + const periodicalCheckForUpdates = di.inject(periodicalCheckForUpdatesInjectable); + const updatingIsEnabled = di.inject(updatingIsEnabledInjectable); + + return { + run: async () => { + if (updatingIsEnabled) { + await periodicalCheckForUpdates.start(); + } + }, + }; + }, + + injectionToken: afterRootFrameIsReadyInjectionToken, +}); + +export default startCheckingForUpdatesInjectable; diff --git a/src/main/application-update/periodical-check-for-updates/stop-checking-for-updates.injectable.ts b/src/main/application-update/periodical-check-for-updates/stop-checking-for-updates.injectable.ts new file mode 100644 index 0000000000..13aefe4d96 --- /dev/null +++ b/src/main/application-update/periodical-check-for-updates/stop-checking-for-updates.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 { beforeQuitOfFrontEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-front-end-injection-token"; +import periodicalCheckForUpdatesInjectable from "./periodical-check-for-updates.injectable"; + +const stopCheckingForUpdatesInjectable = getInjectable({ + id: "stop-checking-for-updates", + + instantiate: (di) => { + const periodicalCheckForUpdates = di.inject(periodicalCheckForUpdatesInjectable); + + return { + run: async () => { + if (periodicalCheckForUpdates.started) { + await periodicalCheckForUpdates.stop(); + } + }, + }; + }, + + injectionToken: beforeQuitOfFrontEndInjectionToken, +}); + +export default stopCheckingForUpdatesInjectable; diff --git a/src/main/application-update/publish-is-configured.injectable.ts b/src/main/application-update/publish-is-configured.injectable.ts new file mode 100644 index 0000000000..321adc8a22 --- /dev/null +++ b/src/main/application-update/publish-is-configured.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 packageJsonInjectable from "../../common/vars/package-json.injectable"; +import { has } from "lodash/fp"; + +// TOOO: Rename to something less technical +const publishIsConfiguredInjectable = getInjectable({ + id: "publish-is-configured", + + instantiate: (di) => { + const packageJson = di.inject(packageJsonInjectable); + + return has("build.publish", packageJson); + }, +}); + +export default publishIsConfiguredInjectable; diff --git a/src/main/application-update/updating-is-enabled.injectable.ts b/src/main/application-update/updating-is-enabled.injectable.ts new file mode 100644 index 0000000000..df5e264219 --- /dev/null +++ b/src/main/application-update/updating-is-enabled.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 electronUpdaterIsActiveInjectable from "../electron-app/features/electron-updater-is-active.injectable"; +import publishIsConfiguredInjectable from "./publish-is-configured.injectable"; + +const updatingIsEnabledInjectable = getInjectable({ + id: "updating-is-enabled", + + instantiate: (di) => { + const electronUpdaterIsActive = di.inject(electronUpdaterIsActiveInjectable); + const publishIsConfigured = di.inject(publishIsConfiguredInjectable); + + return electronUpdaterIsActive && publishIsConfigured; + }, +}); + +export default updatingIsEnabledInjectable; diff --git a/src/main/application-update/watch-if-update-should-happen-on-quit/start-watching-if-update-should-happen-on-quit.injectable.ts b/src/main/application-update/watch-if-update-should-happen-on-quit/start-watching-if-update-should-happen-on-quit.injectable.ts new file mode 100644 index 0000000000..ef31cf5db5 --- /dev/null +++ b/src/main/application-update/watch-if-update-should-happen-on-quit/start-watching-if-update-should-happen-on-quit.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token"; +import watchIfUpdateShouldHappenOnQuitInjectable from "./watch-if-update-should-happen-on-quit.injectable"; + +const startWatchingIfUpdateShouldHappenOnQuitInjectable = getInjectable({ + id: "start-watching-if-update-should-happen-on-quit", + + instantiate: (di) => { + const watchIfUpdateShouldHappenOnQuit = di.inject(watchIfUpdateShouldHappenOnQuitInjectable); + + return { + run: () => { + watchIfUpdateShouldHappenOnQuit.start(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default startWatchingIfUpdateShouldHappenOnQuitInjectable; diff --git a/src/main/application-update/watch-if-update-should-happen-on-quit/stop-watching-if-update-should-happen-on-quit.injectable.ts b/src/main/application-update/watch-if-update-should-happen-on-quit/stop-watching-if-update-should-happen-on-quit.injectable.ts new file mode 100644 index 0000000000..b66cf927f2 --- /dev/null +++ b/src/main/application-update/watch-if-update-should-happen-on-quit/stop-watching-if-update-should-happen-on-quit.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import watchIfUpdateShouldHappenOnQuitInjectable from "./watch-if-update-should-happen-on-quit.injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; + +const stopWatchingIfUpdateShouldHappenOnQuitInjectable = getInjectable({ + id: "stop-watching-if-update-should-happen-on-quit", + + instantiate: (di) => { + const watchIfUpdateShouldHappenOnQuit = di.inject(watchIfUpdateShouldHappenOnQuitInjectable); + + return { + run: () => { + watchIfUpdateShouldHappenOnQuit.stop(); + }, + }; + }, + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default stopWatchingIfUpdateShouldHappenOnQuitInjectable; diff --git a/src/main/application-update/watch-if-update-should-happen-on-quit/watch-if-update-should-happen-on-quit.injectable.ts b/src/main/application-update/watch-if-update-should-happen-on-quit/watch-if-update-should-happen-on-quit.injectable.ts new file mode 100644 index 0000000000..12ec2d7c6e --- /dev/null +++ b/src/main/application-update/watch-if-update-should-happen-on-quit/watch-if-update-should-happen-on-quit.injectable.ts @@ -0,0 +1,54 @@ +/** + * 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 { autorun } from "mobx"; +import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable"; +import setUpdateOnQuitInjectable from "../../electron-app/features/set-update-on-quit.injectable"; +import selectedUpdateChannelInjectable from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable"; +import type { UpdateChannel } from "../../../common/application-update/update-channels"; +import discoveredUpdateVersionInjectable from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable"; + +const watchIfUpdateShouldHappenOnQuitInjectable = getInjectable({ + id: "watch-if-update-should-happen-on-quit", + + instantiate: (di) => { + const setUpdateOnQuit = di.inject(setUpdateOnQuitInjectable); + const selectedUpdateChannel = di.inject(selectedUpdateChannelInjectable); + const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable); + + return getStartableStoppable("watch-if-update-should-happen-on-quit", () => + autorun(() => { + const sufficientlyStableUpdateChannels = + getSufficientlyStableUpdateChannels(selectedUpdateChannel.value.get()); + + const discoveredVersion = discoveredVersionState.value.get(); + + const updateIsDiscoveredFromChannel = discoveredVersion?.updateChannel; + + const updateOnQuit = updateIsDiscoveredFromChannel + ? sufficientlyStableUpdateChannels.includes( + updateIsDiscoveredFromChannel, + ) + : false; + + setUpdateOnQuit(updateOnQuit); + }), + ); + }, +}); + +const getSufficientlyStableUpdateChannels = (updateChannel: UpdateChannel): UpdateChannel[] => { + if (!updateChannel.moreStableUpdateChannel) { + return [updateChannel]; + } + + return [ + updateChannel, + + ...getSufficientlyStableUpdateChannels(updateChannel.moreStableUpdateChannel), + ]; +}; + +export default watchIfUpdateShouldHappenOnQuitInjectable; diff --git a/src/main/ask-boolean/__snapshots__/ask-boolean.test.ts.snap b/src/main/ask-boolean/__snapshots__/ask-boolean.test.ts.snap new file mode 100644 index 0000000000..8176698168 --- /dev/null +++ b/src/main/ask-boolean/__snapshots__/ask-boolean.test.ts.snap @@ -0,0 +1,336 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ask-boolean given started when asking multiple questions renders 1`] = ` + +
    +
    +
    +
    + + + info_outline + + +
    +
    +
    + + some-title + +

    + Some question +

    +
    + + +
    +
    +
    +
    + + + close + + +
    +
    +
    +
    + + + info_outline + + +
    +
    +
    + + some-other-title + +

    + Some other question +

    +
    + + +
    +
    +
    +
    + + + close + + +
    +
    +
    +
    + +`; + +exports[`ask-boolean given started when asking multiple questions when answering to first question renders 1`] = ` + +
    +
    +
    +
    + + + info_outline + + +
    +
    +
    + + some-other-title + +

    + Some other question +

    +
    + + +
    +
    +
    +
    + + + close + + +
    +
    +
    +
    + +`; + +exports[`ask-boolean given started when asking question renders 1`] = ` + +
    +
    +
    +
    + + + info_outline + + +
    +
    +
    + + some-title + +

    + Some question +

    +
    + + +
    +
    +
    +
    + + + close + + +
    +
    +
    +
    + +`; + +exports[`ask-boolean given started when asking question when user answers "no" renders 1`] = ` + +
    +
    +
    + +`; + +exports[`ask-boolean given started when asking question when user answers "yes" renders 1`] = ` + +
    +
    +
    + +`; + +exports[`ask-boolean given started when asking question when user closes notification without answering the question renders 1`] = ` + +
    +
    +
    + +`; diff --git a/src/main/ask-boolean/ask-boolean-answer-channel-listener.injectable.ts b/src/main/ask-boolean/ask-boolean-answer-channel-listener.injectable.ts new file mode 100644 index 0000000000..06bc3972eb --- /dev/null +++ b/src/main/ask-boolean/ask-boolean-answer-channel-listener.injectable.ts @@ -0,0 +1,29 @@ +/** + * 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 { AskBooleanAnswerChannel } from "../../common/ask-boolean/ask-boolean-answer-channel.injectable"; +import askBooleanAnswerChannelInjectable from "../../common/ask-boolean/ask-boolean-answer-channel.injectable"; +import askBooleanPromiseInjectable from "./ask-boolean-promise.injectable"; +import type { MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token"; +import { messageChannelListenerInjectionToken } from "../../common/utils/channel/message-channel-listener-injection-token"; + + +const askBooleanAnswerChannelListenerInjectable = getInjectable({ + id: "ask-boolean-answer-channel-listener", + + instantiate: (di): MessageChannelListener => ({ + channel: di.inject(askBooleanAnswerChannelInjectable), + + handler: ({ id, value }) => { + const answerPromise = di.inject(askBooleanPromiseInjectable, id); + + answerPromise.resolve(value); + }, + }), + + injectionToken: messageChannelListenerInjectionToken, +}); + +export default askBooleanAnswerChannelListenerInjectable; diff --git a/src/main/ask-boolean/ask-boolean-promise.injectable.ts b/src/main/ask-boolean/ask-boolean-promise.injectable.ts new file mode 100644 index 0000000000..827714084f --- /dev/null +++ b/src/main/ask-boolean/ask-boolean-promise.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; + +const askBooleanPromiseInjectable = getInjectable({ + id: "ask-boolean-promise", + + instantiate: (di, questionId: string) => { + void questionId; + + let resolve: (value: boolean) => void; + + const promise = new Promise(_resolve => { + resolve = _resolve; + }); + + return ({ + promise, + + resolve: (value: boolean) => { + resolve(value); + }, + }); + }, + + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, questionId: string) => questionId, + }), +}); + +export default askBooleanPromiseInjectable; diff --git a/src/main/ask-boolean/ask-boolean.injectable.ts b/src/main/ask-boolean/ask-boolean.injectable.ts new file mode 100644 index 0000000000..1fa54629b2 --- /dev/null +++ b/src/main/ask-boolean/ask-boolean.injectable.ts @@ -0,0 +1,39 @@ +/** + * 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 { messageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; +import askBooleanQuestionChannelInjectable from "../../common/ask-boolean/ask-boolean-question-channel.injectable"; +import askBooleanPromiseInjectable from "./ask-boolean-promise.injectable"; +import getRandomIdInjectable from "../../common/utils/get-random-id.injectable"; + +export type AskBoolean = ({ + title, + question, +}: { + title: string; + question: string; +}) => Promise; + +const askBooleanInjectable = getInjectable({ + id: "ask-boolean", + + instantiate: (di): AskBoolean => { + const messageToChannel = di.inject(messageToChannelInjectionToken); + const askBooleanChannel = di.inject(askBooleanQuestionChannelInjectable); + const getRandomId = di.inject(getRandomIdInjectable); + + return async ({ title, question }) => { + const id = getRandomId(); + + const returnValuePromise = di.inject(askBooleanPromiseInjectable, id); + + await messageToChannel(askBooleanChannel, { id, title, question }); + + return await returnValuePromise.promise; + }; + }, +}); + +export default askBooleanInjectable; diff --git a/src/main/ask-boolean/ask-boolean.test.ts b/src/main/ask-boolean/ask-boolean.test.ts new file mode 100644 index 0000000000..79fecdceca --- /dev/null +++ b/src/main/ask-boolean/ask-boolean.test.ts @@ -0,0 +1,206 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import type { AskBoolean } from "./ask-boolean.injectable"; +import askBooleanInjectable from "./ask-boolean.injectable"; +import { getPromiseStatus } from "../../common/test-utils/get-promise-status"; +import type { RenderResult } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; +import getRandomIdInjectable from "../../common/utils/get-random-id.injectable"; + +describe("ask-boolean", () => { + let applicationBuilder: ApplicationBuilder; + let askBoolean: AskBoolean; + + beforeEach(() => { + applicationBuilder = getApplicationBuilder(); + + const getRandomIdFake = jest + .fn() + .mockReturnValueOnce("some-random-id-1") + .mockReturnValueOnce("some-random-id-2"); + + applicationBuilder.dis.mainDi.override(getRandomIdInjectable, () => getRandomIdFake); + + askBoolean = applicationBuilder.dis.mainDi.inject(askBooleanInjectable); + }); + + describe("given started", () => { + let rendered: RenderResult; + + beforeEach(async () => { + rendered = await applicationBuilder.render(); + }); + + describe("when asking question", () => { + let actualPromise: Promise; + + beforeEach(() => { + actualPromise = askBoolean({ + title: "some-title", + question: "Some question", + }); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("shows notification", () => { + const notification = rendered.getByTestId("ask-boolean-some-random-id-1"); + + expect(notification).not.toBeUndefined(); + }); + + describe('when user answers "yes"', () => { + beforeEach(() => { + const button = rendered.getByTestId("ask-boolean-some-random-id-1-button-yes"); + + fireEvent.click(button); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not show notification anymore", () => { + const notification = rendered.queryByTestId("ask-boolean-some-random-id-1"); + + expect(notification).toBeNull(); + }); + + it("resolves", async () => { + const actual = await actualPromise; + + expect(actual).toBe(true); + }); + }); + + describe('when user answers "no"', () => { + beforeEach(() => { + const button = rendered.getByTestId("ask-boolean-some-random-id-1-button-no"); + + fireEvent.click(button); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not show notification anymore", () => { + const notification = rendered.queryByTestId("ask-boolean-some-random-id-1"); + + expect(notification).toBeNull(); + }); + + it("resolves", async () => { + const actual = await actualPromise; + + expect(actual).toBe(false); + }); + }); + + describe("when user closes notification without answering the question", () => { + beforeEach(() => { + const button = rendered.getByTestId("close-notification-for-ask-boolean-for-some-random-id-1"); + + fireEvent.click(button); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not show notification anymore", () => { + const notification = rendered.queryByTestId("ask-boolean-some-random-id-1"); + + expect(notification).toBeNull(); + }); + + it("resolves", async () => { + const actual = await actualPromise; + + expect(actual).toBe(false); + }); + }); + }); + + describe("when asking multiple questions", () => { + let firstQuestionPromise: Promise; + let secondQuestionPromise: Promise; + + beforeEach(() => { + firstQuestionPromise = askBoolean({ + title: "some-title", + question: "Some question", + }); + + secondQuestionPromise = askBoolean({ + title: "some-other-title", + question: "Some other question", + }); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("shows notification for first question", () => { + const notification = rendered.getByTestId("ask-boolean-some-random-id-1"); + + expect(notification).not.toBeUndefined(); + }); + + it("shows notification for second question", () => { + const notification = rendered.getByTestId("ask-boolean-some-random-id-2"); + + expect(notification).not.toBeUndefined(); + }); + + describe("when answering to first question", () => { + beforeEach(() => { + const button = rendered.getByTestId("ask-boolean-some-random-id-1-button-no"); + + fireEvent.click(button); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not show notification for first question anymore", () => { + const notification = rendered.queryByTestId("ask-boolean-some-random-id-1"); + + expect(notification).toBeNull(); + }); + + it("still shows notification for second question", () => { + const notification = rendered.getByTestId("ask-boolean-some-random-id-2"); + + expect(notification).not.toBeUndefined(); + }); + + it("resolves first question", async () => { + const actual = await firstQuestionPromise; + + expect(actual).toBe(false); + }); + + it("does not resolve second question yet", async () => { + const promiseStatus = await getPromiseStatus(secondQuestionPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + }); + }); + }); +}); diff --git a/src/main/catalog-sync-to-renderer/catalog-sync-to-renderer.injectable.ts b/src/main/catalog-sync-to-renderer/catalog-sync-to-renderer.injectable.ts index 37dfd2f7fd..7e588481a8 100644 --- a/src/main/catalog-sync-to-renderer/catalog-sync-to-renderer.injectable.ts +++ b/src/main/catalog-sync-to-renderer/catalog-sync-to-renderer.injectable.ts @@ -17,6 +17,8 @@ const catalogSyncToRendererInjectable = getInjectable({ startCatalogSyncToRenderer(catalogEntityRegistry), ); }, + + causesSideEffects: true, }); export default catalogSyncToRendererInjectable; diff --git a/src/main/electron-app/features/electron-updater-is-active.injectable.ts b/src/main/electron-app/features/electron-updater-is-active.injectable.ts new file mode 100644 index 0000000000..2fe0d7bf06 --- /dev/null +++ b/src/main/electron-app/features/electron-updater-is-active.injectable.ts @@ -0,0 +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 electronUpdaterInjectable from "./electron-updater.injectable"; + +const electronUpdaterIsActiveInjectable = getInjectable({ + id: "electron-updater-is-active", + + instantiate: (di) => { + const electronUpdater = di.inject(electronUpdaterInjectable); + + return electronUpdater.isUpdaterActive(); + }, +}); + +export default electronUpdaterIsActiveInjectable; diff --git a/src/main/electron-app/features/electron-updater.injectable.ts b/src/main/electron-app/features/electron-updater.injectable.ts new file mode 100644 index 0000000000..f9e3335343 --- /dev/null +++ b/src/main/electron-app/features/electron-updater.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 { autoUpdater } from "electron-updater"; + +const electronUpdaterInjectable = getInjectable({ + id: "electron-updater", + instantiate: () => autoUpdater, + causesSideEffects: true, +}); + +export default electronUpdaterInjectable; diff --git a/src/main/electron-app/features/quit-and-install-update.injectable.ts b/src/main/electron-app/features/quit-and-install-update.injectable.ts new file mode 100644 index 0000000000..6b313e21b0 --- /dev/null +++ b/src/main/electron-app/features/quit-and-install-update.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 electronUpdaterInjectable from "./electron-updater.injectable"; + +const quitAndInstallUpdateInjectable = getInjectable({ + id: "quit-and-install-update", + + instantiate: (di) => { + const electronUpdater = di.inject(electronUpdaterInjectable); + + return () => { + electronUpdater.quitAndInstall(true, true); + }; + }, +}); + +export default quitAndInstallUpdateInjectable; diff --git a/src/main/electron-app/features/set-update-on-quit.injectable.ts b/src/main/electron-app/features/set-update-on-quit.injectable.ts new file mode 100644 index 0000000000..43693f8eed --- /dev/null +++ b/src/main/electron-app/features/set-update-on-quit.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 electronUpdaterInjectable from "./electron-updater.injectable"; + +const setUpdateOnQuitInjectable = getInjectable({ + id: "set-update-on-quit", + + instantiate: (di) => { + const electronUpdater = di.inject(electronUpdaterInjectable); + + return (updateOnQuit: boolean) => { + electronUpdater.autoInstallOnAppQuit = updateOnQuit; + }; + }, +}); + +export default setUpdateOnQuitInjectable; diff --git a/src/main/electron-app/runnables/setup-update-checking.injectable.ts b/src/main/electron-app/runnables/setup-update-checking.injectable.ts deleted file mode 100644 index 918e985265..0000000000 --- a/src/main/electron-app/runnables/setup-update-checking.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 { afterRootFrameIsReadyInjectionToken } from "../../start-main-application/runnable-tokens/after-root-frame-is-ready-injection-token"; -import startUpdateCheckingInjectable from "../../start-update-checking.injectable"; - -const setupUpdateCheckingInjectable = getInjectable({ - id: "setup-update-checking", - - instantiate: (di) => { - const startUpdateChecking = di.inject(startUpdateCheckingInjectable); - - return { - run: () => { - startUpdateChecking(); - }, - }; - }, - - injectionToken: afterRootFrameIsReadyInjectionToken, -}); - -export default setupUpdateCheckingInjectable; diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 5889e30090..2cc78ec528 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -4,12 +4,11 @@ */ import glob from "glob"; -import { kebabCase, memoize } from "lodash/fp"; +import { kebabCase, memoize, noop } from "lodash/fp"; import type { DiContainer } from "@ogre-tools/injectable"; import { createContainer } from "@ogre-tools/injectable"; import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import appNameInjectable from "./app-paths/app-name/app-name.injectable"; -import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable"; import writeJsonFileInjectable from "../common/fs/write-json-file.injectable"; import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; import readFileInjectable from "../common/fs/read-file.injectable"; @@ -29,8 +28,6 @@ import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake import joinPathsInjectable from "../common/path/join-paths.injectable"; import { joinPathsFake } from "../common/test-utils/join-paths-fake"; import hotbarStoreInjectable from "../common/hotbars/store.injectable"; -import type { GetDiForUnitTestingOptions } from "../test-utils/get-dis-for-unit-testing"; -import isAutoUpdateEnabledInjectable from "./is-auto-update-enabled.injectable"; import appEventBusInjectable from "../common/app-event-bus/app-event-bus.injectable"; import { EventEmitter } from "../common/event-emitter"; import type { AppEvent } from "../common/app-event-bus/event-bus"; @@ -45,7 +42,6 @@ import setupSentryInjectable from "./start-main-application/runnables/setup-sent import setupShellInjectable from "./start-main-application/runnables/setup-shell.injectable"; import setupSyncingOfWeblinksInjectable from "./start-main-application/runnables/setup-syncing-of-weblinks.injectable"; import stopServicesAndExitAppInjectable from "./stop-services-and-exit-app.injectable"; -import trayInjectable from "./tray/tray.injectable"; import applicationMenuInjectable from "./menu/application-menu.injectable"; import isDevelopmentInjectable from "../common/vars/is-development.injectable"; import setupSystemCaInjectable from "./start-main-application/runnables/setup-system-ca.injectable"; @@ -67,7 +63,7 @@ import type { ClusterFrameInfo } from "../common/cluster-frames"; import { observable } from "mobx"; import waitForElectronToBeReadyInjectable from "./electron-app/features/wait-for-electron-to-be-ready.injectable"; import setupListenerForCurrentClusterFrameInjectable from "./start-main-application/lens-window/current-cluster-frame/setup-listener-for-current-cluster-frame.injectable"; -import ipcMainInjectable from "./app-paths/register-channel/ipc-main/ipc-main.injectable"; +import ipcMainInjectable from "./utils/channel/ipc-main/ipc-main.injectable"; import createElectronWindowForInjectable from "./start-main-application/lens-window/application-window/create-electron-window-for.injectable"; import setupRunnablesAfterWindowIsOpenedInjectable from "./electron-app/runnables/setup-runnables-after-window-is-opened.injectable"; import sendToChannelInElectronBrowserWindowInjectable from "./start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable"; @@ -75,10 +71,21 @@ import broadcastMessageInjectable from "../common/ipc/broadcast-message.injectab import getElectronThemeInjectable from "./electron-app/features/get-electron-theme.injectable"; import syncThemeFromOperatingSystemInjectable from "./electron-app/features/sync-theme-from-operating-system.injectable"; import platformInjectable from "../common/vars/platform.injectable"; -import { noop } from "../renderer/utils"; +import productNameInjectable from "./app-paths/app-name/product-name.injectable"; +import quitAndInstallUpdateInjectable from "./electron-app/features/quit-and-install-update.injectable"; +import electronUpdaterIsActiveInjectable from "./electron-app/features/electron-updater-is-active.injectable"; +import publishIsConfiguredInjectable from "./application-update/publish-is-configured.injectable"; +import checkForPlatformUpdatesInjectable from "./application-update/check-for-platform-updates/check-for-platform-updates.injectable"; import baseBundeledBinariesDirectoryInjectable from "../common/vars/base-bundled-binaries-dir.injectable"; +import setUpdateOnQuitInjectable from "./electron-app/features/set-update-on-quit.injectable"; +import downloadPlatformUpdateInjectable from "./application-update/download-platform-update/download-platform-update.injectable"; +import startCatalogSyncInjectable from "./catalog-sync-to-renderer/start-catalog-sync.injectable"; +import startKubeConfigSyncInjectable from "./start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable"; +import appVersionInjectable from "../common/get-configuration-file-model/app-version/app-version.injectable"; +import getRandomIdInjectable from "../common/utils/get-random-id.injectable"; +import periodicalCheckForUpdatesInjectable from "./application-update/periodical-check-for-updates/periodical-check-for-updates.injectable"; -export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) { +export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) { const { doGeneralOverrides = false, } = opts; @@ -100,10 +107,11 @@ export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) { di.preventSideEffects(); if (doGeneralOverrides) { + di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id"); di.override(hotbarStoreInjectable, () => ({ load: () => {} })); - di.override(userStoreInjectable, () => ({ startMainReactions: () => {} }) as UserStore); + di.override(userStoreInjectable, () => ({ startMainReactions: () => {}, extensionRegistryUrl: { customUrl: "some-custom-url" }}) as UserStore); di.override(extensionsStoreInjectable, () => ({ isEnabled: (opts) => (void opts, false) }) as ExtensionsStore); - di.override(clusterStoreInjectable, () => ({ getById: (id) => (void id, {}) as Cluster }) as ClusterStore); + di.override(clusterStoreInjectable, () => ({ provideInitialFromMain: () => {}, getById: (id) => (void id, {}) as Cluster }) as ClusterStore); di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore); overrideOperatingSystem(di); @@ -114,19 +122,22 @@ export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) { di.override(environmentVariablesInjectable, () => ({})); di.override(commandLineArgumentsInjectable, () => []); + di.override(productNameInjectable, () => "some-product-name"); + di.override(appVersionInjectable, () => "1.0.0"); + di.override(clusterFramesInjectable, () => observable.map()); di.override(stopServicesAndExitAppInjectable, () => () => {}); di.override(lensResourcesDirInjectable, () => "/irrelevant"); - di.override(trayInjectable, () => ({ start: () => {}, stop: () => {} })); di.override(applicationMenuInjectable, () => ({ start: () => {}, stop: () => {} })); + di.override(periodicalCheckForUpdatesInjectable, () => ({ start: () => {}, stop: () => {}, started: false })); + // TODO: Remove usages of globally exported appEventBus to get rid of this di.override(appEventBusInjectable, () => new EventEmitter<[AppEvent]>()); di.override(appNameInjectable, () => "some-app-name"); - di.override(registerChannelInjectable, () => () => undefined); di.override(broadcastMessageInjectable, () => (channel) => { throw new Error(`Tried to broadcast message to channel "${channel}" over IPC without explicit override.`); }); @@ -182,6 +193,8 @@ const overrideRunnablesHavingSideEffects = (di: DiContainer) => { setupSystemCaInjectable, setupListenerForCurrentClusterFrameInjectable, setupRunnablesAfterWindowIsOpenedInjectable, + startCatalogSyncInjectable, + startKubeConfigSyncInjectable, ].forEach((injectable) => { di.override(injectable, () => ({ run: () => {} })); }); @@ -215,6 +228,13 @@ const overrideElectronFeatures = (di: DiContainer) => { di.override(ipcMainInjectable, () => ({})); di.override(getElectronThemeInjectable, () => () => "dark"); di.override(syncThemeFromOperatingSystemInjectable, () => ({ start: () => {}, stop: () => {} })); + di.override(quitAndInstallUpdateInjectable, () => () => {}); + di.override(setUpdateOnQuitInjectable, () => () => {}); + di.override(downloadPlatformUpdateInjectable, () => async () => ({ downloadWasSuccessful: true })); + + di.override(checkForPlatformUpdatesInjectable, () => () => { + throw new Error("Tried to check for platform updates without explicit override."); + }); di.override(createElectronWindowForInjectable, () => () => async () => ({ show: () => {}, @@ -234,5 +254,6 @@ const overrideElectronFeatures = (di: DiContainer) => { ); di.override(setElectronAppPathInjectable, () => () => {}); - di.override(isAutoUpdateEnabledInjectable, () => () => false); + di.override(publishIsConfiguredInjectable, () => false); + di.override(electronUpdaterIsActiveInjectable, () => false); }; diff --git a/src/main/is-auto-update-enabled.injectable.ts b/src/main/is-auto-update-enabled.injectable.ts deleted file mode 100644 index c76bd27e45..0000000000 --- a/src/main/is-auto-update-enabled.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 { isPublishConfigured } from "../common/vars"; -import { autoUpdater } from "electron-updater"; - -const isAutoUpdateEnabledInjectable = getInjectable({ - id: "is-auto-update-enabled", - - instantiate: () => () => { - return autoUpdater.isUpdaterActive() && isPublishConfigured; - }, - - causesSideEffects: true, -}); - -export default isAutoUpdateEnabledInjectable; diff --git a/src/main/menu/application-menu-items.injectable.ts b/src/main/menu/application-menu-items.injectable.ts index 417c40ac2e..4ea447f675 100644 --- a/src/main/menu/application-menu-items.injectable.ts +++ b/src/main/menu/application-menu-items.injectable.ts @@ -3,7 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { checkForUpdates } from "../app-updater"; import { docsUrl, productName, supportUrl } from "../../common/vars"; import { broadcastMessage } from "../../common/ipc"; import { openBrowser } from "../../common/utils"; @@ -12,7 +11,7 @@ import { webContents } from "electron"; import loggerInjectable from "../../common/logger.injectable"; import appNameInjectable from "../app-paths/app-name/app-name.injectable"; import electronMenuItemsInjectable from "./electron-menu-items.injectable"; -import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable"; +import updatingIsEnabledInjectable from "../application-update/updating-is-enabled.injectable"; import navigateToPreferencesInjectable from "../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; import navigateToExtensionsInjectable from "../../common/front-end-routing/routes/extensions/navigate-to-extensions.injectable"; import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; @@ -25,6 +24,7 @@ import showAboutInjectable from "./show-about.injectable"; import applicationWindowInjectable from "../start-main-application/lens-window/application-window/application-window.injectable"; import reloadWindowInjectable from "../start-main-application/lens-window/reload-window.injectable"; import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable"; +import processCheckingForUpdatesInjectable from "../application-update/check-for-updates/process-checking-for-updates.injectable"; function ignoreIf(check: boolean, menuItems: MenuItemOpts[]) { return check ? [] : menuItems; @@ -41,7 +41,7 @@ const applicationMenuItemsInjectable = getInjectable({ const logger = di.inject(loggerInjectable); const appName = di.inject(appNameInjectable); const isMac = di.inject(isMacInjectable); - const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable); + const updatingIsEnabled = di.inject(updatingIsEnabledInjectable); const electronMenuItems = di.inject(electronMenuItemsInjectable); const showAbout = di.inject(showAboutInjectable); const applicationWindow = di.inject(applicationWindowInjectable); @@ -53,12 +53,11 @@ const applicationMenuItemsInjectable = getInjectable({ const navigateToWelcome = di.inject(navigateToWelcomeInjectable); const navigateToAddCluster = di.inject(navigateToAddClusterInjectable); const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable); + const processCheckingForUpdates = di.inject(processCheckingForUpdatesInjectable); + + logger.info(`[MENU]: autoUpdateEnabled=${updatingIsEnabled}`); return computed((): MenuItemOpts[] => { - const autoUpdateDisabled = !isAutoUpdateEnabled(); - - logger.info(`[MENU]: autoUpdateDisabled=${autoUpdateDisabled}`); - const macAppMenu: MenuItemOpts = { label: appName, id: "root", @@ -70,11 +69,11 @@ const applicationMenuItemsInjectable = getInjectable({ showAbout(); }, }, - ...ignoreIf(autoUpdateDisabled, [ + ...ignoreIf(!updatingIsEnabled, [ { label: "Check for updates", click() { - checkForUpdates().then(() => showApplicationWindow()); + processCheckingForUpdates().then(() => showApplicationWindow()); }, }, ]), @@ -282,11 +281,11 @@ const applicationMenuItemsInjectable = getInjectable({ showAbout(); }, }, - ...ignoreIf(autoUpdateDisabled, [ + ...ignoreIf(!updatingIsEnabled, [ { label: "Check for updates", click() { - checkForUpdates().then(() => + processCheckingForUpdates().then(() => showApplicationWindow(), ); }, diff --git a/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts index dd3feb3020..c63191c71c 100644 --- a/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts +++ b/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts @@ -11,7 +11,7 @@ import appNameInjectable from "../../../app-paths/app-name/app-name.injectable"; import appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable"; import { delay } from "../../../../common/utils"; import { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling"; -import ipcMainInjectable from "../../../app-paths/register-channel/ipc-main/ipc-main.injectable"; +import ipcMainInjectable from "../../../utils/channel/ipc-main/ipc-main.injectable"; const applicationWindowInjectable = getInjectable({ id: "application-window", diff --git a/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts index c93877ee6a..a44b0ebd4c 100644 --- a/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts +++ b/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts @@ -58,9 +58,9 @@ const createLensWindowInjectable = getInjectable({ browserWindow?.close(); browserWindow = undefined; }, - send: async (args: SendToViewArgs) => { + send: (args: SendToViewArgs) => { if (!browserWindow) { - browserWindow = await createElectronWindow(); + throw new Error(`Tried to send message to window "${configuration.id}" but the window was closed`); } return browserWindow.send(args); diff --git a/src/main/start-main-application/lens-window/application-window/lens-window-injection-token.ts b/src/main/start-main-application/lens-window/application-window/lens-window-injection-token.ts index f7273206c9..3e62b0894b 100644 --- a/src/main/start-main-application/lens-window/application-window/lens-window-injection-token.ts +++ b/src/main/start-main-application/lens-window/application-window/lens-window-injection-token.ts @@ -14,7 +14,7 @@ export interface SendToViewArgs { export interface LensWindow { show: () => Promise; close: () => void; - send: (args: SendToViewArgs) => Promise; + send: (args: SendToViewArgs) => void; visible: boolean; } diff --git a/src/main/start-main-application/lens-window/navigate-for-extension.injectable.ts b/src/main/start-main-application/lens-window/navigate-for-extension.injectable.ts index 703ef15f60..7bada3f3bd 100644 --- a/src/main/start-main-application/lens-window/navigate-for-extension.injectable.ts +++ b/src/main/start-main-application/lens-window/navigate-for-extension.injectable.ts @@ -36,7 +36,7 @@ const navigateForExtensionInjectable = getInjectable({ (frameInfo) => frameInfo.frameId === frameId, ); - await applicationWindow.send({ + applicationWindow.send({ channel: "extension:navigate", frameInfo, data: [extId, pageId, params], diff --git a/src/main/start-main-application/lens-window/navigate.injectable.ts b/src/main/start-main-application/lens-window/navigate.injectable.ts index c7cbb10d24..f9d80e4205 100644 --- a/src/main/start-main-application/lens-window/navigate.injectable.ts +++ b/src/main/start-main-application/lens-window/navigate.injectable.ts @@ -29,7 +29,7 @@ const navigateInjectable = getInjectable({ ? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER : IpcRendererNavigationEvents.NAVIGATE_IN_APP; - await applicationWindow.send({ + applicationWindow.send({ channel, frameInfo, data: [url], diff --git a/src/main/start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable.ts b/src/main/start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable.ts index d6d964c7df..80c725e17d 100644 --- a/src/main/start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable.ts +++ b/src/main/start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable.ts @@ -25,6 +25,8 @@ const startKubeConfigSyncInjectable = getInjectable({ }; }, + causesSideEffects: true, + injectionToken: afterRootFrameIsReadyInjectionToken, }); diff --git a/src/main/start-main-application/runnables/root-frame-rendered-channel-listener/root-frame-rendered-channel-listener.injectable.ts b/src/main/start-main-application/runnables/root-frame-rendered-channel-listener/root-frame-rendered-channel-listener.injectable.ts new file mode 100644 index 0000000000..83ebf7bf91 --- /dev/null +++ b/src/main/start-main-application/runnables/root-frame-rendered-channel-listener/root-frame-rendered-channel-listener.injectable.ts @@ -0,0 +1,35 @@ +/** + * 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 rootFrameRenderedChannelInjectable from "../../../../common/root-frame-rendered-channel/root-frame-rendered-channel.injectable"; +import { runManyFor } from "../../../../common/runnable/run-many-for"; +import { afterRootFrameIsReadyInjectionToken } from "../../runnable-tokens/after-root-frame-is-ready-injection-token"; +import { messageChannelListenerInjectionToken } from "../../../../common/utils/channel/message-channel-listener-injection-token"; + +const rootFrameRenderedChannelListenerInjectable = getInjectable({ + id: "root-frame-rendered-channel-listener", + + instantiate: (di) => { + const channel = di.inject(rootFrameRenderedChannelInjectable); + + const runMany = runManyFor(di); + + const runRunnablesAfterRootFrameIsReady = runMany( + afterRootFrameIsReadyInjectionToken, + ); + + return { + channel, + + handler: async () => { + await runRunnablesAfterRootFrameIsReady(); + }, + }; + }, + + injectionToken: messageChannelListenerInjectionToken, +}); + +export default rootFrameRenderedChannelListenerInjectable; diff --git a/src/main/start-main-application/runnables/setup-sentry.injectable.ts b/src/main/start-main-application/runnables/setup-sentry.injectable.ts index e9f0587450..d100b93cc6 100644 --- a/src/main/start-main-application/runnables/setup-sentry.injectable.ts +++ b/src/main/start-main-application/runnables/setup-sentry.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import { initializeSentryReporting } from "../../../common/sentry"; import { init } from "@sentry/electron/main"; -import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; +import { beforeApplicationIsLoadingInjectionToken } from "../runnable-tokens/before-application-is-loading-injection-token"; const setupSentryInjectable = getInjectable({ id: "setup-sentry", @@ -18,7 +18,7 @@ const setupSentryInjectable = getInjectable({ causesSideEffects: true, - injectionToken: onLoadOfApplicationInjectionToken, + injectionToken: beforeApplicationIsLoadingInjectionToken, }); export default setupSentryInjectable; diff --git a/src/main/start-update-checking.injectable.ts b/src/main/start-update-checking.injectable.ts deleted file mode 100644 index 4571e70df4..0000000000 --- a/src/main/start-update-checking.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 { startUpdateChecking } from "./app-updater"; -import isAutoUpdateEnabledInjectable from "./is-auto-update-enabled.injectable"; - -const startUpdateCheckingInjectable = getInjectable({ - id: "start-update-checking", - - instantiate: (di) => startUpdateChecking({ - isAutoUpdateEnabled: di.inject(isAutoUpdateEnabledInjectable), - }), - - causesSideEffects: true, -}); - -export default startUpdateCheckingInjectable; diff --git a/src/main/tray/electron-tray/electron-tray.injectable.ts b/src/main/tray/electron-tray/electron-tray.injectable.ts new file mode 100644 index 0000000000..409e7abf3f --- /dev/null +++ b/src/main/tray/electron-tray/electron-tray.injectable.ts @@ -0,0 +1,116 @@ +/** + * 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 { Menu, Tray } from "electron"; +import packageJsonInjectable from "../../../common/vars/package-json.injectable"; +import logger from "../../logger"; +import { TRAY_LOG_PREFIX } from "../tray"; +import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable"; +import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token"; +import { pipeline } from "@ogre-tools/fp"; +import { isEmpty, map, filter } from "lodash/fp"; +import isWindowsInjectable from "../../../common/vars/is-windows.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import trayIconPathInjectable from "../tray-icon-path.injectable"; + +const electronTrayInjectable = getInjectable({ + id: "electron-tray", + + instantiate: (di) => { + const packageJson = di.inject(packageJsonInjectable); + const showApplicationWindow = di.inject(showApplicationWindowInjectable); + const isWindows = di.inject(isWindowsInjectable); + const logger = di.inject(loggerInjectable); + const trayIconPath = di.inject(trayIconPathInjectable); + + let tray: Tray; + + return { + start: () => { + tray = new Tray(trayIconPath); + + tray.setToolTip(packageJson.description); + tray.setIgnoreDoubleClickEvents(true); + + if (isWindows) { + tray.on("click", () => { + showApplicationWindow() + .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error })); + }); + } + }, + + stop: () => { + tray.destroy(); + }, + + setMenuItems: (items: TrayMenuItem[]) => { + pipeline( + items, + convertToElectronMenuTemplate, + Menu.buildFromTemplate, + + (template) => { + tray.setContextMenu(template); + }, + ); + }, + }; + }, + + causesSideEffects: true, +}); + +export default electronTrayInjectable; + +const convertToElectronMenuTemplate = (trayMenuItems: TrayMenuItem[]) => { + const _toTrayMenuOptions = (parentId: string | null) => + pipeline( + trayMenuItems, + + filter((item) => item.parentId === parentId), + + map( + (trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => { + if (trayMenuItem.separator) { + return { id: trayMenuItem.id, type: "separator" }; + } + + const childItems = _toTrayMenuOptions(trayMenuItem.id); + + return { + id: trayMenuItem.id, + label: trayMenuItem.label?.get(), + enabled: trayMenuItem.enabled.get(), + toolTip: trayMenuItem.tooltip, + + ...(isEmpty(childItems) + ? { + type: "normal", + submenu: _toTrayMenuOptions(trayMenuItem.id), + + click: () => { + try { + trayMenuItem.click?.(); + } catch (error) { + logger.error( + `${TRAY_LOG_PREFIX}: clicking item "${trayMenuItem.id} failed."`, + { error }, + ); + } + }, + } + : { + type: "submenu", + submenu: _toTrayMenuOptions(trayMenuItem.id), + }), + + }; + }, + ), + ); + + return _toTrayMenuOptions(null); +}; diff --git a/src/main/tray/electron-tray/start-tray.injectable.ts b/src/main/tray/electron-tray/start-tray.injectable.ts new file mode 100644 index 0000000000..1a223ac3a5 --- /dev/null +++ b/src/main/tray/electron-tray/start-tray.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token"; +import electronTrayInjectable from "./electron-tray.injectable"; + +const startTrayInjectable = getInjectable({ + id: "start-tray", + + instantiate: (di) => { + const electronTray = di.inject(electronTrayInjectable); + + return { + run: () => { + electronTray.start(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default startTrayInjectable; diff --git a/src/main/tray/electron-tray/stop-tray.injectable.ts b/src/main/tray/electron-tray/stop-tray.injectable.ts new file mode 100644 index 0000000000..f66ffb3a64 --- /dev/null +++ b/src/main/tray/electron-tray/stop-tray.injectable.ts @@ -0,0 +1,28 @@ +/** + * 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 electronTrayInjectable from "./electron-tray.injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; +import stopReactiveTrayMenuItemsInjectable from "../reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable"; + +const stopTrayInjectable = getInjectable({ + id: "stop-tray", + + instantiate: (di) => { + const electronTray = di.inject(electronTrayInjectable); + + return { + run: () => { + electronTray.stop(); + }, + + runAfter: di.inject(stopReactiveTrayMenuItemsInjectable), + }; + }, + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default stopTrayInjectable; diff --git a/src/main/tray/install-tray.injectable.ts b/src/main/tray/install-tray.injectable.ts deleted file mode 100644 index 716d602101..0000000000 --- a/src/main/tray/install-tray.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 trayInjectable from "./tray.injectable"; -import { onLoadOfApplicationInjectionToken } from "../start-main-application/runnable-tokens/on-load-of-application-injection-token"; - -const installTrayInjectable = getInjectable({ - id: "install-tray", - - instantiate: (di) => { - const trayInitializer = di.inject(trayInjectable); - - return { - run: async () => { - await trayInitializer.start(); - }, - }; - }, - - injectionToken: onLoadOfApplicationInjectionToken, -}); - -export default installTrayInjectable; diff --git a/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts new file mode 100644 index 0000000000..b11654393a --- /dev/null +++ b/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts @@ -0,0 +1,24 @@ +/** + * 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 "../../../common/utils/get-startable-stoppable"; +import { autorun } from "mobx"; +import trayMenuItemsInjectable from "../tray-menu-item/tray-menu-items.injectable"; +import electronTrayInjectable from "../electron-tray/electron-tray.injectable"; + +const reactiveTrayMenuItemsInjectable = getInjectable({ + id: "reactive-tray-menu-items", + + instantiate: (di) => { + const electronTray = di.inject(electronTrayInjectable); + const trayMenuItems = di.inject(trayMenuItemsInjectable); + + return getStartableStoppable("reactive-tray-menu-items", () => autorun(() => { + electronTray.setMenuItems(trayMenuItems.get()); + })); + }, +}); + +export default reactiveTrayMenuItemsInjectable; diff --git a/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts new file mode 100644 index 0000000000..63025e6a9a --- /dev/null +++ b/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts @@ -0,0 +1,28 @@ +/** + * 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 reactiveTrayMenuItemsInjectable from "./reactive-tray-menu-items.injectable"; +import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token"; +import startTrayInjectable from "../electron-tray/start-tray.injectable"; + +const startReactiveTrayMenuItemsInjectable = getInjectable({ + id: "start-reactive-tray-menu-items", + + instantiate: (di) => { + const reactiveTrayMenuItems = di.inject(reactiveTrayMenuItemsInjectable); + + return { + run: async () => { + await reactiveTrayMenuItems.start(); + }, + + runAfter: di.inject(startTrayInjectable), + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default startReactiveTrayMenuItemsInjectable; diff --git a/src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable.ts new file mode 100644 index 0000000000..384cdc253a --- /dev/null +++ b/src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import reactiveTrayMenuItemsInjectable from "./reactive-tray-menu-items.injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; + +const stopReactiveTrayMenuItemsInjectable = getInjectable({ + id: "stop-reactive-tray-menu-items", + + instantiate: (di) => { + const reactiveTrayMenuItems = di.inject(reactiveTrayMenuItemsInjectable); + + return { + run: async () => { + await reactiveTrayMenuItems.stop(); + }, + }; + }, + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default stopReactiveTrayMenuItemsInjectable; diff --git a/src/main/tray/tray-menu-item/implementations/about-app-tray-item.injectable.ts b/src/main/tray/tray-menu-item/implementations/about-app-tray-item.injectable.ts new file mode 100644 index 0000000000..5fb1a9f34f --- /dev/null +++ b/src/main/tray/tray-menu-item/implementations/about-app-tray-item.injectable.ts @@ -0,0 +1,51 @@ +/** + * 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 productNameInjectable from "../../../app-paths/app-name/product-name.injectable"; +import showApplicationWindowInjectable from "../../../start-main-application/lens-window/show-application-window.injectable"; +import showAboutInjectable from "../../../menu/show-about.injectable"; +import { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token"; +import { computed } from "mobx"; +import withErrorLoggingInjectable from "../../../../common/utils/with-error-logging/with-error-logging.injectable"; +import { withErrorSuppression } from "../../../../common/utils/with-error-suppression/with-error-suppression"; +import { pipeline } from "@ogre-tools/fp"; + +const aboutAppTrayItemInjectable = getInjectable({ + id: "about-app-tray-item", + + instantiate: (di) => { + const productName = di.inject(productNameInjectable); + const showApplicationWindow = di.inject(showApplicationWindowInjectable); + const showAbout = di.inject(showAboutInjectable); + const withErrorLoggingFor = di.inject(withErrorLoggingInjectable); + + return { + id: "about-app", + parentId: null, + orderNumber: 140, + label: computed(() => `About ${productName}`), + enabled: computed(() => true), + visible: computed(() => true), + + click: pipeline( + async () => { + await showApplicationWindow(); + + await showAbout(); + }, + + withErrorLoggingFor(() => "[TRAY]: Opening of show about failed."), + + // TODO: Find out how to improve typing so that instead of + // x => withErrorSuppression(x) there could only be withErrorSuppression + (x) => withErrorSuppression(x), + ), + }; + }, + + injectionToken: trayMenuItemInjectionToken, +}); + +export default aboutAppTrayItemInjectable; diff --git a/src/main/tray/tray-menu-item/implementations/open-app-tray-item.injectable.ts b/src/main/tray/tray-menu-item/implementations/open-app-tray-item.injectable.ts new file mode 100644 index 0000000000..ff19d7718a --- /dev/null +++ b/src/main/tray/tray-menu-item/implementations/open-app-tray-item.injectable.ts @@ -0,0 +1,47 @@ +/** + * 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 { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token"; +import productNameInjectable from "../../../app-paths/app-name/product-name.injectable"; +import showApplicationWindowInjectable from "../../../start-main-application/lens-window/show-application-window.injectable"; +import { computed } from "mobx"; +import withErrorLoggingInjectable from "../../../../common/utils/with-error-logging/with-error-logging.injectable"; +import { withErrorSuppression } from "../../../../common/utils/with-error-suppression/with-error-suppression"; +import { pipeline } from "@ogre-tools/fp"; + +const openAppTrayItemInjectable = getInjectable({ + id: "open-app-tray-item", + + instantiate: (di) => { + const productName = di.inject(productNameInjectable); + const showApplicationWindow = di.inject(showApplicationWindowInjectable); + const withErrorLoggingFor = di.inject(withErrorLoggingInjectable); + + return { + id: "open-app", + parentId: null, + label: computed(() => `Open ${productName}`), + orderNumber: 10, + enabled: computed(() => true), + visible: computed(() => true), + + click: pipeline( + async () => { + await showApplicationWindow(); + }, + + withErrorLoggingFor(() => "[TRAY]: Opening of application window failed."), + + // TODO: Find out how to improve typing so that instead of + // x => withErrorSuppression(x) there could only be withErrorSuppression + (x) => withErrorSuppression(x), + ), + }; + }, + + injectionToken: trayMenuItemInjectionToken, +}); + +export default openAppTrayItemInjectable; diff --git a/src/main/tray/tray-menu-item/implementations/open-preferences-tray-item.injectable.ts b/src/main/tray/tray-menu-item/implementations/open-preferences-tray-item.injectable.ts new file mode 100644 index 0000000000..8c062f6a29 --- /dev/null +++ b/src/main/tray/tray-menu-item/implementations/open-preferences-tray-item.injectable.ts @@ -0,0 +1,43 @@ +/** + * 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 { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token"; +import navigateToPreferencesInjectable from "../../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; +import { computed } from "mobx"; +import { withErrorSuppression } from "../../../../common/utils/with-error-suppression/with-error-suppression"; +import { pipeline } from "@ogre-tools/fp"; +import withErrorLoggingInjectable from "../../../../common/utils/with-error-logging/with-error-logging.injectable"; + +const openPreferencesTrayItemInjectable = getInjectable({ + id: "open-preferences-tray-item", + + instantiate: (di) => { + const navigateToPreferences = di.inject(navigateToPreferencesInjectable); + const withErrorLoggingFor = di.inject(withErrorLoggingInjectable); + + return { + id: "open-preferences", + parentId: null, + label: computed(() => "Preferences"), + orderNumber: 20, + enabled: computed(() => true), + visible: computed(() => true), + + click: pipeline( + navigateToPreferences, + + withErrorLoggingFor(() => "[TRAY]: Opening of preferences failed."), + + // TODO: Find out how to improve typing so that instead of + // x => withErrorSuppression(x) there could only be withErrorSuppression + (x) => withErrorSuppression(x), + ), + }; + }, + + injectionToken: trayMenuItemInjectionToken, +}); + +export default openPreferencesTrayItemInjectable; diff --git a/src/main/tray/tray-menu-item/implementations/quit-app-separator-tray-item.injectable.ts b/src/main/tray/tray-menu-item/implementations/quit-app-separator-tray-item.injectable.ts new file mode 100644 index 0000000000..de83a92fe6 --- /dev/null +++ b/src/main/tray/tray-menu-item/implementations/quit-app-separator-tray-item.injectable.ts @@ -0,0 +1,24 @@ +/** + * 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 { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token"; +import { computed } from "mobx"; + +const quitAppSeparatorTrayItemInjectable = getInjectable({ + id: "quit-app-separator-tray-item", + + instantiate: () => ({ + id: "quit-app-separator", + parentId: null, + orderNumber: 149, + enabled: computed(() => true), + visible: computed(() => true), + separator: true, + }), + + injectionToken: trayMenuItemInjectionToken, +}); + +export default quitAppSeparatorTrayItemInjectable; diff --git a/src/main/tray/tray-menu-item/implementations/quit-app-tray-item.injectable.ts b/src/main/tray/tray-menu-item/implementations/quit-app-tray-item.injectable.ts new file mode 100644 index 0000000000..894a823511 --- /dev/null +++ b/src/main/tray/tray-menu-item/implementations/quit-app-tray-item.injectable.ts @@ -0,0 +1,43 @@ +/** + * 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 { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token"; +import { computed } from "mobx"; +import stopServicesAndExitAppInjectable from "../../../stop-services-and-exit-app.injectable"; +import { withErrorSuppression } from "../../../../common/utils/with-error-suppression/with-error-suppression"; +import { pipeline } from "@ogre-tools/fp"; +import withErrorLoggingInjectable from "../../../../common/utils/with-error-logging/with-error-logging.injectable"; + +const quitAppTrayItemInjectable = getInjectable({ + id: "quit-app-tray-item", + + instantiate: (di) => { + const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable); + const withErrorLoggingFor = di.inject(withErrorLoggingInjectable); + + return { + id: "quit-app", + parentId: null, + orderNumber: 150, + label: computed(() => "Quit App"), + enabled: computed(() => true), + visible: computed(() => true), + + click: pipeline( + stopServicesAndExitApp, + + withErrorLoggingFor(() => "[TRAY]: Quitting application failed."), + + // TODO: Find out how to improve typing so that instead of + // x => withErrorSuppression(x) there could only be withErrorSuppression + (x) => withErrorSuppression(x), + ), + }; + }, + + injectionToken: trayMenuItemInjectionToken, +}); + +export default quitAppTrayItemInjectable; diff --git a/src/main/tray/tray-menu-item/tray-menu-item-injection-token.ts b/src/main/tray/tray-menu-item/tray-menu-item-injection-token.ts new file mode 100644 index 0000000000..f8e9d7c6cc --- /dev/null +++ b/src/main/tray/tray-menu-item/tray-menu-item-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable"; +import type { IComputedValue } from "mobx"; +import type { LensMainExtension } from "../../../extensions/lens-main-extension"; + +export interface TrayMenuItem { + id: string; + parentId: string | null; + orderNumber: number; + enabled: IComputedValue; + visible: IComputedValue; + + label?: IComputedValue; + click?: () => Promise | void; + tooltip?: string; + separator?: boolean; + extension?: LensMainExtension; +} + +export const trayMenuItemInjectionToken = getInjectionToken({ + id: "tray-menu-item", +}); diff --git a/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts b/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts new file mode 100644 index 0000000000..6cbd9e5d33 --- /dev/null +++ b/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts @@ -0,0 +1,90 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { pipeline } from "@ogre-tools/fp"; +import { flatMap, kebabCase } from "lodash/fp"; +import type { Injectable } from "@ogre-tools/injectable"; +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token"; +import type { LensMainExtension } from "../../../extensions/lens-main-extension"; +import type { TrayMenuItem } from "./tray-menu-item-injection-token"; +import { trayMenuItemInjectionToken } from "./tray-menu-item-injection-token"; +import type { TrayMenuRegistration } from "../tray-menu-registration"; +import { withErrorSuppression } from "../../../common/utils/with-error-suppression/with-error-suppression"; +import type { WithErrorLoggingFor } from "../../../common/utils/with-error-logging/with-error-logging.injectable"; +import withErrorLoggingInjectable from "../../../common/utils/with-error-logging/with-error-logging.injectable"; + +const trayMenuItemRegistratorInjectable = getInjectable({ + id: "tray-menu-item-registrator", + + instantiate: (di) => (extension, installationCounter) => { + const mainExtension = extension as LensMainExtension; + const withErrorLoggingFor = di.inject(withErrorLoggingInjectable); + + pipeline( + mainExtension.trayMenus, + + flatMap(toItemInjectablesFor(mainExtension, installationCounter, withErrorLoggingFor)), + + (injectables) => di.register(...injectables), + ); + }, + + injectionToken: extensionRegistratorInjectionToken, +}); + +export default trayMenuItemRegistratorInjectable; + +const toItemInjectablesFor = (extension: LensMainExtension, installationCounter: number, withErrorLoggingFor: WithErrorLoggingFor) => { + const _toItemInjectables = (parentId: string | null) => (registration: TrayMenuRegistration): Injectable[] => { + const trayItemId = registration.id || kebabCase(registration.label || ""); + const id = `${trayItemId}-tray-menu-item-for-extension-${extension.sanitizedExtensionId}-instance-${installationCounter}`; + + const parentInjectable = getInjectable({ + id, + + instantiate: () => ({ + id, + parentId, + orderNumber: 100, + + separator: registration.type === "separator", + + label: computed(() => registration.label || ""), + tooltip: registration.toolTip, + + click: pipeline( + () => { + registration.click?.(registration); + }, + + withErrorLoggingFor(() => `[TRAY]: Clicking of tray item "${trayItemId}" from extension "${extension.sanitizedExtensionId}" failed.`), + + // TODO: Find out how to improve typing so that instead of + // x => withErrorSuppression(x) there could only be withErrorSuppression + (x) => withErrorSuppression(x), + ), + + enabled: computed(() => !!registration.enabled), + visible: computed(() => true), + }), + + injectionToken: trayMenuItemInjectionToken, + }); + + const childMenuItems = registration.submenu || []; + + const childInjectables = childMenuItems.flatMap(_toItemInjectables(id)); + + return [ + parentInjectable, + ...childInjectables, + ]; + }; + + return _toItemInjectables(null); +}; + + diff --git a/src/main/tray/tray-menu-item/tray-menu-items.injectable.ts b/src/main/tray/tray-menu-item/tray-menu-items.injectable.ts new file mode 100644 index 0000000000..c29482007d --- /dev/null +++ b/src/main/tray/tray-menu-item/tray-menu-items.injectable.ts @@ -0,0 +1,49 @@ +/** + * 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 { computed } from "mobx"; + + +import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable"; +import type { TrayMenuItem } from "./tray-menu-item-injection-token"; +import { trayMenuItemInjectionToken } from "./tray-menu-item-injection-token"; +import { pipeline } from "@ogre-tools/fp"; +import { filter, overSome, sortBy } from "lodash/fp"; +import type { LensMainExtension } from "../../../extensions/lens-main-extension"; + +const trayMenuItemsInjectable = getInjectable({ + id: "tray-menu-items", + + instantiate: (di) => { + const extensions = di.inject(mainExtensionsInjectable); + + return computed(() => { + const enabledExtensions = extensions.get(); + + return pipeline( + di.injectMany(trayMenuItemInjectionToken), + + filter((item) => + overSome([ + isNonExtensionItem, + isEnabledExtensionItemFor(enabledExtensions), + ])(item), + ), + + filter(item => item.visible.get()), + items => sortBy("orderNumber", items), + ); + }); + }, +}); + +const isNonExtensionItem = (item: TrayMenuItem) => !item.extension; + +const isEnabledExtensionItemFor = + (enabledExtensions: LensMainExtension[]) => (item: TrayMenuItem) => + !!enabledExtensions.find((extension) => extension === item.extension); + + +export default trayMenuItemsInjectable; diff --git a/src/main/tray/tray.injectable.ts b/src/main/tray/tray.injectable.ts deleted file mode 100644 index 0e61062d50..0000000000 --- a/src/main/tray/tray.injectable.ts +++ /dev/null @@ -1,42 +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 { initTray } from "./tray"; -import trayMenuItemsInjectable from "./tray-menu-items.injectable"; -import navigateToPreferencesInjectable from "../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; -import stopServicesAndExitAppInjectable from "../stop-services-and-exit-app.injectable"; -import { getStartableStoppable } from "../../common/utils/get-startable-stoppable"; -import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable"; -import showAboutInjectable from "../menu/show-about.injectable"; -import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable"; -import trayIconPathInjectable from "./tray-icon-path.injectable"; - -const trayInjectable = getInjectable({ - id: "tray", - - instantiate: (di) => { - const trayMenuItems = di.inject(trayMenuItemsInjectable); - const navigateToPreferences = di.inject(navigateToPreferencesInjectable); - const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable); - const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable); - const showApplicationWindow = di.inject(showApplicationWindowInjectable); - const showAboutPopup = di.inject(showAboutInjectable); - const trayIconPath = di.inject(trayIconPathInjectable); - - return getStartableStoppable("build-of-tray", () => - initTray( - trayMenuItems, - navigateToPreferences, - stopServicesAndExitApp, - isAutoUpdateEnabled, - showApplicationWindow, - showAboutPopup, - trayIconPath, - ), - ); - }, -}); - -export default trayInjectable; diff --git a/src/main/tray/tray.ts b/src/main/tray/tray.ts index c5d0b47ab1..4d7e39c344 100644 --- a/src/main/tray/tray.ts +++ b/src/main/tray/tray.ts @@ -7,25 +7,22 @@ import packageInfo from "../../../package.json"; import { Menu, Tray } from "electron"; import type { IComputedValue } from "mobx"; import { autorun } from "mobx"; -import { checkForUpdates } from "../app-updater"; import logger from "../logger"; -import { isWindows, productName } from "../../common/vars"; +import { isWindows } from "../../common/vars"; import type { Disposer } from "../../common/utils"; -import { disposer, toJS } from "../../common/utils"; -import type { TrayMenuRegistration } from "./tray-menu-registration"; +import { disposer } from "../../common/utils"; +import type { TrayMenuItem } from "./tray-menu-item/tray-menu-item-injection-token"; +import { pipeline } from "@ogre-tools/fp"; +import { filter, isEmpty, map } from "lodash/fp"; -const TRAY_LOG_PREFIX = "[TRAY]"; +export const TRAY_LOG_PREFIX = "[TRAY]"; // note: instance of Tray should be saved somewhere, otherwise it disappears export let tray: Tray | null = null; export function initTray( - trayMenuItems: IComputedValue, - navigateToPreferences: () => void, - stopServicesAndExitApp: () => void, - isAutoUpdateEnabled: () => boolean, + trayMenuItems: IComputedValue, showApplicationWindow: () => Promise, - showAbout: () => void, trayIconPath: string, ): Disposer { tray = new Tray(trayIconPath); @@ -42,7 +39,9 @@ export function initTray( return disposer( autorun(() => { try { - const menu = createTrayMenu(toJS(trayMenuItems.get()), navigateToPreferences, stopServicesAndExitApp, isAutoUpdateEnabled, showApplicationWindow, showAbout); + const options = toTrayMenuOptions(trayMenuItems.get()); + + const menu = Menu.buildFromTemplate(options); tray?.setContextMenu(menu); } catch (error) { @@ -56,66 +55,45 @@ export function initTray( ); } -function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions { - return { - ...trayItem, - submenu: trayItem.submenu ? trayItem.submenu.map(getMenuItemConstructorOptions) : undefined, - click: trayItem.click ? () => { - trayItem.click?.(trayItem); - } : undefined, - }; -} +const toTrayMenuOptions = (trayMenuItems: TrayMenuItem[]) => { + const _toTrayMenuOptions = (parentId: string | null) => + pipeline( + trayMenuItems, -function createTrayMenu( - extensionTrayItems: TrayMenuRegistration[], - navigateToPreferences: () => void, - stopServicesAndExitApp: () => void, - isAutoUpdateEnabled: () => boolean, - showApplicationWindow: () => Promise, - showAbout: () => void, -): Menu { - let template: Electron.MenuItemConstructorOptions[] = [ - { - label: `Open ${productName}`, - click() { - showApplicationWindow().catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error })); - }, - }, - { - label: "Preferences", - click() { - navigateToPreferences(); - }, - }, - ]; + filter((item) => item.parentId === parentId), - if (isAutoUpdateEnabled()) { - template.push({ - label: "Check for updates", - click() { - checkForUpdates() - .then(() => showApplicationWindow()); - }, - }); - } + map( + (trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => { + if (trayMenuItem.separator) { + return { id: trayMenuItem.id, type: "separator" }; + } - template = template.concat(extensionTrayItems.map(getMenuItemConstructorOptions)); + const childItems = _toTrayMenuOptions(trayMenuItem.id); + + return { + id: trayMenuItem.id, + label: trayMenuItem.label?.get(), + enabled: trayMenuItem.enabled.get(), + toolTip: trayMenuItem.tooltip, + + ...(isEmpty(childItems) + ? { + type: "normal", + submenu: _toTrayMenuOptions(trayMenuItem.id), + + click: () => { + trayMenuItem.click?.(); + }, + } + : { + type: "submenu", + submenu: _toTrayMenuOptions(trayMenuItem.id), + }), + }; + }, + ), + ); + + return _toTrayMenuOptions(null); +}; - return Menu.buildFromTemplate(template.concat([ - { - label: `About ${productName}`, - click() { - showApplicationWindow() - .then(showAbout) - .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to show Lens About view`, { error })); - }, - }, - { type: "separator" }, - { - label: "Quit App", - click() { - stopServicesAndExitApp(); - }, - }, - ])); -} diff --git a/src/main/tray/uninstall-tray.injectable.ts b/src/main/tray/uninstall-tray.injectable.ts deleted file mode 100644 index 41b3cd676c..0000000000 --- a/src/main/tray/uninstall-tray.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 trayInjectable from "./tray.injectable"; -import { beforeQuitOfBackEndInjectionToken } from "../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; - -const uninstallTrayInjectable = getInjectable({ - id: "uninstall-tray", - - instantiate: (di) => { - const trayInitializer = di.inject(trayInjectable); - - return { - run: async () => { - await trayInitializer.stop(); - }, - }; - }, - - injectionToken: beforeQuitOfBackEndInjectionToken, -}); - -export default uninstallTrayInjectable; diff --git a/src/main/utils/__test__/update-channel.test.ts b/src/main/utils/__test__/update-channel.test.ts deleted file mode 100644 index e0c20e4707..0000000000 --- a/src/main/utils/__test__/update-channel.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { nextUpdateChannel } from "../update-channel"; - -describe("nextUpdateChannel", () => { - it("returns latest if current channel is latest", () => { - expect(nextUpdateChannel("latest", "latest")).toEqual("latest"); - }); - - it("returns beta if current channel is alpha", () => { - expect(nextUpdateChannel("alpha", "alpha")).toEqual("beta"); - expect(nextUpdateChannel("beta", "alpha")).toEqual("beta"); - expect(nextUpdateChannel("rc", "alpha")).toEqual("beta"); - expect(nextUpdateChannel("latest", "alpha")).toEqual("beta"); - }); - - it("returns latest if current channel is beta", () => { - expect(nextUpdateChannel("alpha", "beta")).toEqual("latest"); - expect(nextUpdateChannel("beta", "beta")).toEqual("latest"); - expect(nextUpdateChannel("rc", "beta")).toEqual("latest"); - expect(nextUpdateChannel("latest", "beta")).toEqual("latest"); - }); - - it("returns default if current channel is unknown", () => { - expect(nextUpdateChannel("alpha", "rc")).toEqual("alpha"); - expect(nextUpdateChannel("beta", "rc")).toEqual("beta"); - expect(nextUpdateChannel("rc", "rc")).toEqual("rc"); - expect(nextUpdateChannel("latest", "rc")).toEqual("latest"); - }); -}); diff --git a/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts b/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts new file mode 100644 index 0000000000..6b7fa9b8df --- /dev/null +++ b/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts @@ -0,0 +1,38 @@ +/** + * 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 ipcMainInjectable from "../ipc-main/ipc-main.injectable"; +import { enlistMessageChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token"; +import { pipeline } from "@ogre-tools/fp"; +import { tentativeParseJson } from "../../../../common/utils/tentative-parse-json"; + +const enlistMessageChannelListenerInjectable = getInjectable({ + id: "enlist-message-channel-listener-for-main", + + instantiate: (di) => { + const ipcMain = di.inject(ipcMainInjectable); + + return ({ channel, handler }) => { + const nativeOnCallback = (_: IpcMainEvent, message: unknown) => { + pipeline( + message, + tentativeParseJson, + handler, + ); + }; + + ipcMain.on(channel.id, nativeOnCallback); + + return () => { + ipcMain.off(channel.id, nativeOnCallback); + }; + }; + }, + + injectionToken: enlistMessageChannelListenerInjectionToken, +}); + +export default enlistMessageChannelListenerInjectable; diff --git a/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts b/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts new file mode 100644 index 0000000000..3bd0398d8e --- /dev/null +++ b/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts @@ -0,0 +1,97 @@ +/** + * 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"; + +describe("enlist message channel listener in main", () => { + let enlistMessageChannelListener: EnlistMessageChannelListener; + let ipcMainStub: IpcMain; + let onMock: jest.Mock; + let offMock: jest.Mock; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + onMock = jest.fn(); + offMock = jest.fn(); + + ipcMainStub = { + on: onMock, + off: offMock, + } as unknown as IpcMain; + + di.override(ipcMainInjectable, () => ipcMainStub); + + enlistMessageChannelListener = di.inject( + enlistMessageChannelListenerInjectionToken, + ); + }); + + describe("when called", () => { + let handlerMock: jest.Mock; + let disposer: () => void; + + beforeEach(() => { + handlerMock = jest.fn(); + + disposer = enlistMessageChannelListener({ + channel: { id: "some-channel-id" }, + handler: handlerMock, + }); + }); + + it("does not call handler yet", () => { + expect(handlerMock).not.toHaveBeenCalled(); + }); + + it("registers the listener", () => { + expect(onMock).toHaveBeenCalledWith( + "some-channel-id", + expect.any(Function), + ); + }); + + it("does not de-register the listener yet", () => { + expect(offMock).not.toHaveBeenCalled(); + }); + + describe("when message arrives", () => { + beforeEach(() => { + onMock.mock.calls[0][1]({} as IpcMainEvent, "some-message"); + }); + + it("calls the handler with the message", () => { + expect(handlerMock).toHaveBeenCalledWith("some-message"); + }); + + it("when disposing the listener, de-registers the listener", () => { + disposer(); + + expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function)); + }); + }); + + it("given number as message, when message arrives, calls the handler with the message", () => { + onMock.mock.calls[0][1]({} as IpcMainEvent, 42); + + expect(handlerMock).toHaveBeenCalledWith(42); + }); + + it("given boolean as message, when message arrives, calls the handler with the message", () => { + onMock.mock.calls[0][1]({} as IpcMainEvent, true); + + expect(handlerMock).toHaveBeenCalledWith(true); + }); + + it("given stringified object as message, when message arrives, calls the handler with the message", () => { + onMock.mock.calls[0][1]({} as IpcMainEvent, JSON.stringify({ some: "object" })); + + expect(handlerMock).toHaveBeenCalledWith({ some: "object" }); + }); + }); +}); diff --git a/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.injectable.ts b/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.injectable.ts new file mode 100644 index 0000000000..6f118288f3 --- /dev/null +++ b/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.injectable.ts @@ -0,0 +1,34 @@ +/** + * 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 ipcMainInjectable from "../ipc-main/ipc-main.injectable"; +import { enlistRequestChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-request-channel-listener-injection-token"; +import { pipeline } from "@ogre-tools/fp"; +import { tentativeParseJson } from "../../../../common/utils/tentative-parse-json"; +import { tentativeStringifyJson } from "../../../../common/utils/tentative-stringify-json"; + +const enlistRequestChannelListenerInjectable = getInjectable({ + id: "enlist-request-channel-listener-for-main", + + instantiate: (di) => { + const ipcMain = di.inject(ipcMainInjectable); + + return ({ channel, handler }) => { + const nativeHandleCallback = (_: IpcMainInvokeEvent, request: unknown) => + pipeline(request, tentativeParseJson, handler, tentativeStringifyJson); + + ipcMain.handle(channel.id, nativeHandleCallback); + + return () => { + ipcMain.off(channel.id, nativeHandleCallback); + }; + }; + }, + + injectionToken: enlistRequestChannelListenerInjectionToken, +}); + +export default enlistRequestChannelListenerInjectable; diff --git a/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.test.ts b/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.test.ts new file mode 100644 index 0000000000..12a5e9af74 --- /dev/null +++ b/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.test.ts @@ -0,0 +1,147 @@ +/** + * 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 type { EnlistRequestChannelListener } from "../../../../common/utils/channel/enlist-request-channel-listener-injection-token"; +import { enlistRequestChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-request-channel-listener-injection-token"; +import { getPromiseStatus } from "../../../../common/test-utils/get-promise-status"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; + +describe("enlist request channel listener in main", () => { + let enlistRequestChannelListener: EnlistRequestChannelListener; + let ipcMainStub: IpcMain; + let handleMock: jest.Mock; + let offMock: jest.Mock; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + handleMock = jest.fn(); + offMock = jest.fn(); + + ipcMainStub = { + handle: handleMock, + off: offMock, + } as unknown as IpcMain; + + di.override(ipcMainInjectable, () => ipcMainStub); + + enlistRequestChannelListener = di.inject( + enlistRequestChannelListenerInjectionToken, + ); + }); + + describe("when called", () => { + let handlerMock: AsyncFnMock<(message: any) => any>; + let disposer: () => void; + + beforeEach(() => { + handlerMock = asyncFn(); + + disposer = enlistRequestChannelListener({ + channel: { id: "some-channel-id" }, + handler: handlerMock, + }); + }); + + it("does not call handler yet", () => { + expect(handlerMock).not.toHaveBeenCalled(); + }); + + it("registers the listener", () => { + expect(handleMock).toHaveBeenCalledWith( + "some-channel-id", + expect.any(Function), + ); + }); + + it("does not de-register the listener yet", () => { + expect(offMock).not.toHaveBeenCalled(); + }); + + describe("when request arrives", () => { + let actualPromise: Promise; + + beforeEach(() => { + actualPromise = handleMock.mock.calls[0][1]( + {} as IpcMainInvokeEvent, + "some-request", + ); + }); + + it("calls the handler with the request", () => { + expect(handlerMock).toHaveBeenCalledWith("some-request"); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + describe("when handler resolves with response, listener resolves with the response", () => { + beforeEach(async () => { + await handlerMock.resolve("some-response"); + }); + + it("resolves with the response", async () => { + const actual = await actualPromise; + + expect(actual).toBe('"some-response"'); + }); + + it("when disposing the listener, de-registers the listener", () => { + disposer(); + + expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function)); + }); + }); + + it("given number as response, when handler resolves with response, listener resolves with stringified response", async () => { + await handlerMock.resolve(42); + + const actual = await actualPromise; + + expect(actual).toBe("42"); + }); + + it("given boolean as response, when handler resolves with response, listener resolves with stringified response", async () => { + await handlerMock.resolve(true); + + const actual = await actualPromise; + + expect(actual).toBe("true"); + }); + + it("given object as response, when handler resolves with response, listener resolves with stringified response", async () => { + await handlerMock.resolve({ some: "object" }); + + const actual = await actualPromise; + + expect(actual).toBe(JSON.stringify({ some: "object" })); + }); + }); + + it("given number as request, when request arrives, calls the handler with the request", () => { + handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, 42); + + expect(handlerMock).toHaveBeenCalledWith(42); + }); + + it("given boolean as request, when request arrives, calls the handler with the request", () => { + handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, true); + + expect(handlerMock).toHaveBeenCalledWith(true); + }); + + it("given stringified object as request, when request arrives, calls the handler with the request", () => { + handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, JSON.stringify({ some: "object" })); + + expect(handlerMock).toHaveBeenCalledWith({ some: "object" }); + }); + }); +}); diff --git a/src/main/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts b/src/main/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts new file mode 100644 index 0000000000..96fea0a2f0 --- /dev/null +++ b/src/main/utils/channel/channel-listeners/start-listening-of-channels.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { onLoadOfApplicationInjectionToken } from "../../../start-main-application/runnable-tokens/on-load-of-application-injection-token"; +import listeningOfChannelsInjectable from "../../../../common/utils/channel/listening-of-channels.injectable"; + +const startListeningOfChannelsInjectable = getInjectable({ + id: "start-listening-of-channels-main", + + instantiate: (di) => { + const listeningOfChannels = di.inject(listeningOfChannelsInjectable); + + return { + run: async () => { + await listeningOfChannels.start(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default startListeningOfChannelsInjectable; diff --git a/src/main/app-paths/register-channel/ipc-main/ipc-main.injectable.ts b/src/main/utils/channel/ipc-main/ipc-main.injectable.ts similarity index 100% rename from src/main/app-paths/register-channel/ipc-main/ipc-main.injectable.ts rename to src/main/utils/channel/ipc-main/ipc-main.injectable.ts diff --git a/src/main/utils/channel/message-to-channel.injectable.ts b/src/main/utils/channel/message-to-channel.injectable.ts new file mode 100644 index 0000000000..00e588a16a --- /dev/null +++ b/src/main/utils/channel/message-to-channel.injectable.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { lensWindowInjectionToken } from "../../start-main-application/lens-window/application-window/lens-window-injection-token"; +import { pipeline } from "@ogre-tools/fp"; +import { getInjectable } from "@ogre-tools/injectable"; +import { filter } from "lodash/fp"; +import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; +import type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token"; +import { tentativeStringifyJson } from "../../../common/utils/tentative-stringify-json"; + +const messageToChannelInjectable = getInjectable({ + id: "message-to-channel", + + instantiate: (di) => { + const getAllLensWindows = () => di.injectMany(lensWindowInjectionToken); + + // TODO: Figure out way to improve typing in internals + // Notice that this should be injected using "messageToChannelInjectionToken" which is typed correctly. + return (channel: MessageChannel, message?: unknown) => { + const stringifiedMessage = tentativeStringifyJson(message); + + + const visibleWindows = pipeline( + getAllLensWindows(), + filter((lensWindow) => !!lensWindow.visible), + ); + + visibleWindows.forEach((lensWindow) => + lensWindow.send({ channel: channel.id, data: stringifiedMessage ? [stringifiedMessage] : [] }), + ); + }; + }, + + injectionToken: messageToChannelInjectionToken, +}); + +export default messageToChannelInjectable; diff --git a/src/main/utils/channel/message-to-channel.test.ts b/src/main/utils/channel/message-to-channel.test.ts new file mode 100644 index 0000000000..cf2fc46549 --- /dev/null +++ b/src/main/utils/channel/message-to-channel.test.ts @@ -0,0 +1,167 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { MessageToChannel } from "../../../common/utils/channel/message-to-channel-injection-token"; +import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; +import closeAllWindowsInjectable from "../../start-main-application/lens-window/hide-all-windows/close-all-windows.injectable"; +import type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import createLensWindowInjectable from "../../start-main-application/lens-window/application-window/create-lens-window.injectable"; +import type { LensWindow } from "../../start-main-application/lens-window/application-window/lens-window-injection-token"; +import { lensWindowInjectionToken } from "../../start-main-application/lens-window/application-window/lens-window-injection-token"; +import type { DiContainer } from "@ogre-tools/injectable"; +import { getInjectable } from "@ogre-tools/injectable"; +import sendToChannelInElectronBrowserWindowInjectable from "../../start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable"; + +describe("message to channel from main", () => { + let messageToChannel: MessageToChannel; + let someTestWindow: LensWindow; + let someOtherTestWindow: LensWindow; + let sendToChannelInBrowserMock: jest.Mock; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + sendToChannelInBrowserMock = jest.fn(); + di.override(sendToChannelInElectronBrowserWindowInjectable, () => sendToChannelInBrowserMock); + + someTestWindow = createTestWindow(di, "some-test-window-id"); + someOtherTestWindow = createTestWindow(di, "some-other-test-window-id"); + + messageToChannel = di.inject(messageToChannelInjectionToken); + + const closeAllWindows = di.inject(closeAllWindowsInjectable); + + closeAllWindows(); + }); + + it("given no visible windows, when messaging to channel, does not message to any window", () => { + messageToChannel(someChannel, "some-message"); + + expect(sendToChannelInBrowserMock).not.toHaveBeenCalled(); + }); + + describe("given visible window", () => { + beforeEach(async () => { + await someTestWindow.show(); + }); + + it("when messaging to channel, messages to window", () => { + messageToChannel(someChannel, "some-message"); + + expect(sendToChannelInBrowserMock.mock.calls).toEqual([ + [ + null, + + { + channel: "some-channel", + data: ['"some-message"'], + }, + ], + ]); + }); + + it("given boolean as message, when messaging to channel, messages to window with stringified message", () => { + messageToChannel(someChannel, true); + + expect(sendToChannelInBrowserMock.mock.calls).toEqual([ + [ + null, + + { + channel: "some-channel", + data: ["true"], + }, + ], + ]); + }); + + it("given number as message, when messaging to channel, messages to window with stringified message", () => { + messageToChannel(someChannel, 42); + + expect(sendToChannelInBrowserMock.mock.calls).toEqual([ + [ + null, + + { + channel: "some-channel", + data: ["42"], + }, + ], + ]); + }); + + it("given object as message, when messaging to channel, messages to window with stringified message", () => { + messageToChannel(someChannel, { some: "object" }); + + expect(sendToChannelInBrowserMock.mock.calls).toEqual([ + [ + null, + + { + channel: "some-channel", + data: [JSON.stringify({ some: "object" })], + }, + ], + ]); + }); + }); + + it("given multiple visible windows, when messaging to channel, messages to window", async () => { + await someTestWindow.show(); + await someOtherTestWindow.show(); + + messageToChannel(someChannel, "some-message"); + + expect(sendToChannelInBrowserMock.mock.calls).toEqual([ + [ + null, + + { + channel: "some-channel", + data: ['"some-message"'], + }, + ], + + [ + null, + + { + channel: "some-channel", + data: ['"some-message"'], + }, + ], + ]); + }); +}); + +const someChannel: MessageChannel = { id: "some-channel" }; + +const createTestWindow = (di: DiContainer, id: string) => { + const testWindowInjectable = getInjectable({ + id, + + instantiate: (di) => { + const createLensWindow = di.inject(createLensWindowInjectable); + + return createLensWindow({ + id, + title: "Some test window", + defaultHeight: 42, + defaultWidth: 42, + getContentSource: () => ({ url: "some-content-url" }), + resizable: true, + windowFrameUtilitiesAreShown: false, + centered: false, + }); + }, + + injectionToken: lensWindowInjectionToken, + }); + + di.register(testWindowInjectable); + + return di.inject(testWindowInjectable); +}; diff --git a/src/main/utils/sync-box/sync-box-initial-value-channel-listener.injectable.ts b/src/main/utils/sync-box/sync-box-initial-value-channel-listener.injectable.ts new file mode 100644 index 0000000000..5eb043291a --- /dev/null +++ b/src/main/utils/sync-box/sync-box-initial-value-channel-listener.injectable.ts @@ -0,0 +1,31 @@ +/** + * 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 syncBoxInitialValueChannelInjectable from "../../../common/utils/sync-box/sync-box-initial-value-channel.injectable"; +import { syncBoxInjectionToken } from "../../../common/utils/sync-box/sync-box-injection-token"; +import { requestChannelListenerInjectionToken } from "../../../common/utils/channel/request-channel-listener-injection-token"; + +const syncBoxInitialValueChannelListenerInjectable = getInjectable({ + id: "sync-box-initial-value-channel-listener", + + instantiate: (di) => { + const channel = di.inject(syncBoxInitialValueChannelInjectable); + const syncBoxes = di.injectMany(syncBoxInjectionToken); + + return { + channel, + + handler: () => + syncBoxes.map((box) => ({ + id: box.id, + value: box.value.get(), + })), + }; + }, + + injectionToken: requestChannelListenerInjectionToken, +}); + +export default syncBoxInitialValueChannelListenerInjectable; diff --git a/src/main/utils/update-channel.ts b/src/main/utils/update-channel.ts deleted file mode 100644 index 598d0f0bfd..0000000000 --- a/src/main/utils/update-channel.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -/** - * Compute the next update channel from the current updating channel - * @param defaultChannel The default (initial) channel to check - * @param channel The current channel that did not have a new version associated with it - * @returns The channel name of the next release version - */ -export function nextUpdateChannel(defaultChannel: string, channel: string | null): string { - switch (channel) { - case "alpha": - return "beta"; - case "beta": - return "latest"; // there is no RC currently - default: - return defaultChannel; - } -} diff --git a/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable.ts b/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable.ts deleted file mode 100644 index b9ddee1007..0000000000 --- a/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.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/ipc-renderer.injectable"; -import { getValueFromRegisteredChannel } from "./get-value-from-registered-channel"; -import type { Channel } from "../../../common/ipc-channel/channel"; - -export type GetValueFromRegisteredChannel = , TInstance>(channel: TChannel) => Promise; - -const getValueFromRegisteredChannelInjectable = getInjectable({ - id: "get-value-from-registered-channel", - - instantiate: (di) => getValueFromRegisteredChannel({ - ipcRenderer: di.inject(ipcRendererInjectable), - }), -}); - -export default getValueFromRegisteredChannelInjectable; diff --git a/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts b/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts deleted file mode 100644 index 8e0c953784..0000000000 --- a/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { IpcRenderer } from "electron"; -import type { Channel } from "../../../common/ipc-channel/channel"; - -interface Dependencies { - ipcRenderer: IpcRenderer; -} - -export const getValueFromRegisteredChannel = ({ ipcRenderer }: Dependencies) => - , TInstance>( - channel: TChannel, - ): Promise => - ipcRenderer.invoke(channel.name); diff --git a/src/renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable.ts b/src/renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable.ts deleted file mode 100644 index 151d77f097..0000000000 --- a/src/renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.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 ipcRendererInjectable from "./ipc-renderer/ipc-renderer.injectable"; -import type { - IpcChannelListener, -} from "../../ipc-channel-listeners/ipc-channel-listener-injection-token"; - -const registerIpcChannelListenerInjectable = getInjectable({ - id: "register-ipc-channel-listener", - - instantiate: (di) => { - const ipc = di.inject(ipcRendererInjectable); - - return ({ channel, handle }: IpcChannelListener) => { - ipc.on(channel.name, (_, data) => { - handle(data); - }); - }; - }, -}); - -export default registerIpcChannelListenerInjectable; diff --git a/src/renderer/app-paths/setup-app-paths.injectable.ts b/src/renderer/app-paths/setup-app-paths.injectable.ts index e6cf30f0dd..14242347f4 100644 --- a/src/renderer/app-paths/setup-app-paths.injectable.ts +++ b/src/renderer/app-paths/setup-app-paths.injectable.ts @@ -3,27 +3,29 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { appPathsIpcChannel } from "../../common/app-paths/app-path-injection-token"; -import getValueFromRegisteredChannelInjectable from "./get-value-from-registered-channel/get-value-from-registered-channel.injectable"; import appPathsStateInjectable from "../../common/app-paths/app-paths-state.injectable"; import { beforeFrameStartsInjectionToken } from "../before-frame-starts/before-frame-starts-injection-token"; +import appPathsChannelInjectable from "../../common/app-paths/app-paths-channel.injectable"; +import { requestFromChannelInjectionToken } from "../../common/utils/channel/request-from-channel-injection-token"; const setupAppPathsInjectable = getInjectable({ id: "setup-app-paths", - instantiate: (di) => ({ - run: async () => { - const getValueFromRegisteredChannel = di.inject( - getValueFromRegisteredChannelInjectable, - ); + instantiate: (di) => { + const requestFromChannel = di.inject(requestFromChannelInjectionToken); + const appPathsChannel = di.inject(appPathsChannelInjectable); + const appPathsState = di.inject(appPathsStateInjectable); - const syncAppPaths = await getValueFromRegisteredChannel(appPathsIpcChannel); + return { + run: async () => { + const appPaths = await requestFromChannel( + appPathsChannel, + ); - const appPathsState = di.inject(appPathsStateInjectable); - - appPathsState.set(syncAppPaths); - }, - }), + appPathsState.set(appPaths); + }, + }; + }, injectionToken: beforeFrameStartsInjectionToken, }); diff --git a/src/renderer/application-update/application-update-status-listener.injectable.ts b/src/renderer/application-update/application-update-status-listener.injectable.ts new file mode 100644 index 0000000000..69ba23608c --- /dev/null +++ b/src/renderer/application-update/application-update-status-listener.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 { getInjectable } from "@ogre-tools/injectable"; +import type { ApplicationUpdateStatusChannel, ApplicationUpdateStatusEventId } from "../../common/application-update/application-update-status-channel.injectable"; +import applicationUpdateStatusChannelInjectable from "../../common/application-update/application-update-status-channel.injectable"; +import showInfoNotificationInjectable from "../components/notifications/show-info-notification.injectable"; +import type { MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token"; +import { messageChannelListenerInjectionToken } from "../../common/utils/channel/message-channel-listener-injection-token"; + +const applicationUpdateStatusListenerInjectable = getInjectable({ + id: "application-update-status-listener", + + instantiate: (di): MessageChannelListener => { + const channel = di.inject(applicationUpdateStatusChannelInjectable); + const showInfoNotification = di.inject(showInfoNotificationInjectable); + + const eventHandlers: Record void }> = { + "checking-for-updates": { + handle: () => { + showInfoNotification("Checking for updates..."); + }, + }, + + "no-updates-available": { + handle: () => { + showInfoNotification("No new updates available"); + }, + }, + + "download-for-update-started": { + handle: (version) => { + showInfoNotification(`Download for version ${version} started...`); + }, + }, + + "download-for-update-failed": { + handle: () => { + showInfoNotification("Download of update failed"); + }, + }, + }; + + return { + channel, + + handler: ({ eventId, version }) => { + eventHandlers[eventId].handle(version); + }, + }; + }, + + injectionToken: messageChannelListenerInjectionToken, +}); + +export default applicationUpdateStatusListenerInjectable; diff --git a/src/renderer/ask-boolean/ask-boolean-question-channel-listener.injectable.tsx b/src/renderer/ask-boolean/ask-boolean-question-channel-listener.injectable.tsx new file mode 100644 index 0000000000..5e9adff4cc --- /dev/null +++ b/src/renderer/ask-boolean/ask-boolean-question-channel-listener.injectable.tsx @@ -0,0 +1,107 @@ +/** + * 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 { AskBooleanQuestionChannel } from "../../common/ask-boolean/ask-boolean-question-channel.injectable"; +import askBooleanQuestionChannelInjectable from "../../common/ask-boolean/ask-boolean-question-channel.injectable"; +import showInfoNotificationInjectable from "../components/notifications/show-info-notification.injectable"; +import { Button } from "../components/button"; +import React from "react"; +import { messageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; +import askBooleanAnswerChannelInjectable from "../../common/ask-boolean/ask-boolean-answer-channel.injectable"; +import notificationsStoreInjectable from "../components/notifications/notifications-store.injectable"; +import type { MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token"; +import { messageChannelListenerInjectionToken } from "../../common/utils/channel/message-channel-listener-injection-token"; + +const askBooleanQuestionChannelListenerInjectable = getInjectable({ + id: "ask-boolean-question-channel-listener", + + instantiate: (di): MessageChannelListener => { + const questionChannel = di.inject(askBooleanQuestionChannelInjectable); + const showInfoNotification = di.inject(showInfoNotificationInjectable); + const messageToChannel = di.inject(messageToChannelInjectionToken); + const answerChannel = di.inject(askBooleanAnswerChannelInjectable); + const notificationsStore = di.inject(notificationsStoreInjectable); + + const sendAnswerFor = (id: string) => (value: boolean) => { + messageToChannel(answerChannel, { id, value }); + }; + + const closeNotification = (notificationId: string) => { + notificationsStore.remove(notificationId); + }; + + const sendAnswerAndCloseNotificationFor = (sendAnswer: (value: boolean) => void, notificationId: string) => (value: boolean) => () => { + sendAnswer(value); + closeNotification(notificationId); + }; + + return { + channel: questionChannel, + + handler: ({ id: questionId, title, question }) => { + const notificationId = `ask-boolean-for-${questionId}`; + + const sendAnswer = sendAnswerFor(questionId); + const sendAnswerAndCloseNotification = sendAnswerAndCloseNotificationFor(sendAnswer, notificationId); + + showInfoNotification( + , + + { + id: notificationId, + timeout: 0, + onClose: () => sendAnswer(false), + }, + ); + }, + }; + }, + + injectionToken: messageChannelListenerInjectionToken, +}); + +export default askBooleanQuestionChannelListenerInjectable; + +const AskBoolean = ({ + id, + title, + message, + onNo, + onYes, +}: { + id: string; + title: string; + message: string; + onNo: () => void; + onYes: () => void; +}) => ( +
    + {title} +

    {message}

    + +
    +
    +
    +); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index b2fa7700b9..fabb17cb5a 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -46,8 +46,7 @@ import { init } from "@sentry/electron/renderer"; import kubernetesClusterCategoryInjectable from "../common/catalog/categories/kubernetes-cluster.injectable"; import autoRegistrationInjectable from "../common/k8s-api/api-manager/auto-registration.injectable"; import assert from "assert"; -import { beforeFrameStartsInjectionToken } from "./before-frame-starts/before-frame-starts-injection-token"; -import { runManyFor } from "../common/runnable/run-many-for"; +import startFrameInjectable from "./start-frame/start-frame.injectable"; configurePackages(); // global packages registerCustomThemes(); // monaco editor themes @@ -68,9 +67,9 @@ export async function bootstrap(di: DiContainer) { initializeSentryReporting(init); } - const beforeFrameStarts = runManyFor(di)(beforeFrameStartsInjectionToken); + const startFrame = di.inject(startFrameInjectable); - await beforeFrameStarts(); + await startFrame(); // TODO: Consolidate import time side-effect to setup time bindEvents(); diff --git a/src/renderer/components/+catalog/catalog.test.tsx b/src/renderer/components/+catalog/catalog.test.tsx index 1fd7e3f3f8..808226727c 100644 --- a/src/renderer/components/+catalog/catalog.test.tsx +++ b/src/renderer/components/+catalog/catalog.test.tsx @@ -26,8 +26,6 @@ import appVersionInjectable from "../../../common/get-configuration-file-model/a import type { AppEvent } from "../../../common/app-event-bus/event-bus"; import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable"; import { computed } from "mobx"; -import ipcRendererInjectable from "../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; -import { UserStore } from "../../../common/user-store"; import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable"; mockWindow(); @@ -107,13 +105,7 @@ describe("", () => { catalogEntityItem = createMockCatalogEntity(onRun); catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable); - UserStore.createInstance(); // TODO: replace with DI - di.override(catalogEntityRegistryInjectable, () => catalogEntityRegistry); - di.override(ipcRendererInjectable, () => ({ - on: jest.fn(), - invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge - } as never)); emitEvent = jest.fn(); @@ -129,7 +121,6 @@ describe("", () => { afterEach(() => { CatalogEntityDetailRegistry.resetInstance(); - UserStore.resetInstance(); jest.clearAllMocks(); jest.restoreAllMocks(); mockFs.restore(); diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 7ec77838fc..66f0830c3d 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -7,7 +7,6 @@ import "@testing-library/jest-dom/extend-expect"; import { fireEvent, screen, waitFor } from "@testing-library/react"; import fse from "fs-extra"; import React from "react"; -import { UserStore } from "../../../../common/user-store"; import type { ExtensionDiscovery } from "../../../../extensions/extension-discovery/extension-discovery"; import type { ExtensionLoader } from "../../../../extensions/extension-loader"; import { ConfirmDialog } from "../../confirm-dialog"; @@ -96,13 +95,10 @@ describe("Extensions", () => { }); extensionDiscovery.uninstallExtension = jest.fn(() => Promise.resolve()); - - UserStore.createInstance(); }); afterEach(() => { mockFs.restore(); - UserStore.resetInstance(); }); it("disables uninstall and disable buttons while uninstalling", async () => { diff --git a/src/renderer/components/+preferences/application.tsx b/src/renderer/components/+preferences/application.tsx index bbda49f1a0..d3c0fd4414 100644 --- a/src/renderer/components/+preferences/application.tsx +++ b/src/renderer/components/+preferences/application.tsx @@ -12,7 +12,7 @@ import type { UserStore } from "../../../common/user-store"; import { Input } from "../input"; import { Switch } from "../switch"; import moment from "moment-timezone"; -import { updateChannels, defaultExtensionRegistryUrl, defaultUpdateChannel, defaultLocaleTimezone, defaultExtensionRegistryUrlLocation } from "../../../common/user-store/preferences-helpers"; +import { defaultExtensionRegistryUrl, defaultLocaleTimezone, defaultExtensionRegistryUrlLocation } from "../../../common/user-store/preferences-helpers"; import type { IComputedValue } from "mobx"; import { runInAction } from "mobx"; import { isUrl } from "../input/input_validators"; @@ -24,11 +24,17 @@ import { Preferences } from "./preferences"; import userStoreInjectable from "../../../common/user-store/user-store.injectable"; import themeStoreInjectable from "../../themes/store.injectable"; import { defaultThemeId } from "../../../common/vars"; +import { updateChannels } from "../../../common/application-update/update-channels"; +import { map, toPairs } from "lodash/fp"; +import { pipeline } from "@ogre-tools/fp"; +import type { SelectedUpdateChannel } from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable"; +import selectedUpdateChannelInjectable from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable"; interface Dependencies { appPreferenceItems: IComputedValue; userStore: UserStore; themeStore: ThemeStore; + selectedUpdateChannel: SelectedUpdateChannel; } const timezoneOptions = moment.tz.names() @@ -36,10 +42,16 @@ const timezoneOptions = moment.tz.names() value: timezone, label: timezone.replace("_", " "), })); -const updateChannelOptions = Array.from(updateChannels, ([channel, { label }]) => ({ - value: channel, - label, -})); + +const updateChannelOptions = pipeline( + toPairs(updateChannels), + + map(([, channel]) => ({ + value: channel.id, + label: channel.label, + })), +); + const extensionInstallRegistryOptions = [ { value: "default", @@ -55,7 +67,7 @@ const extensionInstallRegistryOptions = [ }, ] as const; -const NonInjectedApplication: React.FC = ({ appPreferenceItems, userStore, themeStore }) => { +const NonInjectedApplication: React.FC = ({ appPreferenceItems, userStore, themeStore, selectedUpdateChannel }) => { const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || ""); const themeOptions = [ { @@ -144,8 +156,8 @@ const NonInjectedApplication: React.FC = ({ appPreferenceItems, us ", () => { di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(rendererExtensionsInjectable, () => computed(() => [] as LensRendererExtension[])); - di.override(ipcRendererInjectable, () => ({ - on: jest.fn(), - invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge - } as never)); - + di.permitSideEffects(getConfigurationFileModelInjectable); di.permitSideEffects(appVersionInjectable); }); diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx index 178874ed76..5c8162cd2d 100644 --- a/src/renderer/components/test-utils/get-application-builder.tsx +++ b/src/renderer/components/test-utils/get-application-builder.tsx @@ -18,14 +18,13 @@ import type { RenderResult } from "@testing-library/react"; import { fireEvent } from "@testing-library/react"; import type { KubeResource } from "../../../common/rbac"; import { Sidebar } from "../layout/sidebar"; -import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing"; import type { DiContainer } from "@ogre-tools/injectable"; import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable"; import type { ClusterStore } from "../../../common/cluster-store/cluster-store"; import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable"; import currentRouteComponentInjectable from "../../routes/current-route-component.injectable"; import { pipeline } from "@ogre-tools/fp"; -import { flatMap, compact, join, get, filter } from "lodash/fp"; +import { flatMap, compact, join, get, filter, find, map, matches } from "lodash/fp"; import preferenceNavigationItemsInjectable from "../+preferences/preferences-navigation/preference-navigation-items.injectable"; import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; import type { MenuItemOpts } from "../../../main/menu/application-menu-items.injectable"; @@ -44,6 +43,14 @@ import { flushPromises } from "../../../common/test-utils/flush-promises"; import type { NamespaceStore } from "../+namespaces/store"; import namespaceStoreInjectable from "../+namespaces/store.injectable"; import historyInjectable from "../../navigation/history.injectable"; +import type { TrayMenuItem } from "../../../main/tray/tray-menu-item/tray-menu-item-injection-token"; +import electronTrayInjectable from "../../../main/tray/electron-tray/electron-tray.injectable"; +import applicationWindowInjectable from "../../../main/start-main-application/lens-window/application-window/application-window.injectable"; +import { Notifications } from "../notifications/notifications"; +import broadcastThatRootFrameIsRenderedInjectable from "../../frames/root-frame/broadcast-that-root-frame-is-rendered.injectable"; +import { getDiForUnitTesting as getRendererDi } from "../../getDiForUnitTesting"; +import { getDiForUnitTesting as getMainDi } from "../../../main/getDiForUnitTesting"; +import { overrideChannels } from "../../../test-utils/channel-fakes/override-channels"; type Callback = (dis: DiContainers) => void | Promise; @@ -56,6 +63,11 @@ export interface ApplicationBuilder { beforeRender: (callback: Callback) => ApplicationBuilder; render: () => Promise; + tray: { + click: (id: string) => Promise; + get: (id: string) => TrayMenuItem | undefined; + }; + applicationMenu: { click: (path: string) => Promise; }; @@ -80,14 +92,23 @@ interface DiContainers { interface Environment { renderSidebar: () => React.ReactNode; + beforeRender: () => void; onAllowKubeResource: () => void; } export const getApplicationBuilder = () => { - const { rendererDi, mainDi } = getDisForUnitTesting({ + const mainDi = getMainDi({ doGeneralOverrides: true, }); + const overrideChannelsForWindow = overrideChannels(mainDi); + + const rendererDi = getRendererDi({ + doGeneralOverrides: true, + }); + + overrideChannelsForWindow(rendererDi); + const dis = { rendererDi, mainDi }; const clusterStoreStub = { @@ -110,6 +131,12 @@ export const getApplicationBuilder = () => { application: { renderSidebar: () => null, + beforeRender: () => { + const nofifyThatRootFrameIsRendered = rendererDi.inject(broadcastThatRootFrameIsRenderedInjectable); + + nofifyThatRootFrameIsRendered(); + }, + onAllowKubeResource: () => { throw new Error( "Tried to allow kube resource when environment is not cluster frame.", @@ -119,6 +146,7 @@ export const getApplicationBuilder = () => { clusterFrame: { renderSidebar: () => , + beforeRender: () => {}, onAllowKubeResource: () => {}, } as Environment, }; @@ -138,6 +166,17 @@ export const getApplicationBuilder = () => { computed(() => []), ); + let trayMenuItemsStateFake: TrayMenuItem[]; + + mainDi.override(electronTrayInjectable, () => ({ + start: () => {}, + stop: () => {}, + + setMenuItems: (items) => { + trayMenuItemsStateFake = items; + }, + })); + let allowedResourcesState: IObservableArray; let rendered: RenderResult; @@ -180,6 +219,32 @@ export const getApplicationBuilder = () => { }, }, + tray: { + get: (id: string) => { + return trayMenuItemsStateFake.find(matches({ id })); + }, + + click: async (id: string) => { + const menuItem = pipeline( + trayMenuItemsStateFake, + find((menuItem) => menuItem.id === id), + ); + + if (!menuItem) { + const availableIds = pipeline( + trayMenuItemsStateFake, + filter(item => !!item.click), + map(item => item.id), + join(", "), + ); + + throw new Error(`Tried to click tray menu item with ID ${id} which does not exist. Available IDs are: "${availableIds}"`); + } + + await menuItem.click?.(); + }, + }, + preferences: { close: () => { const link = rendered.getByTestId("close-preferences"); @@ -318,6 +383,10 @@ export const getApplicationBuilder = () => { await startMainApplication(); + const applicationWindow = mainDi.inject(applicationWindowInjectable); + + await applicationWindow.show(); + const startFrame = rendererDi.inject(startFrameInjectable); await startFrame(); @@ -330,6 +399,8 @@ export const getApplicationBuilder = () => { await callback(dis); } + environment.beforeRender(); + rendered = render( {environment.renderSidebar()} @@ -345,6 +416,8 @@ export const getApplicationBuilder = () => { return ; }} + + , ); diff --git a/src/renderer/components/update-button/__tests__/update-button.test.tsx b/src/renderer/components/update-button/__tests__/update-button.test.tsx index ff52035a48..8133bd3845 100644 --- a/src/renderer/components/update-button/__tests__/update-button.test.tsx +++ b/src/renderer/components/update-button/__tests__/update-button.test.tsx @@ -3,15 +3,25 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { render, act } from "@testing-library/react"; +import { act } from "@testing-library/react"; import React from "react"; import { UpdateButton } from "../update-button"; import "@testing-library/jest-dom/extend-expect"; +import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; +import type { DiRender } from "../../test-utils/renderFor"; +import { renderFor } from "../../test-utils/renderFor"; const update = jest.fn(); describe("", () => { + let render: DiRender; + beforeEach(() => { + + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + render = renderFor(di); + update.mockClear(); }); diff --git a/src/renderer/frames/root-frame/broadcast-that-root-frame-is-rendered.injectable.ts b/src/renderer/frames/root-frame/broadcast-that-root-frame-is-rendered.injectable.ts new file mode 100644 index 0000000000..e6493e2832 --- /dev/null +++ b/src/renderer/frames/root-frame/broadcast-that-root-frame-is-rendered.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; +import rootFrameIsRenderedChannelInjectable from "../../../common/root-frame-rendered-channel/root-frame-rendered-channel.injectable"; + +const broadcastThatRootFrameIsRenderedInjectable = getInjectable({ + id: "broadcast-that-root-frame-is-rendered", + + instantiate: (di) => { + const messageToChannel = di.inject(messageToChannelInjectionToken); + const rootFrameIsRenderedChannel = di.inject(rootFrameIsRenderedChannelInjectable); + + return () => { + messageToChannel(rootFrameIsRenderedChannel); + }; + }, +}); + +export default broadcastThatRootFrameIsRenderedInjectable; diff --git a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts index b05c557476..b77ed399fc 100644 --- a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts +++ b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import { initRootFrame } from "./init-root-frame"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; -import ipcRendererInjectable from "../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; +import ipcRendererInjectable from "../../../utils/channel/ipc-renderer.injectable"; import bindProtocolAddRouteHandlersInjectable from "../../../protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable"; import lensProtocolRouterRendererInjectable from "../../../protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable"; import catalogEntityRegistryInjectable from "../../../api/catalog/entity/registry.injectable"; diff --git a/src/renderer/frames/root-frame/root-frame.tsx b/src/renderer/frames/root-frame/root-frame.tsx index 41fdea10ac..6cb4d016c5 100644 --- a/src/renderer/frames/root-frame/root-frame.tsx +++ b/src/renderer/frames/root-frame/root-frame.tsx @@ -11,17 +11,22 @@ import { ErrorBoundary } from "../../components/error-boundary"; import { Notifications } from "../../components/notifications"; import { ConfirmDialog } from "../../components/confirm-dialog"; import { CommandContainer } from "../../components/command-palette/command-container"; -import { ipcRenderer } from "electron"; -import { IpcRendererNavigationEvents } from "../../navigation/events"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import broadcastThatRootFrameIsRenderedInjectable from "./broadcast-that-root-frame-is-rendered.injectable"; +// Todo: remove import-time side-effect. injectSystemCAs(); +interface Dependencies { + broadcastThatRootFrameIsRendered: () => void; +} + @observer -export class RootFrame extends React.Component { +class NonInjectedRootFrame extends React.Component { static displayName = "RootFrame"; componentDidMount() { - ipcRenderer.send(IpcRendererNavigationEvents.LOADED); + this.props.broadcastThatRootFrameIsRendered(); } render() { @@ -37,3 +42,14 @@ export class RootFrame extends React.Component { ); } } + +export const RootFrame = withInjectables( + NonInjectedRootFrame, + + { + getProps: (di, props) => ({ + broadcastThatRootFrameIsRendered: di.inject(broadcastThatRootFrameIsRenderedInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index 22ac104e88..3d89322cdf 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -7,12 +7,11 @@ import glob from "glob"; import { memoize, noop } from "lodash/fp"; import { createContainer } from "@ogre-tools/injectable"; import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; -import getValueFromRegisteredChannelInjectable from "./app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable"; +import requestFromChannelInjectable from "./utils/channel/request-from-channel.injectable"; import loggerInjectable from "../common/logger.injectable"; import { overrideFsWithFakes } from "../test-utils/override-fs-with-fakes"; import { createMemoryHistory } from "history"; -import registerIpcChannelListenerInjectable from "./app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable"; -import focusWindowInjectable from "./ipc-channel-listeners/focus-window.injectable"; +import focusWindowInjectable from "./navigation/focus-window.injectable"; import extensionsStoreInjectable from "../extensions/extensions-store/extensions-store.injectable"; import type { ExtensionsStore } from "../extensions/extensions-store/extensions-store"; import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable"; @@ -32,19 +31,22 @@ import { joinPathsFake } from "../common/test-utils/join-paths-fake"; import hotbarStoreInjectable from "../common/hotbars/store.injectable"; import terminalSpawningPoolInjectable from "./components/dock/terminal/terminal-spawning-pool.injectable"; import hostedClusterIdInjectable from "../common/cluster-store/hosted-cluster-id.injectable"; -import type { GetDiForUnitTestingOptions } from "../test-utils/get-dis-for-unit-testing"; import historyInjectable from "./navigation/history.injectable"; import { ApiManager } from "../common/k8s-api/api-manager"; import lensResourcesDirInjectable from "../common/vars/lens-resources-dir.injectable"; import broadcastMessageInjectable from "../common/ipc/broadcast-message.injectable"; import apiManagerInjectable from "../common/k8s-api/api-manager/manager.injectable"; -import ipcRendererInjectable - from "./app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; +import ipcRendererInjectable from "./utils/channel/ipc-renderer.injectable"; import type { IpcRenderer } from "electron"; import setupOnApiErrorListenersInjectable from "./api/setup-on-api-errors.injectable"; import { observable } from "mobx"; +import defaultShellInjectable from "./components/+preferences/default-shell.injectable"; +import appVersionInjectable from "../common/get-configuration-file-model/app-version/app-version.injectable"; +import provideInitialValuesForSyncBoxesInjectable from "./utils/sync-box/provide-initial-values-for-sync-boxes.injectable"; +import requestAnimationFrameInjectable from "./components/animate/request-animation-frame.injectable"; +import getRandomIdInjectable from "../common/utils/get-random-id.injectable"; -export const getDiForUnitTesting = (opts: GetDiForUnitTestingOptions = {}) => { +export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) => { const { doGeneralOverrides = false, } = opts; @@ -65,6 +67,7 @@ export const getDiForUnitTesting = (opts: GetDiForUnitTestingOptions = {}) => { di.preventSideEffects(); if (doGeneralOverrides) { + di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id"); di.override(isMacInjectable, () => true); di.override(isWindowsInjectable, () => false); di.override(isLinuxInjectable, () => false); @@ -75,8 +78,12 @@ export const getDiForUnitTesting = (opts: GetDiForUnitTestingOptions = {}) => { di.override(getAbsolutePathInjectable, () => getAbsolutePathFake); di.override(joinPathsInjectable, () => joinPathsFake); + di.override(appVersionInjectable, () => "1.0.0"); + di.override(historyInjectable, () => createMemoryHistory()); + di.override(requestAnimationFrameInjectable, () => (callback) => callback()); + di.override(lensResourcesDirInjectable, () => "/irrelevant"); di.override(ipcRendererInjectable, () => ({ @@ -99,6 +106,9 @@ export const getDiForUnitTesting = (opts: GetDiForUnitTestingOptions = {}) => { di.override(clusterStoreInjectable, () => ({ getById: (id): Cluster => ({}) as Cluster }) as ClusterStore); di.override(setupOnApiErrorListenersInjectable, () => ({ run: () => {} })); + di.override(provideInitialValuesForSyncBoxesInjectable, () => ({ run: () => {} })); + + di.override(defaultShellInjectable, () => "some-default-shell"); di.override( userStoreInjectable, @@ -114,8 +124,7 @@ export const getDiForUnitTesting = (opts: GetDiForUnitTestingOptions = {}) => { di.override(apiManagerInjectable, () => new ApiManager()); - di.override(getValueFromRegisteredChannelInjectable, () => () => Promise.resolve(undefined as never)); - di.override(registerIpcChannelListenerInjectable, () => () => undefined); + di.override(requestFromChannelInjectable, () => () => Promise.resolve(undefined as never)); overrideFsWithFakes(di); diff --git a/src/renderer/ipc-channel-listeners/ipc-channel-listener-injection-token.ts b/src/renderer/ipc-channel-listeners/ipc-channel-listener-injection-token.ts deleted file mode 100644 index 235b90873c..0000000000 --- a/src/renderer/ipc-channel-listeners/ipc-channel-listener-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 { Channel } from "../../common/ipc-channel/channel"; - - -export interface IpcChannelListener { - channel: Channel; - handle: (value: any) => void; -} - -export const ipcChannelListenerInjectionToken = - getInjectionToken({ - id: "ipc-channel-listener-injection-token", - }); diff --git a/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts b/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts deleted file mode 100644 index bf9568b71c..0000000000 --- a/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts +++ /dev/null @@ -1,28 +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 { ipcChannelListenerInjectionToken } from "./ipc-channel-listener-injection-token"; -import registerIpcChannelListenerInjectable from "../app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable"; -import { beforeFrameStartsInjectionToken } from "../before-frame-starts/before-frame-starts-injection-token"; - -const registerIpcChannelListenersInjectable = getInjectable({ - id: "register-ipc-channel-listeners", - - instantiate: di => ({ - run: async () => { - const registerIpcChannelListener = di.inject(registerIpcChannelListenerInjectable); - - const listeners = di.injectMany(ipcChannelListenerInjectionToken); - - listeners.forEach(listener => { - registerIpcChannelListener(listener); - }); - }, - }), - - injectionToken: beforeFrameStartsInjectionToken, -}); - -export default registerIpcChannelListenersInjectable; diff --git a/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx b/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx index c787899e84..22434df1b6 100644 --- a/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx +++ b/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx @@ -5,17 +5,19 @@ import { getInjectable } from "@ogre-tools/injectable"; import navigateToEntitySettingsInjectable from "../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; import type { ListNamespaceForbiddenArgs } from "../../common/ipc/cluster"; -import { Notifications, notificationsStore } from "../components/notifications"; +import { Notifications } from "../components/notifications"; import { ClusterStore } from "../../common/cluster-store/cluster-store"; import { Button } from "../components/button"; import type { IpcRendererEvent } from "electron"; import React from "react"; +import notificationsStoreInjectable from "../components/notifications/notifications-store.injectable"; const listNamespacesForbiddenHandlerInjectable = getInjectable({ id: "list-namespaces-forbidden-handler", instantiate: (di) => { const navigateToEntitySettings = di.inject(navigateToEntitySettingsInjectable); + const notificationsStore = di.inject(notificationsStoreInjectable); const notificationLastDisplayedAt = new Map(); const intervalBetweenNotifications = 1000 * 60; // 60s diff --git a/src/renderer/ipc/register-listeners.tsx b/src/renderer/ipc/register-listeners.tsx index a9e0e76d2d..4d2ed9f5c8 100644 --- a/src/renderer/ipc/register-listeners.tsx +++ b/src/renderer/ipc/register-listeners.tsx @@ -3,89 +3,14 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import React from "react"; import type { IpcRendererEvent } from "electron"; import { ipcRenderer } from "electron"; -import type { UpdateAvailableFromMain, BackchannelArg } from "../../common/ipc"; -import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, ipcRendererOn, AutoUpdateChecking, AutoUpdateNoUpdateAvailable } from "../../common/ipc"; -import { Notifications, notificationsStore } from "../components/notifications"; -import { Button } from "../components/button"; -import { isMac } from "../../common/vars"; +import { onCorrect } from "../../common/ipc"; +import { Notifications } from "../components/notifications"; import { defaultHotbarCells } from "../../common/hotbars/types"; import { type ListNamespaceForbiddenArgs, clusterListNamespaceForbiddenChannel, isListNamespaceForbiddenArgs } from "../../common/ipc/cluster"; import { hotbarTooManyItemsChannel } from "../../common/ipc/hotbar"; -function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void { - notificationsStore.remove(notificationId); - ipcRenderer.send(backchannel, data); -} - -function RenderYesButtons(props: { backchannel: string; notificationId: string }) { - if (isMac) { - /** - * auto-updater's "installOnQuit" is not applicable for macOS as per their docs. - * - * See: https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/src/AppUpdater.ts#L27-L32 - */ - return ( -
    -
    - ), - { - id: notificationId, - onClose() { - sendToBackchannel(backchannel, notificationId, { doUpdate: false }); - }, - }, - ); -} - function HotbarTooManyItemsHandler(): void { Notifications.error(`Cannot have more than ${defaultHotbarCells} items pinned to a hotbar`); } @@ -98,12 +23,6 @@ interface Dependencies { } export const registerIpcListeners = ({ listNamespacesForbiddenHandler }: Dependencies) => () => { - onCorrect({ - source: ipcRenderer, - channel: UpdateAvailableChannel, - listener: UpdateAvailableHandler, - verifier: areArgsUpdateAvailableFromMain, - }); onCorrect({ source: ipcRenderer, channel: clusterListNamespaceForbiddenChannel, @@ -115,11 +34,4 @@ export const registerIpcListeners = ({ listNamespacesForbiddenHandler }: Depende channel: hotbarTooManyItemsChannel, listener: HotbarTooManyItemsHandler, verifier: (args: unknown[]): args is [] => args.length === 0, - }); - ipcRendererOn(AutoUpdateChecking, () => { - Notifications.shortInfo("Checking for updates"); - }); - ipcRendererOn(AutoUpdateNoUpdateAvailable, () => { - Notifications.shortInfo("No update is currently available"); - }); -}; + });}; diff --git a/src/renderer/ipc-channel-listeners/focus-window.injectable.ts b/src/renderer/navigation/focus-window.injectable.ts similarity index 100% rename from src/renderer/ipc-channel-listeners/focus-window.injectable.ts rename to src/renderer/navigation/focus-window.injectable.ts diff --git a/src/renderer/ipc-channel-listeners/navigation-listener.injectable.ts b/src/renderer/navigation/navigation-channel-listener.injectable.ts similarity index 52% rename from src/renderer/ipc-channel-listeners/navigation-listener.injectable.ts rename to src/renderer/navigation/navigation-channel-listener.injectable.ts index 36a1d02559..3a17451071 100644 --- a/src/renderer/ipc-channel-listeners/navigation-listener.injectable.ts +++ b/src/renderer/navigation/navigation-channel-listener.injectable.ts @@ -3,26 +3,29 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { ipcChannelListenerInjectionToken } from "./ipc-channel-listener-injection-token"; -import { appNavigationIpcChannel, clusterFrameNavigationIpcChannel } from "../../common/front-end-routing/navigation-ipc-channel"; import currentlyInClusterFrameInjectable from "../routes/currently-in-cluster-frame.injectable"; -import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token"; +import appNavigationChannelInjectable from "../../common/front-end-routing/app-navigation-channel.injectable"; +import clusterFrameNavigationChannelInjectable from "../../common/front-end-routing/cluster-frame-navigation-channel.injectable"; import focusWindowInjectable from "./focus-window.injectable"; +import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token"; +import { messageChannelListenerInjectionToken } from "../../common/utils/channel/message-channel-listener-injection-token"; -const navigationListenerInjectable = getInjectable({ - id: "navigation-listener", +const navigationChannelListenerInjectable = getInjectable({ + id: "navigation-channel-listener", instantiate: (di) => { - const navigateToUrl = di.inject(navigateToUrlInjectionToken); const currentlyInClusterFrame = di.inject(currentlyInClusterFrameInjectable); + const appNavigationChannel = di.inject(appNavigationChannelInjectable); + const clusterFrameNavigationChannel = di.inject(clusterFrameNavigationChannelInjectable); const focusWindow = di.inject(focusWindowInjectable); + const navigateToUrl = di.inject(navigateToUrlInjectionToken); return { channel: currentlyInClusterFrame - ? clusterFrameNavigationIpcChannel - : appNavigationIpcChannel, + ? clusterFrameNavigationChannel + : appNavigationChannel, - handle: (url: string) => { + handler: (url: string) => { navigateToUrl(url); if (!currentlyInClusterFrame) { @@ -31,8 +34,7 @@ const navigationListenerInjectable = getInjectable({ }, }; }, - - injectionToken: ipcChannelListenerInjectionToken, + injectionToken: messageChannelListenerInjectionToken, }); -export default navigationListenerInjectable; +export default navigationChannelListenerInjectable; diff --git a/src/renderer/port-forward/about-port-forwarding.injectable.ts b/src/renderer/port-forward/about-port-forwarding.injectable.ts index 4656310964..29dba28962 100644 --- a/src/renderer/port-forward/about-port-forwarding.injectable.ts +++ b/src/renderer/port-forward/about-port-forwarding.injectable.ts @@ -7,18 +7,22 @@ import { aboutPortForwarding } from "./port-forward-notify"; import navigateToPortForwardsInjectable from "../../common/front-end-routing/routes/cluster/network/port-forwards/navigate-to-port-forwards.injectable"; import hostedClusterIdInjectable from "../../common/cluster-store/hosted-cluster-id.injectable"; import assert from "assert"; +import notificationsStoreInjectable from "../components/notifications/notifications-store.injectable"; const aboutPortForwardingInjectable = getInjectable({ id: "about-port-forwarding", instantiate: (di) => { const hostedClusterId = di.inject(hostedClusterIdInjectable); + const notificationsStore = di.inject(notificationsStoreInjectable); + const navigateToPortForwards = di.inject(navigateToPortForwardsInjectable); assert(hostedClusterId, "Only allowed to notify about port forward errors within a cluster frame"); return aboutPortForwarding({ - navigateToPortForwards: di.inject(navigateToPortForwardsInjectable), + navigateToPortForwards, hostedClusterId, + notificationsStore, }); }, }); diff --git a/src/renderer/port-forward/notify-error-port-forwarding.injectable.ts b/src/renderer/port-forward/notify-error-port-forwarding.injectable.ts index 9d4cd5caa7..a0f1ed714f 100644 --- a/src/renderer/port-forward/notify-error-port-forwarding.injectable.ts +++ b/src/renderer/port-forward/notify-error-port-forwarding.injectable.ts @@ -7,18 +7,22 @@ import { notifyErrorPortForwarding } from "./port-forward-notify"; import navigateToPortForwardsInjectable from "../../common/front-end-routing/routes/cluster/network/port-forwards/navigate-to-port-forwards.injectable"; import hostedClusterIdInjectable from "../../common/cluster-store/hosted-cluster-id.injectable"; import assert from "assert"; +import notificationsStoreInjectable from "../components/notifications/notifications-store.injectable"; const notifyErrorPortForwardingInjectable = getInjectable({ id: "notify-error-port-forwarding", instantiate: (di) => { const hostedClusterId = di.inject(hostedClusterIdInjectable); + const notificationsStore = di.inject(notificationsStoreInjectable); + const navigateToPortForwards = di.inject(navigateToPortForwardsInjectable); assert(hostedClusterId, "Only allowed to notify about port forward errors within a cluster frame"); return notifyErrorPortForwarding({ - navigateToPortForwards: di.inject(navigateToPortForwardsInjectable), + navigateToPortForwards, hostedClusterId, + notificationsStore, }); }, }); diff --git a/src/renderer/port-forward/port-forward-notify.tsx b/src/renderer/port-forward/port-forward-notify.tsx index 086771ad46..40dc295602 100644 --- a/src/renderer/port-forward/port-forward-notify.tsx +++ b/src/renderer/port-forward/port-forward-notify.tsx @@ -5,17 +5,20 @@ import React from "react"; import { Button } from "../components/button"; -import { Notifications, notificationsStore } from "../components/notifications"; +import type { NotificationsStore } from "../components/notifications"; +import { Notifications } from "../components/notifications"; import type { NavigateToPortForwards } from "../../common/front-end-routing/routes/cluster/network/port-forwards/navigate-to-port-forwards.injectable"; interface AboutPortForwardingDependencies { navigateToPortForwards: NavigateToPortForwards; hostedClusterId: string; + notificationsStore: NotificationsStore; } export const aboutPortForwarding = ({ navigateToPortForwards, hostedClusterId, + notificationsStore, }: AboutPortForwardingDependencies) => () => { const notificationId = `port-forward-notification-${hostedClusterId}`; @@ -49,12 +52,14 @@ export const aboutPortForwarding = ({ interface NotifyErrorPortForwardingDependencies { navigateToPortForwards: NavigateToPortForwards; hostedClusterId: string; + notificationsStore: NotificationsStore; } export const notifyErrorPortForwarding = ({ navigateToPortForwards, hostedClusterId, + notificationsStore, }: NotifyErrorPortForwardingDependencies) => (msg: string) => { const notificationId = `port-forward-error-notification-${hostedClusterId}`; diff --git a/src/renderer/themes/store.injectable.ts b/src/renderer/themes/store.injectable.ts index d959925caa..a67075584e 100644 --- a/src/renderer/themes/store.injectable.ts +++ b/src/renderer/themes/store.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable"; -import ipcRendererInjectable from "../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; +import ipcRendererInjectable from "../utils/channel/ipc-renderer.injectable"; import { ThemeStore } from "./store"; const themeStoreInjectable = getInjectable({ diff --git a/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts b/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts new file mode 100644 index 0000000000..6d76e35340 --- /dev/null +++ b/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts @@ -0,0 +1,38 @@ +/** + * 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 { getInjectable } from "@ogre-tools/injectable"; +import type { IpcRendererEvent } from "electron"; +import { enlistMessageChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token"; +import { tentativeParseJson } from "../../../../common/utils/tentative-parse-json"; +import { pipeline } from "@ogre-tools/fp"; + +const enlistMessageChannelListenerInjectable = getInjectable({ + id: "enlist-message-channel-listener-for-renderer", + + instantiate: (di) => { + const ipcRenderer = di.inject(ipcRendererInjectable); + + return ({ channel, handler }) => { + const nativeCallback = (_: IpcRendererEvent, message: unknown) => { + pipeline( + message, + tentativeParseJson, + handler, + ); + }; + + ipcRenderer.on(channel.id, nativeCallback); + + return () => { + ipcRenderer.off(channel.id, nativeCallback); + }; + }; + }, + + injectionToken: enlistMessageChannelListenerInjectionToken, +}); + +export default enlistMessageChannelListenerInjectable; diff --git a/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts b/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts new file mode 100644 index 0000000000..4653dcdd5d --- /dev/null +++ b/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts @@ -0,0 +1,97 @@ +/** + * 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"; + +describe("enlist message channel listener in renderer", () => { + let enlistMessageChannelListener: EnlistMessageChannelListener; + let ipcRendererStub: IpcRenderer; + let onMock: jest.Mock; + let offMock: jest.Mock; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + onMock = jest.fn(); + offMock = jest.fn(); + + ipcRendererStub = { + on: onMock, + off: offMock, + } as unknown as IpcRenderer; + + di.override(ipcRendererInjectable, () => ipcRendererStub); + + enlistMessageChannelListener = di.inject( + enlistMessageChannelListenerInjectionToken, + ); + }); + + describe("when called", () => { + let handlerMock: jest.Mock; + let disposer: () => void; + + beforeEach(() => { + handlerMock = jest.fn(); + + disposer = enlistMessageChannelListener({ + channel: { id: "some-channel-id" }, + handler: handlerMock, + }); + }); + + it("does not call handler yet", () => { + expect(handlerMock).not.toHaveBeenCalled(); + }); + + it("registers the listener", () => { + expect(onMock).toHaveBeenCalledWith( + "some-channel-id", + expect.any(Function), + ); + }); + + it("does not de-register the listener yet", () => { + expect(offMock).not.toHaveBeenCalled(); + }); + + describe("when message arrives", () => { + beforeEach(() => { + onMock.mock.calls[0][1]({} as IpcRendererEvent, "some-message"); + }); + + it("calls the handler with the message", () => { + expect(handlerMock).toHaveBeenCalledWith("some-message"); + }); + + it("when disposing the listener, de-registers the listener", () => { + disposer(); + + expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function)); + }); + }); + + it("given number as message, when message arrives, calls the handler with the message", () => { + onMock.mock.calls[0][1]({} as IpcRendererEvent, 42); + + expect(handlerMock).toHaveBeenCalledWith(42); + }); + + it("given boolean as message, when message arrives, calls the handler with the message", () => { + onMock.mock.calls[0][1]({} as IpcRendererEvent, true); + + expect(handlerMock).toHaveBeenCalledWith(true); + }); + + it("given stringified object as message, when message arrives, calls the handler with the message", () => { + onMock.mock.calls[0][1]({} as IpcRendererEvent, JSON.stringify({ some: "object" })); + + expect(handlerMock).toHaveBeenCalledWith({ some: "object" }); + }); + }); +}); diff --git a/src/renderer/utils/channel/channel-listeners/enlist-request-channel-listener.injectable.ts b/src/renderer/utils/channel/channel-listeners/enlist-request-channel-listener.injectable.ts new file mode 100644 index 0000000000..03253a06f2 --- /dev/null +++ b/src/renderer/utils/channel/channel-listeners/enlist-request-channel-listener.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 { getInjectable } from "@ogre-tools/injectable"; +import { enlistRequestChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-request-channel-listener-injection-token"; + +const enlistRequestChannelListenerInjectable = getInjectable({ + id: "enlist-request-channel-listener-for-renderer", + + instantiate: () => { + // Requests from main to renderer are not implemented yet. + return () => () => {}; + }, + + injectionToken: enlistRequestChannelListenerInjectionToken, +}); + +export default enlistRequestChannelListenerInjectable; diff --git a/src/renderer/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts b/src/renderer/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts new file mode 100644 index 0000000000..c37c9b1864 --- /dev/null +++ b/src/renderer/utils/channel/channel-listeners/start-listening-of-channels.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { beforeFrameStartsInjectionToken } from "../../../before-frame-starts/before-frame-starts-injection-token"; +import listeningOfChannelsInjectable from "../../../../common/utils/channel/listening-of-channels.injectable"; + +const startListeningOfChannelsInjectable = getInjectable({ + id: "start-listening-of-channels-renderer", + + instantiate: (di) => { + const listeningOfChannels = di.inject(listeningOfChannelsInjectable); + + return { + run: async () => { + await listeningOfChannels.start(); + }, + }; + }, + + injectionToken: beforeFrameStartsInjectionToken, +}); + +export default startListeningOfChannelsInjectable; diff --git a/src/renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable.ts b/src/renderer/utils/channel/ipc-renderer.injectable.ts similarity index 100% rename from src/renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable.ts rename to src/renderer/utils/channel/ipc-renderer.injectable.ts diff --git a/src/renderer/utils/channel/message-to-channel.injectable.ts b/src/renderer/utils/channel/message-to-channel.injectable.ts new file mode 100644 index 0000000000..3e493fd322 --- /dev/null +++ b/src/renderer/utils/channel/message-to-channel.injectable.ts @@ -0,0 +1,26 @@ +/** + * 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 { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; +import sendToMainInjectable from "./send-to-main.injectable"; +import type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token"; + +const messageToChannelInjectable = getInjectable({ + id: "message-to-channel", + + instantiate: (di) => { + const sendToMain = di.inject(sendToMainInjectable); + + // TODO: Figure out way to improve typing in internals + // Notice that this should be injected using "messageToChannelInjectionToken" which is typed correctly. + return (channel: MessageChannel, message?: unknown) => { + sendToMain(channel.id, message); + }; + }, + + injectionToken: messageToChannelInjectionToken, +}); + +export default messageToChannelInjectable; diff --git a/src/renderer/utils/channel/message-to-channel.test.ts b/src/renderer/utils/channel/message-to-channel.test.ts new file mode 100644 index 0000000000..443abfb0dc --- /dev/null +++ b/src/renderer/utils/channel/message-to-channel.test.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 { MessageToChannel } from "../../../common/utils/channel/message-to-channel-injection-token"; +import type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; +import ipcRendererInjectable from "./ipc-renderer.injectable"; +import type { IpcRenderer } from "electron"; + +describe("message to channel from renderer", () => { + let messageToChannel: MessageToChannel; + let sendMock: jest.Mock; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + sendMock = jest.fn(); + + di.override(ipcRendererInjectable, () => ({ + send: sendMock, + }) as unknown as IpcRenderer); + + messageToChannel = di.inject(messageToChannelInjectionToken); + }); + + it("given string as message, when messaging to channel, sends stringified message", () => { + messageToChannel(someChannel, "some-message"); + + expect(sendMock).toHaveBeenCalledWith("some-channel-id", '"some-message"'); + }); + + it("given boolean as message, when messaging to channel, sends stringified message", () => { + messageToChannel(someChannel, true); + + expect(sendMock).toHaveBeenCalledWith("some-channel-id", "true"); + }); + + it("given number as message, when messaging to channel, sends stringified message", () => { + messageToChannel(someChannel, 42); + + expect(sendMock).toHaveBeenCalledWith("some-channel-id", "42"); + }); + + it("given object as message, when messaging to channel, sends stringified message", () => { + messageToChannel(someChannel, { some: "object" }); + + expect(sendMock).toHaveBeenCalledWith( + "some-channel-id", + JSON.stringify({ some: "object" }), + ); + }); +}); + +const someChannel: MessageChannel = { id: "some-channel-id" }; diff --git a/src/renderer/utils/channel/request-from-channel.injectable.ts b/src/renderer/utils/channel/request-from-channel.injectable.ts new file mode 100644 index 0000000000..f77287a2ed --- /dev/null +++ b/src/renderer/utils/channel/request-from-channel.injectable.ts @@ -0,0 +1,30 @@ +/** + * 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 { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token"; +import { pipeline } from "@ogre-tools/fp"; +import { tentativeStringifyJson } from "../../../common/utils/tentative-stringify-json"; +import { tentativeParseJson } from "../../../common/utils/tentative-parse-json"; + +const requestFromChannelInjectable = getInjectable({ + id: "request-from-channel", + + instantiate: (di) => { + const ipcRenderer = di.inject(ipcRendererInjectable); + + return async (channel, ...[request]) => + await pipeline( + request, + tentativeStringifyJson, + (req) => ipcRenderer.invoke(channel.id, req), + tentativeParseJson, + ); + }, + + injectionToken: requestFromChannelInjectionToken, +}); + +export default requestFromChannelInjectable; diff --git a/src/renderer/utils/channel/request-from-channel.test.ts b/src/renderer/utils/channel/request-from-channel.test.ts new file mode 100644 index 0000000000..d7b343bf02 --- /dev/null +++ b/src/renderer/utils/channel/request-from-channel.test.ts @@ -0,0 +1,121 @@ +/** + * 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-injection-token"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import ipcRendererInjectable from "./ipc-renderer.injectable"; +import type { IpcRenderer } from "electron"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { RequestFromChannel } from "../../../common/utils/channel/request-from-channel-injection-token"; +import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token"; +import requestFromChannelInjectable from "./request-from-channel.injectable"; +import { getPromiseStatus } from "../../../common/test-utils/get-promise-status"; + +describe("request from channel in renderer", () => { + let requestFromChannel: RequestFromChannel; + let invokeMock: AsyncFnMock<(channelId: string, request: any) => any>; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + di.unoverride(requestFromChannelInjectable); + + invokeMock = asyncFn(); + + di.override(ipcRendererInjectable, () => ({ + invoke: invokeMock, + }) as unknown as IpcRenderer); + + requestFromChannel = di.inject(requestFromChannelInjectionToken); + }); + + describe("when messaging to channel", () => { + let actualPromise: Promise; + + beforeEach(() => { + actualPromise = requestFromChannel(someChannel, "some-message"); + }); + + it("sends stringified message", () => { + expect(invokeMock).toHaveBeenCalledWith("some-channel-id", '"some-message"'); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("when invoking resolves, resolves", async () => { + await invokeMock.resolve("some-response"); + + const actual = await actualPromise; + + expect(actual).toBe("some-response"); + }); + + it("when invoking resolves with stringified string, resolves with string", async () => { + await invokeMock.resolve('"some-response"'); + + const actual = await actualPromise; + + expect(actual).toBe("some-response"); + }); + + it("when invoking resolves with stringified boolean, resolves with boolean", async () => { + await invokeMock.resolve("true"); + + const actual = await actualPromise; + + expect(actual).toBe(true); + }); + + it("when invoking resolves with stringified number, resolves with number", async () => { + await invokeMock.resolve("42"); + + const actual = await actualPromise; + + expect(actual).toBe(42); + }); + + it("when invoking resolves with stringified object, resolves with object", async () => { + await invokeMock.resolve(JSON.stringify({ some: "object" })); + + const actual = await actualPromise; + + expect(actual).toEqual({ some: "object" }); + }); + }); + + it("given string as message, when messaging to channel, sends stringified message", () => { + requestFromChannel(someChannel, "some-message"); + + expect(invokeMock).toHaveBeenCalledWith("some-channel-id", '"some-message"'); + }); + + it("given boolean as message, when messaging to channel, sends stringified message", () => { + requestFromChannel(someChannel, true); + + expect(invokeMock).toHaveBeenCalledWith("some-channel-id", "true"); + }); + + it("given number as message, when messaging to channel, sends stringified message", () => { + requestFromChannel(someChannel, 42); + + expect(invokeMock).toHaveBeenCalledWith("some-channel-id", "42"); + }); + + it("given object as message, when messaging to channel, sends stringified message", () => { + requestFromChannel(someChannel, { some: "object" }); + + expect(invokeMock).toHaveBeenCalledWith( + "some-channel-id", + JSON.stringify({ some: "object" }), + ); + }); +}); + +const someChannel: MessageChannel = { id: "some-channel-id" }; diff --git a/src/renderer/utils/channel/send-to-main.injectable.ts b/src/renderer/utils/channel/send-to-main.injectable.ts new file mode 100644 index 0000000000..0811a78798 --- /dev/null +++ b/src/renderer/utils/channel/send-to-main.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import type { JsonValue } from "type-fest"; +import ipcRendererInjectable from "./ipc-renderer.injectable"; +import { tentativeStringifyJson } from "../../../common/utils/tentative-stringify-json"; + +const sendToMainInjectable = getInjectable({ + id: "send-to-main", + + instantiate: (di) => { + const ipcRenderer = di.inject(ipcRendererInjectable); + + // TODO: Figure out way to improve typing in internals + return (channelId: string, message: JsonValue extends T ? T : never ) => { + const stringifiedMessage = tentativeStringifyJson(message); + + ipcRenderer.send(channelId, ...(stringifiedMessage ? [stringifiedMessage] : [])); + }; + }, +}); + +export default sendToMainInjectable; diff --git a/src/renderer/utils/sync-box/provide-initial-values-for-sync-boxes.injectable.ts b/src/renderer/utils/sync-box/provide-initial-values-for-sync-boxes.injectable.ts new file mode 100644 index 0000000000..39d63e5d46 --- /dev/null +++ b/src/renderer/utils/sync-box/provide-initial-values-for-sync-boxes.injectable.ts @@ -0,0 +1,33 @@ +/** + * 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 { beforeFrameStartsInjectionToken } from "../../before-frame-starts/before-frame-starts-injection-token"; +import syncBoxInitialValueChannelInjectable from "../../../common/utils/sync-box/sync-box-initial-value-channel.injectable"; +import syncBoxStateInjectable from "../../../common/utils/sync-box/sync-box-state.injectable"; +import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token"; + +const provideInitialValuesForSyncBoxesInjectable = getInjectable({ + id: "provide-initial-values-for-sync-boxes", + + instantiate: (di) => { + const requestFromChannel = di.inject(requestFromChannelInjectionToken); + const syncBoxInitialValueChannel = di.inject(syncBoxInitialValueChannelInjectable); + const setSyncBoxState = (id: string, state: any) => di.inject(syncBoxStateInjectable, id).set(state); + + return { + run: async () => { + const initialValues = await requestFromChannel(syncBoxInitialValueChannel); + + initialValues.forEach(({ id, value }) => { + setSyncBoxState(id, value); + }); + }, + }; + }, + + injectionToken: beforeFrameStartsInjectionToken, +}); + +export default provideInitialValuesForSyncBoxesInjectable; diff --git a/src/test-utils/channel-fakes/override-channels.ts b/src/test-utils/channel-fakes/override-channels.ts new file mode 100644 index 0000000000..4d7a337e04 --- /dev/null +++ b/src/test-utils/channel-fakes/override-channels.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 type { DiContainer } from "@ogre-tools/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 const overrideChannels = (mainDi: DiContainer) => { + const overrideMessagingFromMainToWindowForWindow = overrideMessagingFromMainToWindow(mainDi); + const overrideMessagingFromWindowToForWindow = overrideMessagingFromWindowToMain(mainDi); + const overrideRequestingFromWindowToMainForWindow = overrideRequestingFromWindowToMain(mainDi); + + return (windowDi: DiContainer) => { + overrideMessagingFromMainToWindowForWindow(windowDi); + overrideMessagingFromWindowToForWindow(windowDi); + overrideRequestingFromWindowToMainForWindow(windowDi); + }; +}; diff --git a/src/test-utils/channel-fakes/override-messaging-from-main-to-window.ts b/src/test-utils/channel-fakes/override-messaging-from-main-to-window.ts new file mode 100644 index 0000000000..6ade9b7f0d --- /dev/null +++ b/src/test-utils/channel-fakes/override-messaging-from-main-to-window.ts @@ -0,0 +1,91 @@ +/** + * 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 type { MessageChannel } from "../../common/utils/channel/message-channel-injection-token"; +import sendToChannelInElectronBrowserWindowInjectable from "../../main/start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable"; +import type { SendToViewArgs } from "../../main/start-main-application/lens-window/application-window/lens-window-injection-token"; +import enlistMessageChannelListenerInjectableInRenderer from "../../renderer/utils/channel/channel-listeners/enlist-message-channel-listener.injectable"; +import type { DiContainer } from "@ogre-tools/injectable"; +import assert from "assert"; +import { tentativeParseJson } from "../../common/utils/tentative-parse-json"; + +export const overrideMessagingFromMainToWindow = (mainDi: DiContainer) => { + const messageChannelListenerFakesForRenderer = new Map< + string, + Set>> + >(); + + mainDi.override( + sendToChannelInElectronBrowserWindowInjectable, + + () => + ( + browserWindow, + { channel: channelId, frameInfo, data = [] }: SendToViewArgs, + ) => { + const listeners = + messageChannelListenerFakesForRenderer.get(channelId) || new Set(); + + if (frameInfo) { + throw new Error( + `Tried to send message to frame "${frameInfo.frameId}" in process "${frameInfo.processId}" using channel "${channelId}" which isn't supported yet.`, + ); + } + + if (data.length > 1) { + throw new Error( + `Tried to send message to channel "${channelId}" with more than one argument which is not supported in MessageChannelListener yet.`, + ); + } + + if (listeners.size === 0) { + throw new Error( + `Tried to send message to channel "${channelId}" but there where no listeners. Current channels with listeners: "${[ + ...messageChannelListenerFakesForRenderer.keys(), + ].join('", "')}"`, + ); + } + + const message = tentativeParseJson(data[0]); + + listeners.forEach((listener) => listener.handler(message)); + }, + ); + + return (windowDi: DiContainer) => { + windowDi.override( + enlistMessageChannelListenerInjectableInRenderer, + + () => (listener) => { + if (!messageChannelListenerFakesForRenderer.has(listener.channel.id)) { + messageChannelListenerFakesForRenderer.set( + listener.channel.id, + new Set(), + ); + } + + const listeners = messageChannelListenerFakesForRenderer.get( + listener.channel.id, + ); + + assert(listeners); + + // TODO: Figure out typing + listeners.add( + listener as unknown as MessageChannelListener>, + ); + + return () => { + // TODO: Figure out typing + listeners.delete( + listener as unknown as MessageChannelListener< + MessageChannel + >, + ); + }; + }, + ); + }; +}; diff --git a/src/test-utils/channel-fakes/override-messaging-from-window-to-main.ts b/src/test-utils/channel-fakes/override-messaging-from-window-to-main.ts new file mode 100644 index 0000000000..ba6813235b --- /dev/null +++ b/src/test-utils/channel-fakes/override-messaging-from-window-to-main.ts @@ -0,0 +1,64 @@ +/** + * 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 assert from "assert"; +import type { MessageChannel } from "../../common/utils/channel/message-channel-injection-token"; +import type { MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token"; +import enlistMessageChannelListenerInjectableInMain from "../../main/utils/channel/channel-listeners/enlist-message-channel-listener.injectable"; +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 channelId = listener.channel.id; + + if (!messageChannelListenerFakesForMain.has(channelId)) { + messageChannelListenerFakesForMain.set(channelId, new Set()); + } + + const listeners = messageChannelListenerFakesForMain.get( + channelId, + ); + + assert(listeners); + + // TODO: Figure out typing + listeners.add( + listener as unknown as MessageChannelListener>, + ); + + return () => { + // TODO: Figure out typing + listeners.delete( + listener as unknown as MessageChannelListener>, + ); + }; + }, + ); + + return (windowDi: DiContainer) => { + windowDi.override(sendToMainInjectable, () => (channelId, message) => { + const listeners = + messageChannelListenerFakesForMain.get(channelId) || new Set(); + + if (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('", "')}"`, + ); + } + + listeners.forEach((listener) => listener.handler(message)); + }); + }; +}; diff --git a/src/test-utils/channel-fakes/override-requesting-from-window-to-main.ts b/src/test-utils/channel-fakes/override-requesting-from-window-to-main.ts new file mode 100644 index 0000000000..8ee4227289 --- /dev/null +++ b/src/test-utils/channel-fakes/override-requesting-from-window-to-main.ts @@ -0,0 +1,59 @@ +/** + * 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 { RequestChannel } from "../../common/utils/channel/request-channel-injection-token"; +import type { RequestChannelListener } from "../../common/utils/channel/request-channel-listener-injection-token"; +import enlistRequestChannelListenerInjectableInMain from "../../main/utils/channel/channel-listeners/enlist-request-channel-listener.injectable"; +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.get(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, + + // TODO: Figure out typing + listener as unknown as RequestChannelListener< + RequestChannel + >, + ); + + 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`, + ); + } + + return requestListener.handler(request); + }, + ); + }; +}; diff --git a/src/test-utils/get-dis-for-unit-testing.ts b/src/test-utils/get-dis-for-unit-testing.ts deleted file mode 100644 index 2f7d59d036..0000000000 --- a/src/test-utils/get-dis-for-unit-testing.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 { getDiForUnitTesting as getRendererDi } from "../renderer/getDiForUnitTesting"; -import { getDiForUnitTesting as getMainDi } from "../main/getDiForUnitTesting"; -import { overrideIpcBridge } from "./override-ipc-bridge"; - -export interface GetDiForUnitTestingOptions { - doGeneralOverrides?: boolean; -} - -export const getDisForUnitTesting = (opts?: GetDiForUnitTestingOptions) => { - const rendererDi = getRendererDi(opts); - const mainDi = getMainDi(opts); - - overrideIpcBridge({ rendererDi, mainDi }); - - return { - rendererDi, - mainDi, - }; -}; diff --git a/src/test-utils/override-ipc-bridge.ts b/src/test-utils/override-ipc-bridge.ts deleted file mode 100644 index a914cc66b6..0000000000 --- a/src/test-utils/override-ipc-bridge.ts +++ /dev/null @@ -1,104 +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 { Channel } from "../common/ipc-channel/channel"; -import getValueFromRegisteredChannelInjectable from "../renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable"; -import registerChannelInjectable from "../main/app-paths/register-channel/register-channel.injectable"; -import asyncFn from "@async-fn/jest"; -import registerIpcChannelListenerInjectable from "../renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable"; -import type { SendToViewArgs } from "../main/start-main-application/lens-window/application-window/lens-window-injection-token"; -import sendToChannelInElectronBrowserWindowInjectable from "../main/start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable"; -import { isEmpty } from "lodash/fp"; - - -export const overrideIpcBridge = ({ - rendererDi, - mainDi, -}: { - rendererDi: DiContainer; - mainDi: DiContainer; -}) => { - const fakeChannelMap = new Map< - Channel, - { promise: Promise; resolve: (arg0: any) => Promise } - >(); - - const mainIpcRegistrations = { - set: , TInstance>( - key: TChannel, - callback: () => TChannel["_template"], - ) => { - if (!fakeChannelMap.has(key)) { - const mockInstance = asyncFn(); - - fakeChannelMap.set(key, { - promise: mockInstance(), - resolve: mockInstance.resolve, - }); - } - - return fakeChannelMap.get(key)?.resolve(callback); - }, - - get: , TInstance>(key: TChannel) => { - if (!fakeChannelMap.has(key)) { - const mockInstance = asyncFn(); - - fakeChannelMap.set(key, { - promise: mockInstance(), - resolve: mockInstance.resolve, - }); - } - - return fakeChannelMap.get(key)?.promise; - }, - }; - - rendererDi.override( - getValueFromRegisteredChannelInjectable, - () => async (channel) => { - const callback = await mainIpcRegistrations.get(channel); - - return callback(); - }, - ); - - mainDi.override(registerChannelInjectable, () => (channel, callback) => { - mainIpcRegistrations.set(channel, callback); - }); - - const rendererIpcFakeHandles = new Map< - string, - ((...args: any[]) => void)[] - >(); - - rendererDi.override( - registerIpcChannelListenerInjectable, - () => - ({ channel, handle }) => { - const existingHandles = rendererIpcFakeHandles.get(channel.name) || []; - - rendererIpcFakeHandles.set(channel.name, [...existingHandles, handle]); - }, - ); - - mainDi.override( - sendToChannelInElectronBrowserWindowInjectable, - () => - (browserWindow, { channel: channelName, data = [] }: SendToViewArgs) => { - const handles = rendererIpcFakeHandles.get(channelName) || []; - - if (isEmpty(handles)) { - throw new Error( - `Tried to send message to channel "${channelName}" but there where no listeners. Current channels with listeners: "${[ - ...rendererIpcFakeHandles.keys(), - ].join('", "')}"`, - ); - } - - handles.forEach((handle) => handle(...data)); - }, - ); -}; diff --git a/yarn.lock b/yarn.lock index 6884aee6b0..3974e6dfb2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,10 +26,10 @@ static-eval "2.0.2" underscore "1.7.0" -"@async-fn/jest@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@async-fn/jest/-/jest-1.6.0.tgz#48980e6f07c4d0d72b468b8b57a1b3be8473a746" - integrity sha512-Jm4kf9qQSzcOZIyiI13C4EM4euSLORA8O4JTOWwy7SwaUr8lhVOn0nVbNLx9jnP35JTYeLsLZHfAyZLhYDIl2g== +"@async-fn/jest@1.6.1": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@async-fn/jest/-/jest-1.6.1.tgz#ca298832fa1e7fb650ea2abd1a466acbbcf2cd58" + integrity sha512-UZoKtoccMr2VZjNOeRo6JJRg4Cb2O8EQH3bxIg3jl20Ya1KGIFs5kW3bf/0GRLY9XwL1mKlr1VHtp2vXlvcqtg== "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.8.3": version "7.16.0" From fba5892c8a438bbc53090d4f5bc915dce7101c2c Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 3 Jun 2022 08:55:11 -0400 Subject: [PATCH 43/43] Fix Tooltip not showing when switching hotbars (#5519) --- .../__snapshots__/tooltip.test.tsx.snap | 50 +++++++++++++++ .../components/tooltip/tooltip.test.tsx | 63 +++++++++++++++++++ src/renderer/components/tooltip/tooltip.tsx | 9 +-- 3 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 src/renderer/components/tooltip/__snapshots__/tooltip.test.tsx.snap create mode 100644 src/renderer/components/tooltip/tooltip.test.tsx diff --git a/src/renderer/components/tooltip/__snapshots__/tooltip.test.tsx.snap b/src/renderer/components/tooltip/__snapshots__/tooltip.test.tsx.snap new file mode 100644 index 0000000000..253a79f941 --- /dev/null +++ b/src/renderer/components/tooltip/__snapshots__/tooltip.test.tsx.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` does not render to DOM if not visibile 1`] = ` + +
    +
    + Target Text +
    +
    + +`; + +exports[` renders to DOM when forced to by visibile prop 1`] = ` + +
    + +
    + Target Text +
    +
    + +`; + +exports[` renders to DOM when hovering over target 1`] = ` + +
    + +
    + Target Text +
    +
    + +`; diff --git a/src/renderer/components/tooltip/tooltip.test.tsx b/src/renderer/components/tooltip/tooltip.test.tsx new file mode 100644 index 0000000000..0f51e5b2f1 --- /dev/null +++ b/src/renderer/components/tooltip/tooltip.test.tsx @@ -0,0 +1,63 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import assert from "assert"; +import React from "react"; +import { Tooltip } from "./tooltip"; + +describe("", () => { + it("does not render to DOM if not visibile", () => { + const result = render(( + <> + I am a tooltip +
    Target Text
    + + )); + + expect(result.baseElement).toMatchSnapshot(); + }); + + it("renders to DOM when hovering over target", () => { + const result = render(( + <> + + I am a tooltip + +
    Target Text
    + + )); + + const target = result.baseElement.querySelector("#my-target"); + + assert(target); + + userEvent.hover(target); + expect(result.baseElement).toMatchSnapshot(); + }); + + it("renders to DOM when forced to by visibile prop", () => { + const result = render(( + <> + + I am a tooltip + +
    Target Text
    + + )); + + expect(result.baseElement).toMatchSnapshot(); + }); +}); diff --git a/src/renderer/components/tooltip/tooltip.tsx b/src/renderer/components/tooltip/tooltip.tsx index b993a0a368..e13e8d3680 100644 --- a/src/renderer/components/tooltip/tooltip.tsx +++ b/src/renderer/components/tooltip/tooltip.tsx @@ -55,7 +55,7 @@ export class Tooltip extends React.Component { @observable.ref elem: HTMLDivElement | null = null; @observable activePosition?: TooltipPosition; - @observable isVisible = this.props.visible ?? false; + @observable isVisible = false; @observable isContentVisible = false; // animation manager constructor(props: TooltipProps) { @@ -217,13 +217,14 @@ export class Tooltip extends React.Component { } render() { - if (!this.isVisible) { + const { style, formatters, usePortal, children, visible = this.isVisible } = this.props; + + if (!visible) { return null; } - const { style, formatters, usePortal, children } = this.props; const className = cssNames("Tooltip", this.props.className, formatters, this.activePosition, { - visible: this.isContentVisible, + visible: this.isContentVisible || this.props.visible, formatter: !!formatters, }); const tooltip = (