diff --git a/.adr.json b/.adr.json new file mode 100644 index 0000000000..39e4f9f139 --- /dev/null +++ b/.adr.json @@ -0,0 +1,6 @@ +{ + "language": "en", + "path": "docs/architecture/decisions/", + "prefix": "", + "digits": 4 +} diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml index 9c942690b0..7418a86269 100644 --- a/.github/workflows/check-docs.yml +++ b/.github/workflows/check-docs.yml @@ -23,8 +23,8 @@ jobs: - name: Generate Extensions API Reference using typedocs run: | - yarn install - yarn typedocs-extensions-api + yarn install + yarn typedocs-extensions-api - name: Verify that the markdown is valid run: | diff --git a/.github/workflows/electronegativity.yml b/.github/workflows/electronegativity.yml index 6e634082c4..0cc4340f39 100644 --- a/.github/workflows/electronegativity.yml +++ b/.github/workflows/electronegativity.yml @@ -19,7 +19,7 @@ jobs: - uses: doyensec/electronegativity-action@v1.1 with: input: src/ - electron-version: "15.5.7" + electron-version: "19.0.4" severity: medium - name: Upload sarif diff --git a/.github/workflows/mkdocs-manual.yml b/.github/workflows/mkdocs-manual.yml index cda83b86c3..56bfd0c831 100644 --- a/.github/workflows/mkdocs-manual.yml +++ b/.github/workflows/mkdocs-manual.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python 3.7 uses: actions/setup-python@v2 with: - python-version: '3.x' + python-version: "3.x" - name: Install dependencies run: | @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 - ref: '${{ github.event.inputs.version }}' + ref: "${{ github.event.inputs.version }}" - name: Using Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 @@ -43,8 +43,8 @@ jobs: - name: Checkout master branch from lens uses: actions/checkout@v2 with: - path: 'master' - ref: 'master' + path: "master" + ref: "master" - name: Bring in latest mkdocs.yml from master run: | diff --git a/.github/workflows/publish-release-npm.yml b/.github/workflows/publish-release-npm.yml index 79a06c000f..becca1f790 100644 --- a/.github/workflows/publish-release-npm.yml +++ b/.github/workflows/publish-release-npm.yml @@ -23,11 +23,11 @@ jobs: - name: Generate NPM package run: | - make build-npm + make build-npm - name: publish new release if: contains(github.ref, 'refs/tags/v') run: | - make publish-npm + make publish-npm env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.yarnrc b/.yarnrc index 22e66ac2fe..811b4fd7c3 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ -disturl "https://atom.io/download/electron" -target "15.5.0" +disturl "https://electronjs.org/headers" +target "19.0.4" runtime "electron" diff --git a/build/download_binaries.ts b/build/download_binaries.ts index 5b4f960c5e..ff26a8fb2b 100644 --- a/build/download_binaries.ts +++ b/build/download_binaries.ts @@ -13,7 +13,6 @@ import { promisify } from "util"; import { pipeline as _pipeline, Transform, Writable } from "stream"; import type { SingleBar } from "cli-progress"; import { MultiBar } from "cli-progress"; -import AbortController from "abort-controller"; import { extract } from "tar-stream"; import gunzip from "gunzip-maybe"; import { getBinaryName, normalizedPlatform } from "../src/common/vars"; diff --git a/build/notarize.js b/build/notarize.js index 3fb8cdee42..ded81f6dd1 100644 --- a/build/notarize.js +++ b/build/notarize.js @@ -18,7 +18,7 @@ exports.default = async function notarizing(context) { const appName = context.packager.appInfo.productFilename; return await notarize({ - appBundleId: "io.kontena.lens-app", + appBundleId: process.env.APPBUNDLEID || "io.kontena.lens-app", appPath: `${appOutDir}/${appName}.app`, appleId: process.env.APPLEID, appleIdPassword: process.env.APPLEIDPASS, diff --git a/docs/architecture/decisions/README.md b/docs/architecture/decisions/README.md new file mode 100644 index 0000000000..ff03a7ca8a --- /dev/null +++ b/docs/architecture/decisions/README.md @@ -0,0 +1,2 @@ +# Architecture Decision Records + diff --git a/docs/extensions/get-started/anatomy.md b/docs/extensions/get-started/anatomy.md index 8cfcd57076..4794261d8f 100644 --- a/docs/extensions/get-started/anatomy.md +++ b/docs/extensions/get-started/anatomy.md @@ -55,7 +55,7 @@ Some of the most-important fields include: "license": "MIT", "homepage": "https://github.com/lensapp/lens-extension-samples", "engines": { - "node": "^14.18.12", + "node": "^16.14.2", "lens": "5.4" }, "main": "dist/main.js", @@ -72,7 +72,7 @@ Some of the most-important fields include: "ts-loader": "^8.0.4", "typescript": "^4.5.5", "@types/react": "^17.0.44", - "@types/node": "^14.18.12", + "@types/node": "^16.14.2", "webpack": "^4.44.2", "webpack-cli": "^3.3.11" } diff --git a/package.json b/package.json index 3e6ff7be63..3dd4c90dbd 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,10 @@ "email": "info@k8slens.dev" }, "scripts": { + "adr:create": "echo \"What is the title?\"; read title; adr new \"$title\"", + "adr:change-status": "echo \"Decision number?:\"; read decision; adr status $decision", + "adr:update-readme": "adr update", + "adr:list": "adr list", "dev": "concurrently -i -k \"yarn run dev-run -C\" yarn:dev:*", "dev-build": "concurrently yarn:compile:*", "debug-build": "concurrently yarn:compile:main yarn:compile:extension-types", @@ -210,16 +214,15 @@ "@kubernetes/client-node": "^0.17.0", "@material-ui/styles": "^4.11.5", "@ogre-tools/fp": "9.0.1", - "@ogre-tools/injectable": "9.0.1", - "@ogre-tools/injectable-extension-for-auto-registration": "9.0.1", - "@ogre-tools/injectable-extension-for-mobx": "9.0.1", - "@ogre-tools/injectable-react": "9.0.1", + "@ogre-tools/injectable": "9.0.2", + "@ogre-tools/injectable-extension-for-auto-registration": "9.0.2", + "@ogre-tools/injectable-extension-for-mobx": "9.0.2", + "@ogre-tools/injectable-react": "9.0.2", "@sentry/electron": "^3.0.7", "@sentry/integrations": "^6.19.3", "@side/jest-runtime": "^1.0.1", "@tanstack/react-table": "^8.5.5", "@types/circular-dependency-plugin": "5.0.5", - "abort-controller": "^3.0.0", "auto-bind": "^4.0.0", "await-lock": "^2.2.2", "byline": "^5.0.0", @@ -249,7 +252,7 @@ "mobx-observable-history": "^2.0.3", "mobx-react": "^7.5.2", "mobx-utils": "^6.0.4", - "mock-fs": "^5.1.2", + "mock-fs": "^5.1.4", "moment": "^2.29.4", "moment-timezone": "^0.5.34", "monaco-editor": "^0.29.1", @@ -284,7 +287,8 @@ "winston": "^3.8.1", "winston-console-format": "^1.0.8", "winston-transport-browserconsole": "^1.0.5", - "ws": "^8.8.0" + "ws": "^8.8.1", + "xterm-link-provider": "^1.3.1" }, "devDependencies": { "@async-fn/jest": "1.6.4", @@ -293,7 +297,7 @@ "@material-ui/lab": "^4.0.0-alpha.60", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", "@sentry/types": "^6.19.7", - "@swc/core": "^1.2.218", + "@swc/core": "^1.2.223", "@swc/jest": "^0.2.22", "@testing-library/dom": "^7.31.2", "@testing-library/jest-dom": "^5.16.4", @@ -315,7 +319,7 @@ "@types/hapi__subtext": "^7.0.0", "@types/html-webpack-plugin": "^3.2.6", "@types/http-proxy": "^1.17.9", - "@types/jest": "^28.1.3", + "@types/jest": "^28.1.6", "@types/js-yaml": "^4.0.5", "@types/jsdom": "^16.2.14", "@types/lodash": "^4.14.181", @@ -323,7 +327,7 @@ "@types/md5-file": "^4.0.2", "@types/mini-css-extract-plugin": "^2.4.0", "@types/mock-fs": "^4.13.1", - "@types/node": "^16.11.45", + "@types/node": "^16.11.47", "@types/node-fetch": "^2.6.2", "@types/npm": "^2.0.32", "@types/proper-lockfile": "^4.1.2", @@ -352,8 +356,9 @@ "@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.30.7", + "@typescript-eslint/eslint-plugin": "^5.32.0", "@typescript-eslint/parser": "^5.31.0", + "adr": "^1.4.1", "ansi_up": "^5.1.0", "chart.js": "^2.9.4", "circular-dependency-plugin": "^5.2.2", @@ -364,12 +369,12 @@ "css-loader": "^6.7.1", "deepdash": "^5.3.9", "dompurify": "^2.3.10", - "electron": "^15.5.7", - "electron-builder": "^23.1.0", + "electron": "^19.0.4", + "electron-builder": "^23.3.3", "electron-notarize": "^0.3.0", - "esbuild": "^0.14.49", + "esbuild": "^0.14.53", "esbuild-loader": "^2.19.0", - "eslint": "^8.20.0", + "eslint": "^8.21.0", "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "^2.26.0", "eslint-plugin-react": "^7.30.1", @@ -382,7 +387,7 @@ "identity-obj-proxy": "^3.0.0", "ignore-loader": "^0.1.2", "include-media": "^1.4.9", - "jest": "^28.1.2", + "jest": "^28.1.3", "jest-canvas-mock": "^2.3.1", "jest-environment-jsdom": "^28.1.3", "jest-fetch-mock": "^3.0.3", @@ -393,7 +398,7 @@ "node-gyp": "^8.3.0", "node-loader": "^2.0.0", "nodemon": "^2.0.19", - "playwright": "^1.24.1", + "playwright": "^1.24.2", "postcss": "^8.4.14", "postcss-loader": "^6.2.1", "randomcolor": "^0.6.2", @@ -402,9 +407,10 @@ "react-refresh-typescript": "^2.0.7", "react-router-dom": "^5.3.3", "react-select": "^5.4.0", - "react-select-event": "^5.5.0", + "react-select-event": "^5.5.1", + "react-table": "^7.8.0", "react-window": "^1.8.7", - "sass": "^1.53.0", + "sass": "^1.54.2", "sass-loader": "^12.6.0", "sharp": "^0.30.7", "style-loader": "^3.3.1", diff --git a/src/behaviours/tray/clicking-tray-menu-item-originating-from-extension.test.ts b/src/behaviours/tray/clicking-tray-menu-item-originating-from-extension.test.ts index 006760f902..31556a4b7d 100644 --- a/src/behaviours/tray/clicking-tray-menu-item-originating-from-extension.test.ts +++ b/src/behaviours/tray/clicking-tray-menu-item-originating-from-extension.test.ts @@ -8,6 +8,7 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import loggerInjectable from "../../common/logger.injectable"; import type { Logger } from "../../common/logger"; +import getRandomIdInjectable from "../../common/utils/get-random-id.injectable"; describe("clicking tray menu item originating from extension", () => { let applicationBuilder: ApplicationBuilder; @@ -20,6 +21,7 @@ describe("clicking tray menu item originating from extension", () => { logErrorMock = jest.fn(); mainDi.override(loggerInjectable, () => ({ error: logErrorMock }) as unknown as Logger); + mainDi.override(getRandomIdInjectable, () => () => "some-random-id"); }); await applicationBuilder.render(); @@ -42,7 +44,7 @@ describe("clicking tray menu item originating from extension", () => { it("when item is clicked, triggers the click handler", () => { applicationBuilder.tray.click( - "some-label-tray-menu-item-for-extension-some-extension-id", + "some-random-id-tray-menu-item-for-extension-some-extension-id", ); expect(clickMock).toHaveBeenCalled(); @@ -55,13 +57,13 @@ describe("clicking tray menu item originating from extension", () => { }); applicationBuilder.tray.click( - "some-label-tray-menu-item-for-extension-some-extension-id", + "some-random-id-tray-menu-item-for-extension-some-extension-id", ); }); it("logs the error", () => { expect(logErrorMock).toHaveBeenCalledWith( - '[TRAY]: Clicking of tray item "some-label" from extension "some-extension-id" failed.', + '[TRAY]: Clicking of tray item "some-random-id" from extension "some-extension-id" failed.', expect.any(Error), ); }); @@ -72,13 +74,13 @@ describe("clicking tray menu item originating from extension", () => { clickMock.mockImplementation(() => Promise.reject("some-rejection")); applicationBuilder.tray.click( - "some-label-tray-menu-item-for-extension-some-extension-id", + "some-random-id-tray-menu-item-for-extension-some-extension-id", ); }); it("logs the error", () => { expect(logErrorMock).toHaveBeenCalledWith( - '[TRAY]: Clicking of tray item "some-label" from extension "some-extension-id" failed.', + '[TRAY]: Clicking of tray item "some-random-id" from extension "some-extension-id" failed.', "some-rejection", ); }); @@ -92,7 +94,7 @@ describe("clicking tray menu item originating from extension", () => { it("does not have the tray menu item from extension", () => { expect( applicationBuilder.tray.get( - "some-label-tray-menu-item-for-extension-some-extension-id", + "some-random-id-tray-menu-item-for-extension-some-extension-id", ), ).toBeNull(); }); @@ -103,7 +105,7 @@ describe("clicking tray menu item originating from extension", () => { expect( applicationBuilder.tray.get( - "some-label-tray-menu-item-for-extension-some-extension-id", + "some-random-id-tray-menu-item-for-extension-some-extension-id", ), ).not.toBeNull(); }); diff --git a/src/behaviours/tray/extension-adding-tray-items.test.tsx b/src/behaviours/tray/extension-adding-tray-items.test.tsx index 35e09e0f76..5d9004ba5e 100644 --- a/src/behaviours/tray/extension-adding-tray-items.test.tsx +++ b/src/behaviours/tray/extension-adding-tray-items.test.tsx @@ -13,6 +13,7 @@ describe("preferences: extension adding tray items", () => { let builder: ApplicationBuilder; let someObservableForVisibility: IObservableValue; let someObservableForEnabled: IObservableValue; + let someObservableLabel: IObservableValue; beforeEach(async () => { builder = getApplicationBuilder(); @@ -25,6 +26,7 @@ describe("preferences: extension adding tray items", () => { someObservableForVisibility = observable.box(false); someObservableForEnabled = observable.box(false); + someObservableLabel = observable.box("Some label"); const testExtension = getExtensionFake({ id: "some-extension-id", @@ -33,38 +35,51 @@ describe("preferences: extension adding tray items", () => { mainOptions: { trayMenus: [ { + id: "some-controlled-visibility", label: "some-controlled-visibility", click: () => {}, visible: computed(() => someObservableForVisibility.get()), }, { + id: "some-uncontrolled-visibility", label: "some-uncontrolled-visibility", click: () => {}, }, { + id: "some-controlled-enabled", label: "some-controlled-enabled", click: () => {}, enabled: computed(() => someObservableForEnabled.get()), }, { + id: "some-uncontrolled-enabled", label: "some-uncontrolled-enabled", click: () => {}, }, { + id: "some-statically-enabled", label: "some-statically-enabled", click: () => {}, enabled: true, }, { + id: "some-statically-disabled", label: "some-statically-disabled", click: () => {}, enabled: false, }, + + { + id: "some-item-with-controlled-label", + label: computed(() => someObservableLabel.get()), + click: () => {}, + enabled: true, + }, ], }, }); @@ -72,6 +87,37 @@ describe("preferences: extension adding tray items", () => { builder.extensions.enable(testExtension); }); + describe("given controlled label", () => { + it("has the label", () => { + const item = builder.tray.get( + "some-item-with-controlled-label-tray-menu-item-for-extension-some-extension", + ); + + expect(item?.label).toBe("Some label"); + }); + + it("when label changes, updates the label", () => { + runInAction(() => { + someObservableLabel.set("Some new label"); + }); + + const item = builder.tray.get( + "some-item-with-controlled-label-tray-menu-item-for-extension-some-extension", + ); + + expect(item?.label).toBe("Some new label"); + + }); + }); + + it("given item is statically disabled, item is disabled", () => { + const item = builder.tray.get( + "some-statically-disabled-tray-menu-item-for-extension-some-extension", + ); + + expect(item?.enabled).toBe(false); + }); + it("shows item which doesn't control the visibility", () => { expect( builder.tray.get( diff --git a/src/common/k8s-api/__tests__/kube-api.test.ts b/src/common/k8s-api/__tests__/kube-api.test.ts index 36f2aea748..0b0efd86da 100644 --- a/src/common/k8s-api/__tests__/kube-api.test.ts +++ b/src/common/k8s-api/__tests__/kube-api.test.ts @@ -6,7 +6,6 @@ import { forRemoteCluster, KubeApi } from "../kube-api"; import { KubeJsonApi } from "../kube-json-api"; import { KubeObject } from "../kube-object"; -import AbortController from "abort-controller"; import { delay } from "../../utils/delay"; import { PassThrough } from "stream"; import { ApiManager } from "../api-manager"; diff --git a/src/common/k8s-api/endpoints/horizontal-pod-autoscaler.api.ts b/src/common/k8s-api/endpoints/horizontal-pod-autoscaler.api.ts index 9b1fa75955..70857fd881 100644 --- a/src/common/k8s-api/endpoints/horizontal-pod-autoscaler.api.ts +++ b/src/common/k8s-api/endpoints/horizontal-pod-autoscaler.api.ts @@ -58,11 +58,11 @@ export interface ResourceMetricSource { } export interface BaseHorizontalPodAutoscalerMetricSpec { - resource: ResourceMetricSource; - object: ObjectMetricSource; - external: ExternalMetricSource; - pods: PodsMetricSource; containerResource: ContainerResourceMetricSource; + external: ExternalMetricSource; + object: ObjectMetricSource; + pods: PodsMetricSource; + resource: ResourceMetricSource; } export type HorizontalPodAutoscalerMetricSpec = @@ -72,6 +72,55 @@ export type HorizontalPodAutoscalerMetricSpec = | OptionVarient | OptionVarient; +export interface ContainerResourceMetricStatus { + container: string; + currentAverageUtilization?: number; + currentAverageValue: string; + name: string; +} + +export interface ExternalMetricStatus { + currentAverageValue?: string; + currentValue: string; + metricName: string; + metricSelector?: LabelSelector; +} + +export interface ObjectMetricStatus { + averageValue?: string; + currentValue?: string; + metricName: string; + selector?: LabelSelector; + target: CrossVersionObjectReference; +} + +export interface PodsMetricStatus { + currentAverageValue: string; + metricName: string; + selector?: LabelSelector; +} + +export interface ResourceMetricStatus { + currentAverageUtilization?: number; + currentAverageValue: string; + name: string; +} + +export interface BaseHorizontalPodAutoscalerMetricStatus { + containerResource: ContainerResourceMetricStatus; + external: ExternalMetricStatus; + object: ObjectMetricStatus; + pods: PodsMetricStatus; + resource: ResourceMetricStatus; +} + +export type HorizontalPodAutoscalerMetricStatus = + | OptionVarient + | OptionVarient + | OptionVarient + | OptionVarient + | OptionVarient; + export interface CrossVersionObjectReference { kind: string; name: string; @@ -89,7 +138,7 @@ export interface HorizontalPodAutoscalerStatus { conditions?: BaseKubeObjectCondition[]; currentReplicas: number; desiredReplicas: number; - currentMetrics: HorizontalPodAutoscalerMetricSpec[]; + currentMetrics?: HorizontalPodAutoscalerMetricStatus[]; } interface MetricCurrentTarget { @@ -142,114 +191,11 @@ export class HorizontalPodAutoscaler extends KubeObject< return this.status?.currentMetrics ?? []; } - protected getMetricName(metric: HorizontalPodAutoscalerMetricSpec): string { - switch (metric.type) { - case HpaMetricType.Resource: - return metric.resource.name; - case HpaMetricType.Pods: - return metric.pods.metricName; - case HpaMetricType.Object: - return metric.object.metricName; - case HpaMetricType.External: - return metric.external.metricName; - case HpaMetricType.ContainerResource: - return metric.containerResource.name; - default: - return ``; - } - } - - protected getResourceMetricValue(currentMetric: ResourceMetricSource | undefined, targetMetric: ResourceMetricSource): MetricCurrentTarget { - return { - current: ( - currentMetric?.targetAverageUtilization - ? `${currentMetric.targetAverageUtilization}%` - : currentMetric?.targetAverageValue - ), - target: ( - targetMetric?.targetAverageUtilization - ? `${targetMetric.targetAverageUtilization}%` - : targetMetric?.targetAverageValue - ), - }; - } - - protected getPodsMetricValue(currentMetric: PodsMetricSource | undefined, targetMetric: PodsMetricSource): MetricCurrentTarget { - return { - current: currentMetric?.targetAverageValue, - target: targetMetric?.targetAverageValue, - }; - } - - protected getObjectMetricValue(currentMetric: ObjectMetricSource | undefined, targetMetric: ObjectMetricSource): MetricCurrentTarget { - return { - current: ( - currentMetric?.targetValue - ?? currentMetric?.averageValue - ), - target: ( - targetMetric?.targetValue - ?? targetMetric?.averageValue - ), - }; - } - - protected getExternalMetricValue(currentMetric: ExternalMetricSource | undefined, targetMetric: ExternalMetricSource): MetricCurrentTarget { - return { - current: ( - currentMetric?.targetValue - ?? currentMetric?.targetAverageValue - ), - target: ( - targetMetric?.targetValue - ?? targetMetric?.targetAverageValue - ), - }; - } - - protected getContainerResourceMetricValue(currentMetric: ContainerResourceMetricSource | undefined, targetMetric: ContainerResourceMetricSource): MetricCurrentTarget { - return { - current: ( - currentMetric?.targetAverageUtilization - ? `${currentMetric.targetAverageUtilization}%` - : currentMetric?.targetAverageValue - ), - target: ( - targetMetric?.targetAverageUtilization - ? `${targetMetric.targetAverageUtilization}%` - : targetMetric?.targetAverageValue - ), - }; - } - - protected getMetricCurrentTarget(metric: HorizontalPodAutoscalerMetricSpec): MetricCurrentTarget { - const currentMetric = this.getMetrics() - .find(m => ( - m.type === metric.type - && this.getMetricName(m) === this.getMetricName(metric) - )); - - switch (metric.type) { - case HpaMetricType.Resource: - return this.getResourceMetricValue(currentMetric?.resource, metric.resource); - case HpaMetricType.Pods: - return this.getPodsMetricValue(currentMetric?.pods, metric.pods); - case HpaMetricType.Object: - return this.getObjectMetricValue(currentMetric?.object, metric.object); - case HpaMetricType.External: - return this.getExternalMetricValue(currentMetric?.external, metric.external); - case HpaMetricType.ContainerResource: - return this.getContainerResourceMetricValue(currentMetric?.containerResource, metric.containerResource); - default: - return {}; - } - } - getMetricValues(metric: HorizontalPodAutoscalerMetricSpec): string { const { current = "unknown", target = "unknown", - } = this.getMetricCurrentTarget(metric); + } = getMetricCurrentTarget(metric, this.getCurrentMetrics()); return `${current} / ${target}`; } @@ -263,3 +209,105 @@ export class HorizontalPodAutoscalerApi extends KubeApi }); } } + +function getMetricName(metric: HorizontalPodAutoscalerMetricSpec | HorizontalPodAutoscalerMetricStatus): string | undefined { + switch (metric.type) { + case HpaMetricType.Resource: + return metric.resource.name; + case HpaMetricType.Pods: + return metric.pods.metricName; + case HpaMetricType.Object: + return metric.object.metricName; + case HpaMetricType.External: + return metric.external.metricName; + case HpaMetricType.ContainerResource: + return metric.containerResource.name; + default: + return undefined; + } +} + +function getResourceMetricValue(currentMetric: ResourceMetricStatus | undefined, targetMetric: ResourceMetricSource): MetricCurrentTarget { + return { + current: ( + typeof currentMetric?.currentAverageUtilization === "number" + ? `${currentMetric.currentAverageUtilization}%` + : currentMetric?.currentAverageValue + ), + target: ( + typeof targetMetric?.targetAverageUtilization === "number" + ? `${targetMetric.targetAverageUtilization}%` + : targetMetric?.targetAverageValue + ), + }; +} + +function getPodsMetricValue(currentMetric: PodsMetricStatus | undefined, targetMetric: PodsMetricSource): MetricCurrentTarget { + return { + current: currentMetric?.currentAverageValue, + target: targetMetric?.targetAverageValue, + }; +} + +function getObjectMetricValue(currentMetric: ObjectMetricStatus | undefined, targetMetric: ObjectMetricSource): MetricCurrentTarget { + return { + current: ( + currentMetric?.currentValue + ?? currentMetric?.averageValue + ), + target: ( + targetMetric?.targetValue + ?? targetMetric?.averageValue + ), + }; +} + +function getExternalMetricValue(currentMetric: ExternalMetricStatus | undefined, targetMetric: ExternalMetricSource): MetricCurrentTarget { + return { + current: ( + currentMetric?.currentValue + ?? currentMetric?.currentAverageValue + ), + target: ( + targetMetric?.targetValue + ?? targetMetric?.targetAverageValue + ), + }; +} + +function getContainerResourceMetricValue(currentMetric: ContainerResourceMetricStatus | undefined, targetMetric: ContainerResourceMetricSource): MetricCurrentTarget { + return { + current: ( + typeof currentMetric?.currentAverageUtilization === "number" + ? `${currentMetric.currentAverageUtilization}%` + : currentMetric?.currentAverageValue + ), + target: ( + typeof targetMetric?.targetAverageUtilization === "number" + ? `${targetMetric.targetAverageUtilization}%` + : targetMetric?.targetAverageValue + ), + }; +} + +function getMetricCurrentTarget(spec: HorizontalPodAutoscalerMetricSpec, status: HorizontalPodAutoscalerMetricStatus[]): MetricCurrentTarget { + const currentMetric = status.find(m => ( + m.type === spec.type + && getMetricName(m) === getMetricName(spec) + )); + + switch (spec.type) { + case HpaMetricType.Resource: + return getResourceMetricValue(currentMetric?.resource, spec.resource); + case HpaMetricType.Pods: + return getPodsMetricValue(currentMetric?.pods, spec.pods); + case HpaMetricType.Object: + return getObjectMetricValue(currentMetric?.object, spec.object); + case HpaMetricType.External: + return getExternalMetricValue(currentMetric?.external, spec.external); + case HpaMetricType.ContainerResource: + return getContainerResourceMetricValue(currentMetric?.containerResource, spec.containerResource); + default: + return {}; + } +} diff --git a/src/common/k8s-api/kube-api.ts b/src/common/k8s-api/kube-api.ts index 5427b22159..31fad33d72 100644 --- a/src/common/k8s-api/kube-api.ts +++ b/src/common/k8s-api/kube-api.ts @@ -19,7 +19,6 @@ import { KubeJsonApi } from "./kube-json-api"; import type { Disposer } from "../utils"; import { isDefined, noop, WrappedAbortController } from "../utils"; import type { RequestInit } from "node-fetch"; -import type AbortController from "abort-controller"; import type { AgentOptions } from "https"; import { Agent } from "https"; import type { Patch } from "rfc6902"; @@ -29,6 +28,9 @@ import logger from "../logger"; import { Environments, getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import autoRegistrationEmitterInjectable from "./api-manager/auto-registration-emitter.injectable"; +// TODO: upgrade node-fetch once we are starting to use ES modules +type LegacyAbortSignal = NonNullable; + /** * The options used for creating a `KubeApi` */ @@ -717,7 +719,7 @@ export class KubeApi< const requestParams = timeout ? { query: { timeoutSeconds: timeout }} : {}; const watchUrl = this.getWatchUrl(namespace); const responsePromise = this.request.getResponse(watchUrl, requestParams, { - signal: abortController.signal, + signal: abortController.signal as LegacyAbortSignal, timeout: 600_000, }); diff --git a/src/common/k8s-api/kube-object.store.ts b/src/common/k8s-api/kube-object.store.ts index 0af08e560c..852e615332 100644 --- a/src/common/k8s-api/kube-object.store.ts +++ b/src/common/k8s-api/kube-object.store.ts @@ -15,13 +15,15 @@ import { ItemStore } from "../item.store"; import type { KubeApiQueryParams, KubeApi, KubeApiWatchCallback } from "./kube-api"; import { parseKubeApi } from "./kube-api-parse"; import type { RequestInit } from "node-fetch"; -import AbortController from "abort-controller"; import type { Patch } from "rfc6902"; import logger from "../logger"; import assert from "assert"; import type { PartialDeep } from "type-fest"; import { entries } from "../utils/objects"; +// TODO: upgrade node-fetch once we are starting to use ES modules +type LegacyAbortSignal = NonNullable; + export type OnLoadFailure = (error: unknown) => void; export interface KubeObjectStoreLoadingParams { @@ -477,7 +479,8 @@ export abstract class KubeObjectStore< callback, }); - const { signal } = abortController; + // TODO: upgrade node-fetch once we are starting to use ES modules + const signal = abortController.signal as LegacyAbortSignal; const callback: KubeApiWatchCallback = (data, error) => { if (!this.isLoaded || error?.type === "aborted") return; diff --git a/src/common/test-utils/get-global-override.ts b/src/common/test-utils/get-global-override.ts index 3a556df793..ac3c86a33e 100644 --- a/src/common/test-utils/get-global-override.ts +++ b/src/common/test-utils/get-global-override.ts @@ -4,6 +4,11 @@ */ import type { Injectable } from "@ogre-tools/injectable"; +export interface GlobalOverride { + injectable: Injectable; + overridingInstantiate: any; +} + export const getGlobalOverride = >( injectable: T, overridingInstantiate: T["instantiate"], diff --git a/src/common/utils/__tests__/union-env-path.test.ts b/src/common/utils/__tests__/union-env-path.test.ts new file mode 100644 index 0000000000..ff8ca916d2 --- /dev/null +++ b/src/common/utils/__tests__/union-env-path.test.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 path from "path"; +import { unionPATHs } from "../union-env-path"; + +describe("unionPATHs", () => { + it("return the same path if given only one with no double delimiters", () => { + expect(unionPATHs(`/bin/bar${path.delimiter}/usr/bin`)).toBe(`/bin/bar${path.delimiter}/usr/bin`); + }); + + it("return equivalent path if given only one with no double delimiters", () => { + expect(unionPATHs(`/bin/bar${path.delimiter}${path.delimiter}/usr/bin`)).toBe(`/bin/bar${path.delimiter}/usr/bin`); + }); + + it("should remove duplicate entries, appending non duplicates in order received", () => { + expect(unionPATHs( + `/bin/bar${path.delimiter}/usr/bin`, + `/bin/bar${path.delimiter}/usr/lens/bat`, + )).toBe(`/bin/bar${path.delimiter}/usr/bin${path.delimiter}/usr/lens/bat`); + }); + + it("should remove duplicate entries, appending non duplicates in order received, 3", () => { + expect(unionPATHs( + `/bin/bar${path.delimiter}/usr/bin`, + `/bin/bar${path.delimiter}/usr/lens/bat`, + `/usr/local/lens${path.delimiter}/usr/bin`, + )).toBe(`/bin/bar${path.delimiter}/usr/bin${path.delimiter}/usr/lens/bat${path.delimiter}/usr/local/lens`); + }); +}); diff --git a/src/common/utils/abort-controller.ts b/src/common/utils/abort-controller.ts index e115fd5868..5382281cf9 100644 --- a/src/common/utils/abort-controller.ts +++ b/src/common/utils/abort-controller.ts @@ -2,10 +2,13 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import AbortController from "abort-controller"; +/** + * This is like an `AbortController` but will also abort if the parent aborts, + * but won't make the parent abort if this aborts (single direction) + */ export class WrappedAbortController extends AbortController { - constructor(parent?: AbortController) { + constructor(parent?: AbortController | undefined) { super(); parent?.signal.addEventListener("abort", () => { diff --git a/src/common/utils/convertCpu.ts b/src/common/utils/convertCpu.ts index 5d537b34e7..86b57b78f8 100644 --- a/src/common/utils/convertCpu.ts +++ b/src/common/utils/convertCpu.ts @@ -3,18 +3,38 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +import { TypedRegEx } from "typed-regex"; + // Helper to convert CPU K8S units to numbers -const thousand = 1000; -const million = thousand * thousand; -const shortBillion = thousand * million; +const unitConverters = new Map([ + ["n", 1000 ** -3], + ["u", 1000 ** -2], + ["m", 1000 ** -1], // milli + ["", 1000 ** 0], // no units + ["k", 1000 ** 1], + ["M", 1000 ** 2], + ["G", 1000 ** 3], + ["P", 1000 ** 4], + ["T", 1000 ** 5], + ["E", 1000 ** 6], +]); -export function cpuUnitsToNumber(cpu: string) { - const cpuNum = parseInt(cpu); +const cpuUnitsRegex = TypedRegEx("^(?[+-]?[0-9.]+(e[-+]?[0-9]+)?)(?[EinumkKMGTP]*)$"); - if (cpu.includes("m")) return cpuNum / thousand; - if (cpu.includes("u")) return cpuNum / million; - if (cpu.includes("n")) return cpuNum / shortBillion; +export function cpuUnitsToNumber(value: string) { + const match = cpuUnitsRegex.captures(value); - return parseFloat(cpu); + if (!match) { + return undefined; + } + + const { digits = "", unit } = match; + const conversion = unitConverters.get(unit); + + if (conversion === undefined) { + return undefined; + } + + return parseFloat(digits) * conversion; } diff --git a/src/common/utils/delay.ts b/src/common/utils/delay.ts index 2c4a5c455e..d86395026b 100644 --- a/src/common/utils/delay.ts +++ b/src/common/utils/delay.ts @@ -3,8 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { AbortController } from "abort-controller"; - /** * Return a promise that will be resolved after at least `timeout` ms have * passed. If `failFast` is provided then the promise is also resolved if it has diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index f0aa66294b..e16f803c56 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -22,7 +22,6 @@ export * from "./hash-set"; export * from "./n-fircate"; export * from "./noop"; export * from "./observable-crate/impl"; -export * from "./openBrowser"; export * from "./paths"; export * from "./promise-exec"; export * from "./readonly"; diff --git a/src/common/utils/open-link-in-browser.global-override-for-injectable.ts b/src/common/utils/open-link-in-browser.global-override-for-injectable.ts new file mode 100644 index 0000000000..62c1539757 --- /dev/null +++ b/src/common/utils/open-link-in-browser.global-override-for-injectable.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import openLinkInBrowserInjectable from "./open-link-in-browser.injectable"; + +export default getGlobalOverride(openLinkInBrowserInjectable, () => async () => {}); diff --git a/src/common/utils/open-link-in-browser.injectable.ts b/src/common/utils/open-link-in-browser.injectable.ts new file mode 100644 index 0000000000..eaa91939e9 --- /dev/null +++ b/src/common/utils/open-link-in-browser.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 { shell } from "electron"; + +const allowedProtocols = new Set(["http:", "https:"]); + +export type OpenLinkInBrowser = (url: string) => Promise; + +const openLinkInBrowserInjectable = getInjectable({ + id: "open-link-in-browser", + instantiate: (): OpenLinkInBrowser => ( + async (url) => { + const { protocol } = new URL(url); + + if (!allowedProtocols.has(protocol)) { + throw new TypeError("not an http(s) URL"); + } + + await shell.openExternal(url); + } + ), + causesSideEffects: true, +}); + +export default openLinkInBrowserInjectable; diff --git a/src/common/utils/openBrowser.ts b/src/common/utils/openBrowser.ts deleted file mode 100644 index 3ba29b699b..0000000000 --- a/src/common/utils/openBrowser.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { shell } from "electron"; - -const allowedProtocols = new Set(["http:", "https:"]); - -/** - * Opens a link using the program configured as the default browser - * on the target platform. Will reject URLs with a scheme other than - * http or https to prevent programs other than the default browser - * running. - * - * @param url The URL to open - */ -export function openBrowser(url: string): Promise { - if (allowedProtocols.has(new URL(url).protocol)) { - return shell.openExternal(url); - } - - return Promise.reject(new TypeError("not an http(s) URL")); -} - -/** - * @deprecated use openBrowser - */ -export const openExternal = openBrowser; diff --git a/src/common/utils/reject-promise.ts b/src/common/utils/reject-promise.ts index 8212bacd3f..0211586783 100644 --- a/src/common/utils/reject-promise.ts +++ b/src/common/utils/reject-promise.ts @@ -3,8 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { AbortSignal } from "abort-controller"; - /** * Creates a new promise that will be rejected when the signal rejects. * diff --git a/src/common/utils/union-env-path.ts b/src/common/utils/union-env-path.ts new file mode 100644 index 0000000000..991e2c776c --- /dev/null +++ b/src/common/utils/union-env-path.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 path from "path"; +import * as iter from "./iter"; + +/** + * Join all entires with a PATH env var delimated string together + * @param PATHs Any number of PATH env variables + * + * NOTE: This function does not attempt to handle any sort of escape sequences since after testing + * it was found that `zsh` (at least on `macOS`) does not when trying to find programs + */ +export function unionPATHs(...PATHs: string[]): string { + const entries = new Set(iter.filterFlatMap(PATHs, PATH => PATH.split(path.delimiter))); + + return iter.join(entries.values(), path.delimiter); +} diff --git a/src/extensions/common-api/utils.ts b/src/extensions/common-api/utils.ts index 8e6ba1c9af..3dea165238 100644 --- a/src/extensions/common-api/utils.ts +++ b/src/extensions/common-api/utils.ts @@ -3,6 +3,15 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export { Singleton, openExternal, openBrowser, getAppVersion } from "../../common/utils"; +import openLinkInBrowserInjectable from "../../common/utils/open-link-in-browser.injectable"; +import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api"; + +export { Singleton, getAppVersion } from "../../common/utils"; export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault"; export { cssNames } from "../../renderer/utils/cssNames"; + +/** + * @deprecated Use {@link openBrowser} instead + */ +export const openExternal = asLegacyGlobalFunctionForExtensionApi(openLinkInBrowserInjectable); +export const openBrowser = asLegacyGlobalFunctionForExtensionApi(openLinkInBrowserInjectable); diff --git a/src/jest.setup.ts b/src/jest.setup.ts index 4b3f787cfc..c2f151ff76 100644 --- a/src/jest.setup.ts +++ b/src/jest.setup.ts @@ -8,6 +8,8 @@ import configurePackages from "./common/configure-packages"; import { configure } from "mobx"; import { setImmediate } from "timers"; import { TextEncoder, TextDecoder as TextDecoderNode } from "util"; +import glob from "glob"; +import path from "path"; // setup default configuration for external npm-packages configurePackages(); @@ -45,3 +47,13 @@ global.ResizeObserver = class { jest.mock("./renderer/components/monaco-editor/monaco-editor"); jest.mock("./renderer/components/tooltip/withTooltip"); + +const getInjectables = (environment: "renderer" | "main", filePathGlob: string) => + glob.sync(`./{common,extensions,${environment}}/**/${filePathGlob}`, { + cwd: __dirname, + }).map(x => path.resolve(__dirname, x)); + +(global as any).rendererInjectablePaths = getInjectables("renderer", "*.injectable.{ts,tsx}"); +(global as any).rendererGlobalOverridePaths = getInjectables("renderer", "*.global-override-for-injectable.{ts,tsx}"); +(global as any).mainInjectablePaths = getInjectables("main", "*.injectable.{ts,tsx}"); +(global as any).mainGlobalOverridePaths = getInjectables("main", "*.global-override-for-injectable.{ts,tsx}"); 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 index 4c80e8178e..347015b584 100644 --- 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 @@ -4,8 +4,8 @@ */ 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"; +import { afterApplicationIsLoadedInjectionToken } from "../../start-main-application/runnable-tokens/after-application-is-loaded-injection-token"; const startCheckingForUpdatesInjectable = getInjectable({ id: "start-checking-for-updates", @@ -23,7 +23,7 @@ const startCheckingForUpdatesInjectable = getInjectable({ }; }, - injectionToken: afterRootFrameIsReadyInjectionToken, + injectionToken: afterApplicationIsLoadedInjectionToken, }); 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 index 13aefe4d96..944642b674 100644 --- 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 @@ -3,8 +3,8 @@ * 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"; +import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; const stopCheckingForUpdatesInjectable = getInjectable({ id: "stop-checking-for-updates", @@ -21,7 +21,7 @@ const stopCheckingForUpdatesInjectable = getInjectable({ }; }, - injectionToken: beforeQuitOfFrontEndInjectionToken, + injectionToken: beforeQuitOfBackEndInjectionToken, }); export default stopCheckingForUpdatesInjectable; diff --git a/src/main/catalog-sources/kubeconfig-sync/manager.ts b/src/main/catalog-sources/kubeconfig-sync/manager.ts index 892fa97eb9..1d281bc936 100644 --- a/src/main/catalog-sources/kubeconfig-sync/manager.ts +++ b/src/main/catalog-sources/kubeconfig-sync/manager.ts @@ -11,9 +11,8 @@ import { watch } from "chokidar"; import type { Stats } from "fs"; import fs from "fs"; import path from "path"; -import type stream from "stream"; import type { Disposer } from "../../../common/utils"; -import { bytesToUnits, getOrInsertWith, iter, noop } from "../../../common/utils"; +import { disposer, bytesToUnits, getOrInsertWith, iter, noop } from "../../../common/utils"; import logger from "../../logger"; import type { KubeConfig } from "@kubernetes/client-node"; import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers"; @@ -265,47 +264,35 @@ const diffChangedConfigFor = (dependencies: ComputeDiffDependencies) => ({ fileP return noop; } - // TODO: replace with an AbortController with fs.readFile when we upgrade to Node 16 (after it comes out) - const fileReader = fs.createReadStream(filePath, { - mode: fs.constants.O_RDONLY, + const controller = new AbortController(); + const fileContentsP = fs.promises.readFile(filePath, { + signal: controller.signal, }); - const readStream: stream.Readable = fileReader; - const decoder = new TextDecoder("utf-8", { fatal: true }); - let fileString = ""; - let closed = false; + const cleanup = disposer( + () => controller.abort(), + ); - const cleanup = () => { - closed = true; - fileReader.close(); // This may not close the stream. - // Artificially marking end-of-stream, as if the underlying resource had - // indicated end-of-file by itself, allows the stream to close. - // This does not cancel pending read operations, and if there is such an - // operation, the process may still not be able to exit successfully - // until it finishes. - fileReader.push(null); - fileReader.read(0); - readStream.removeAllListeners(); - }; + fileContentsP + .then((fileData) => { + const decoder = new TextDecoder("utf-8", { fatal: true }); - readStream - .on("data", (chunk: Buffer) => { try { - fileString += decoder.decode(chunk, { stream: true }); + const fileString = decoder.decode(fileData); + + computeDiff(dependencies)(fileString, source, filePath); } catch (error) { logger.warn(`${logPrefix} skipping ${filePath}: ${error}`); source.clear(); cleanup(); } }) - .on("close", () => cleanup()) - .on("error", error => { - cleanup(); - logger.warn(`${logPrefix} failed to read file: ${error}`, { filePath }); - }) - .on("end", () => { - if (!closed) { - computeDiff(dependencies)(fileString, source, filePath); + .catch(error => { + if (controller.signal.aborted) { + return; } + + logger.warn(`${logPrefix} failed to read file: ${error}`, { filePath }); + cleanup(); }); return cleanup; diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts index c912fcf5b5..dcdacfb253 100644 --- a/src/main/cluster-detectors/distribution-detector.ts +++ b/src/main/cluster-detectors/distribution-detector.ts @@ -96,8 +96,6 @@ export class DistributionDetector extends BaseClusterDetector { } public async getKubernetesVersion() { - if (this.cluster.version) return this.cluster.version; - const response = await this.k8sRequest("/version"); return response.gitVersion; diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 9c5a3ab741..2e9e5ad26d 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -3,8 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import glob from "glob"; -import { kebabCase, memoize, noop, chunk } from "lodash/fp"; +import { kebabCase, noop, chunk } from "lodash/fp"; import type { DiContainer, Injectable } 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"; @@ -101,6 +100,7 @@ import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; import electronInjectable from "./utils/resolve-system-proxy/electron.injectable"; import type { HotbarStore } from "../common/hotbars/store"; import focusApplicationInjectable from "./electron-app/features/focus-application.injectable"; +import type { GlobalOverride } from "../common/test-utils/get-global-override"; export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) { const { @@ -113,9 +113,9 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) setLegacyGlobalDiForExtensionApi(di, Environments.main); - const filePaths = getInjectableFilePaths(); - - const injectables = filePaths.map(filePath => require(filePath).default); + const injectables: Injectable[] = (global as any).mainInjectablePaths.map( + (filePath: string) => require(filePath).default, + ); chunk(100)(injectables).forEach(chunkInjectables => { di.register(...chunkInjectables); @@ -124,10 +124,8 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) di.preventSideEffects(); if (doGeneralOverrides) { - const globalOverrideFilePaths = getGlobalOverridePaths(); - - const globalOverrides = globalOverrideFilePaths.map( - (filePath) => require(filePath).default, + const globalOverrides: GlobalOverride[] = (global as any).mainGlobalOverridePaths.map( + (filePath: string) => require(filePath).default, ); globalOverrides.forEach(globalOverride => { @@ -215,20 +213,6 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) return di; } -const getInjectableFilePaths = memoize(() => [ - ...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }), - ...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }), - ...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }), -]); - -const getGlobalOverridePaths = memoize(() => - glob.sync( - "../{common,extensions,main}/**/*.global-override-for-injectable.{ts,tsx}", - - { cwd: __dirname }, - ), -); - // TODO: Reorganize code in Runnables to get rid of requirement for override const overrideRunnablesHavingSideEffects = (di: DiContainer) => { [ diff --git a/src/main/menu/application-menu-items.injectable.ts b/src/main/menu/application-menu-items.injectable.ts index f223c539c6..b96cfabfad 100644 --- a/src/main/menu/application-menu-items.injectable.ts +++ b/src/main/menu/application-menu-items.injectable.ts @@ -5,7 +5,6 @@ import { getInjectable } from "@ogre-tools/injectable"; import { docsUrl, productName, supportUrl } from "../../common/vars"; import { broadcastMessage } from "../../common/ipc"; -import { openBrowser } from "../../common/utils"; import type { MenuItemConstructorOptions } from "electron"; import { webContents } from "electron"; import loggerInjectable from "../../common/logger.injectable"; @@ -25,6 +24,7 @@ import applicationWindowInjectable from "../start-main-application/lens-window/a 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"; +import openLinkInBrowserInjectable from "../../common/utils/open-link-in-browser.injectable"; function ignoreIf(check: boolean, menuItems: MenuItemOpts[]) { return check ? [] : menuItems; @@ -54,6 +54,7 @@ const applicationMenuItemsInjectable = getInjectable({ const navigateToAddCluster = di.inject(navigateToAddClusterInjectable); const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable); const processCheckingForUpdates = di.inject(processCheckingForUpdatesInjectable); + const openLinkInBrowser = di.inject(openLinkInBrowserInjectable); logger.info(`[MENU]: autoUpdateEnabled=${updatingIsEnabled}`); @@ -260,7 +261,7 @@ const applicationMenuItemsInjectable = getInjectable({ label: "Documentation", id: "documentation", click: async () => { - openBrowser(docsUrl).catch((error) => { + openLinkInBrowser(docsUrl).catch((error) => { logger.error("[MENU]: failed to open browser", { error }); }); }, @@ -269,7 +270,7 @@ const applicationMenuItemsInjectable = getInjectable({ label: "Support", id: "support", click: async () => { - openBrowser(supportUrl).catch((error) => { + openLinkInBrowser(supportUrl).catch((error) => { logger.error("[MENU]: failed to open browser", { error }); }); }, diff --git a/src/main/shell-sync.ts b/src/main/shell-sync.ts index 4f78d973ae..d2c58d0abe 100644 --- a/src/main/shell-sync.ts +++ b/src/main/shell-sync.ts @@ -8,6 +8,7 @@ import os from "os"; import { app } from "electron"; import logger from "./logger"; import { isSnap } from "../common/vars"; +import { unionPATHs } from "../common/utils/union-env-path"; /** * shellSync loads what would have been the environment if this application was @@ -25,7 +26,8 @@ export async function shellSync() { } if (!isSnap) { - process.env.PATH = env.PATH; + // Prefer the synced PATH over the initial one + process.env.PATH = unionPATHs(env.PATH ?? "", process.env.PATH ?? ""); } // The spread operator allows joining of objects. The precedence is last to first. diff --git a/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts index 00fcdb6231..2eef4b8196 100644 --- a/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts +++ b/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts @@ -6,10 +6,10 @@ import { getInjectable } from "@ogre-tools/injectable"; import loggerInjectable from "../../../../common/logger.injectable"; import applicationWindowStateInjectable from "./application-window-state.injectable"; import { BrowserWindow } from "electron"; -import { openBrowser } from "../../../../common/utils"; import sendToChannelInElectronBrowserWindowInjectable from "./send-to-channel-in-electron-browser-window.injectable"; import type { ElectronWindow } from "./create-lens-window.injectable"; import type { RequireExactlyOne } from "type-fest"; +import openLinkInBrowserInjectable from "../../../../common/utils/open-link-in-browser.injectable"; export type ElectronWindowTitleBarStyle = "hiddenInset" | "hidden" | "default" | "customButtonsOnHover"; @@ -46,6 +46,7 @@ const createElectronWindowInjectable = getInjectable({ instantiate: (di): CreateElectronWindow => { const logger = di.inject(loggerInjectable); const sendToChannelInLensWindow = di.inject(sendToChannelInElectronBrowserWindowInjectable); + const openLinkInBrowser = di.inject(openLinkInBrowserInjectable); return (configuration) => { const applicationWindowState = di.inject( @@ -76,9 +77,7 @@ const createElectronWindowInjectable = getInjectable({ webPreferences: { nodeIntegration: true, nodeIntegrationInSubFrames: true, - webviewTag: true, contextIsolation: false, - nativeWindowOpen: false, }, }); @@ -88,20 +87,16 @@ const createElectronWindowInjectable = getInjectable({ .on("focus", () => { configuration.onFocus?.(); }) - .on("blur", () => { configuration.onBlur?.(); }) - .on("closed", () => { configuration.onClose(); applicationWindowState.unmanage(); }) - .webContents.on("dom-ready", () => { configuration.onDomReady?.(); }) - .on("did-fail-load", (_event, code, desc) => { logger.error( `[CREATE-ELECTRON-WINDOW]: Failed to load window "${configuration.id}"`, @@ -111,54 +106,13 @@ const createElectronWindowInjectable = getInjectable({ }, ); }) - .on("did-finish-load", () => { logger.info( `[CREATE-ELECTRON-WINDOW]: Window "${configuration.id}" loaded`, ); }) - - .on("will-attach-webview", (event, webPreferences, params) => { - logger.debug( - `[CREATE-ELECTRON-WINDOW]: Attaching webview to window "${configuration.id}"`, - ); - // Following is security recommendations because we allow webview tag (webviewTag: true) - // suggested by https://www.electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation - // and https://www.electronjs.org/docs/tutorial/security#10-do-not-use-allowpopups - - if (webPreferences.preload) { - logger.warn( - "[CREATE-ELECTRON-WINDOW]: Strip away preload scripts of webview", - ); - delete webPreferences.preload; - } - - // @ts-expect-error some electron version uses webPreferences.preloadURL/webPreferences.preload - if (webPreferences.preloadURL) { - logger.warn( - "[CREATE-ELECTRON-WINDOW]: Strip away preload scripts of webview", - ); - delete webPreferences.preload; - } - - if (params.allowpopups) { - logger.warn( - "[CREATE-ELECTRON-WINDOW]: We do not allow allowpopups props, stop webview from renderer", - ); - - // event.preventDefault() will destroy the guest page. - event.preventDefault(); - - return; - } - - // Always disable Node.js integration for all webviews - webPreferences.nodeIntegration = false; - webPreferences.nativeWindowOpen = false; - }) - .setWindowOpenHandler((details) => { - openBrowser(details.url).catch((error) => { + openLinkInBrowser(details.url).catch((error) => { logger.error("[CREATE-ELECTRON-WINDOW]: failed to open browser", { error, }); 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 80c725e17d..0b585f3d65 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 @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { afterRootFrameIsReadyInjectionToken } from "../../runnable-tokens/after-root-frame-is-ready-injection-token"; +import { afterApplicationIsLoadedInjectionToken } from "../../runnable-tokens/after-application-is-loaded-injection-token"; import directoryForKubeConfigsInjectable from "../../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import ensureDirInjectable from "../../../../common/fs/ensure-dir.injectable"; import kubeconfigSyncManagerInjectable from "../../../catalog-sources/kubeconfig-sync/manager.injectable"; @@ -27,7 +27,7 @@ const startKubeConfigSyncInjectable = getInjectable({ causesSideEffects: true, - injectionToken: afterRootFrameIsReadyInjectionToken, + injectionToken: afterApplicationIsLoadedInjectionToken, }); export default startKubeConfigSyncInjectable; diff --git a/src/main/start-main-application/runnables/kube-config-sync/stop-kube-config-sync.injectable.ts b/src/main/start-main-application/runnables/kube-config-sync/stop-kube-config-sync.injectable.ts index 2613ab77ba..6bdf5a7476 100644 --- a/src/main/start-main-application/runnables/kube-config-sync/stop-kube-config-sync.injectable.ts +++ b/src/main/start-main-application/runnables/kube-config-sync/stop-kube-config-sync.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { beforeQuitOfFrontEndInjectionToken } from "../../runnable-tokens/before-quit-of-front-end-injection-token"; +import { beforeQuitOfBackEndInjectionToken } from "../../runnable-tokens/before-quit-of-back-end-injection-token"; import kubeconfigSyncManagerInjectable from "../../../catalog-sources/kubeconfig-sync/manager.injectable"; const stopKubeConfigSyncInjectable = getInjectable({ @@ -19,7 +19,7 @@ const stopKubeConfigSyncInjectable = getInjectable({ }; }, - injectionToken: beforeQuitOfFrontEndInjectionToken, + injectionToken: beforeQuitOfBackEndInjectionToken, }); export default stopKubeConfigSyncInjectable; 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 index 136f869b8f..f1ebe7885d 100644 --- 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 @@ -3,7 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { pipeline } from "@ogre-tools/fp"; -import { kebabCase } from "lodash/fp"; import type { Injectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable"; import { computed } from "mobx"; @@ -16,7 +15,7 @@ import { withErrorSuppression } from "../../../common/utils/with-error-suppressi 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"; import getRandomIdInjectable from "../../../common/utils/get-random-id.injectable"; -import { isBoolean } from "../../../common/utils"; +import { isBoolean, isString } from "../../../common/utils"; const trayMenuItemRegistratorInjectable = getInjectable({ id: "tray-menu-item-registrator", @@ -38,7 +37,7 @@ export default trayMenuItemRegistratorInjectable; const toItemInjectablesFor = (extension: LensMainExtension, withErrorLoggingFor: WithErrorLoggingFor, getRandomId: () => string) => { const _toItemInjectables = (parentId: string | null) => (registration: TrayMenuRegistration): Injectable[] => { - const trayItemId = registration.id || kebabCase(registration.label || getRandomId()); + const trayItemId = registration.id || getRandomId(); const id = `${trayItemId}-tray-menu-item-for-extension-${extension.sanitizedExtensionId}`; const parentInjectable = getInjectable({ @@ -51,7 +50,18 @@ const toItemInjectablesFor = (extension: LensMainExtension, withErrorLoggingFor: separator: registration.type === "separator", - label: computed(() => registration.label || ""), + label: computed(() => { + if (!registration.label) { + return ""; + } + + if (isString(registration.label)) { + return registration.label; + } + + return registration.label.get(); + }), + tooltip: registration.toolTip, click: () => { diff --git a/src/main/tray/tray-menu-registration.ts b/src/main/tray/tray-menu-registration.ts index c192dd89fb..8b316b7b92 100644 --- a/src/main/tray/tray-menu-registration.ts +++ b/src/main/tray/tray-menu-registration.ts @@ -6,7 +6,7 @@ import type { IComputedValue } from "mobx"; export interface TrayMenuRegistration { - label?: string; + label?: string | IComputedValue; click?: (menuItem: TrayMenuRegistration) => void; id?: string; type?: "normal" | "separator" | "submenu"; diff --git a/src/main/utils/shell-env.ts b/src/main/utils/shell-env.ts index 63f3b53159..abcbeb5bed 100644 --- a/src/main/utils/shell-env.ts +++ b/src/main/utils/shell-env.ts @@ -6,7 +6,7 @@ import shellEnvironment from "shell-env"; import logger from "../logger"; -export type EnvironmentVariables = Record; +export type EnvironmentVariables = Partial>; let shellSyncFailed = false; diff --git a/src/renderer/components/+config-autoscalers/hpa-details.tsx b/src/renderer/components/+config-autoscalers/hpa-details.tsx index c265085489..887280d298 100644 --- a/src/renderer/components/+config-autoscalers/hpa-details.tsx +++ b/src/renderer/components/+config-autoscalers/hpa-details.tsx @@ -63,27 +63,13 @@ class NonInjectedHpaDetails extends React.Component - Resource - {metricSpec.name} - {" "} - on Pods - {addition} - - ); + return `Resource ${metricSpec.name} on Pods${addition}`; } case HpaMetricType.Pods: - return ( - <> - {metric.pods.metricName} - {" "} - on Pods - - ); + return `${metric.pods.metricName} on Pods`; case HpaMetricType.Object: { return ( @@ -95,15 +81,7 @@ class NonInjectedHpaDetails extends React.Component - {metric.external.metricName} - {" "} - on - {" "} - {JSON.stringify(metric.external.metricSelector)} - - ); + return `${metric.external.metricName} on ${JSON.stringify(metric.external.metricSelector)}`; } }; diff --git a/src/renderer/components/+config-resource-quotas/resource-quota-details.tsx b/src/renderer/components/+config-resource-quotas/resource-quota-details.tsx index 2443ea60a3..30a7d3496c 100644 --- a/src/renderer/components/+config-resource-quotas/resource-quota-details.tsx +++ b/src/renderer/components/+config-resource-quotas/resource-quota-details.tsx @@ -19,7 +19,7 @@ import logger from "../../../common/logger"; export interface ResourceQuotaDetailsProps extends KubeObjectDetailsProps { } -function transformUnit(name: string, value: string): number { +function transformUnit(name: string, value: string): number | undefined { if (name.includes("memory") || name.includes("storage")) { return unitsToBytes(value); } @@ -36,23 +36,38 @@ function renderQuotas(quota: ResourceQuota): JSX.Element[] { return object.entries(hard) .filter(hasDefinedTupleValue) - .map(([name, value]) => { - const current = transformUnit(name, value); - const max = transformUnit(name, value); - const usage = max === 0 ? 100 : Math.ceil(current / max * 100); // special case 0 max as always 100% usage + .map(([name, rawMax]) => { + const rawCurrent = used[name] ?? "0"; + const current = transformUnit(name, rawCurrent); + const max = transformUnit(name, rawMax); + + if (current === undefined || max === undefined) { + return ( +
+ {name} + + {`${rawCurrent} / ${rawMax}`} + +
+ ); + } + + const usage = max === 0 + ? 100 // special case 0 max as always 100% usage + : current / max * 100; return (
{name} - {`${used[name]} / ${value}`} + {`${rawCurrent} / ${rawMax}`} - {`Set: ${value}. Usage: ${usage}%`} + {`Set: ${rawMax}. Usage: ${+usage.toFixed(2)}%`}

)} /> diff --git a/src/renderer/components/+network-port-forwards/port-forward-menu.tsx b/src/renderer/components/+network-port-forwards/port-forward-menu.tsx index 70b5a0278d..a0edc693a6 100644 --- a/src/renderer/components/+network-port-forwards/port-forward-menu.tsx +++ b/src/renderer/components/+network-port-forwards/port-forward-menu.tsx @@ -6,7 +6,6 @@ import React from "react"; import { autoBind, cssNames } from "../../utils"; import type { PortForwardItem, PortForwardStore } from "../../port-forward"; -import { openPortForward } from "../../port-forward"; import type { MenuActionsProps } from "../menu/menu-actions"; import { MenuActions } from "../menu/menu-actions"; import { MenuItem } from "../menu"; @@ -15,6 +14,8 @@ import { Notifications } from "../notifications"; import { withInjectables } from "@ogre-tools/injectable-react"; import portForwardDialogModelInjectable from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable"; import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable"; +import type { OpenPortForward } from "../../port-forward/open-port-forward.injectable"; +import openPortForwardInjectable from "../../port-forward/open-port-forward.injectable"; export interface PortForwardMenuProps extends MenuActionsProps { portForward: PortForwardItem; @@ -24,6 +25,7 @@ export interface PortForwardMenuProps extends MenuActionsProps { interface Dependencies { portForwardStore: PortForwardStore; openPortForwardDialog: (item: PortForwardItem) => void; + openPortForward: OpenPortForward; } class NonInjectedPortForwardMenu extends React.Component { @@ -94,7 +96,7 @@ class NonInjectedPortForwardMenu { portForward.status === "Active" && ( - openPortForward(portForward)}> + this.props.openPortForward(portForward)}> ({ portForwardStore: di.inject(portForwardStoreInjectable), openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open, + openPortForward: di.inject(openPortForwardInjectable), ...props, }), }, diff --git a/src/renderer/components/+network-services/service-port-component.tsx b/src/renderer/components/+network-services/service-port-component.tsx index e151a32382..79aeff7ec5 100644 --- a/src/renderer/components/+network-services/service-port-component.tsx +++ b/src/renderer/components/+network-services/service-port-component.tsx @@ -13,7 +13,7 @@ import { cssNames } from "../../utils"; import { Notifications } from "../notifications"; import { Button } from "../button"; import type { ForwardedPort, PortForwardStore } from "../../port-forward"; -import { openPortForward, predictProtocol } from "../../port-forward"; +import { predictProtocol } from "../../port-forward"; import { Spinner } from "../spinner"; import { withInjectables } from "@ogre-tools/injectable-react"; import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable"; @@ -21,6 +21,8 @@ import portForwardDialogModelInjectable from "../../port-forward/port-forward-di import logger from "../../../common/logger"; import aboutPortForwardingInjectable from "../../port-forward/about-port-forwarding.injectable"; import notifyErrorPortForwardingInjectable from "../../port-forward/notify-error-port-forwarding.injectable"; +import type { OpenPortForward } from "../../port-forward/open-port-forward.injectable"; +import openPortForwardInjectable from "../../port-forward/open-port-forward.injectable"; export interface ServicePortComponentProps { service: Service; @@ -32,6 +34,7 @@ interface Dependencies { openPortForwardDialog: (item: ForwardedPort, options: { openInBrowser: boolean; onClose: () => void }) => void; aboutPortForwarding: () => void; notifyErrorPortForwarding: (message: string) => void; + openPortForward: OpenPortForward; } @observer @@ -88,7 +91,7 @@ class NonInjectedServicePortComponent extends React.Component this.portForward()}> {port.toString()} -