diff --git a/.github/workflows/bump-master-version.yaml b/.github/workflows/bump-master-version.yaml new file mode 100644 index 0000000000..504473e43f --- /dev/null +++ b/.github/workflows/bump-master-version.yaml @@ -0,0 +1,35 @@ +name: Bump Version on master +on: + pull_request: + types: + - closed + branches: + - master + workflow_dispatch: + +jobs: + release: + name: Release + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release') }} + steps: + - name: Checkout Release from lens + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Install dependencies + run: | + npm i --location=global semver + - name: Bump version to first alpha of next minor version + run: | + NEW_VERSION=$(cat package.json | jq .version --raw-output| xargs semver -i preminor --preid alpha) + cat package.json | jq --arg new_version "$NEW_VERSION" '.version = $new_version' > new-package.json + mv new-package.json package.json + - uses: peter-evans/create-pull-request@v4 + with: + add-paths: package.json + commit-message: Update package.json version to next preminor because of recent release + signoff: true + delete-branch: true + title: Update version to next preminor + labels: chore diff --git a/.github/workflows/mkdocs-manual.yml b/.github/workflows/mkdocs-manual.yml index 56bfd0c831..03ec36da46 100644 --- a/.github/workflows/mkdocs-manual.yml +++ b/.github/workflows/mkdocs-manual.yml @@ -53,9 +53,9 @@ jobs: rm -fr ./docs/clusters ./docs/contributing ./docs/faq ./docs/getting-started ./docs/helm ./docs/support ./docs/supporting sed -i '/Protocol Handlers/d' ./mkdocs.yml sed -i '/IPC/d' ./mkdocs.yml - sed -i 's#../../clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/extensions/get-started/your-first-extension.md - sed -i 's#clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/README.md - sed -i 's#../../contributing/README.md#https://docs.k8slens.dev/latest/contributing/#g' ./docs/extensions/guides/generator.md + sed -i 's#../../clusters/adding-clusters.md#https://docs.k8slens.dev/getting-started/add-cluster/#g' ./docs/extensions/get-started/your-first-extension.md + sed -i 's#clusters/adding-clusters.md#https://docs.k8slens.dev//getting-started/adding-clusters/#g' ./docs/README.md + sed -i 's#../../contributing/README.md#https://docs.k8slens.dev/contributing/#g' ./docs/extensions/guides/generator.md - name: git config run: | diff --git a/.github/workflows/require-milestone.yml b/.github/workflows/require-milestone.yml new file mode 100644 index 0000000000..388aa48013 --- /dev/null +++ b/.github/workflows/require-milestone.yml @@ -0,0 +1,14 @@ +name: Require Milestone +on: + pull_request: + types: [opened, edited, synchronize] +jobs: + milestone: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Require Milestone + run: | + exit $(gh pr view ${{ github.event.pull_request.number }} --json milestone | jq 'if .milestone == null then 1 else 0 end') + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1c745f852d..beb082afe9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,3 @@ # Contributing to Lens -See [Contributing to Lens](https://docs.k8slens.dev/latest/contributing/) documentation. +See [Contributing to Lens](https://docs.k8slens.dev/contributing/) documentation. diff --git a/README.md b/README.md index 0c4a63c174..f581e90401 100644 --- a/README.md +++ b/README.md @@ -19,15 +19,15 @@ Lens IDE a standalone application for MacOS, Windows and Linux operating systems ## Installation -See [Getting Started](https://docs.k8slens.dev/main/getting-started/install-lens/) page. +See [Getting Started](https://docs.k8slens.dev/getting-started/install-lens/) page. ## Development -See [Development](https://docs.k8slens.dev/latest/contributing/development/) page. +See [Development](https://docs.k8slens.dev/contributing/development/) page. ## Contributing -See [Contributing](https://docs.k8slens.dev/latest/contributing/) page. +See [Contributing](https://docs.k8slens.dev/contributing/) page. ## License diff --git a/docs/extensions/get-started/your-first-extension.md b/docs/extensions/get-started/your-first-extension.md index c8c1167943..ec797f7441 100644 --- a/docs/extensions/get-started/your-first-extension.md +++ b/docs/extensions/get-started/your-first-extension.md @@ -78,7 +78,7 @@ npm run dev You must restart Lens for the extension to load. After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds. -With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/main/catalog/) for details on how to add a cluster in Lens IDE. +With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/getting-started/add-cluster/) for details on how to add a cluster in Lens IDE. You will see the "Hello World" page in the left-side cluster menu. ## Develop the Extension diff --git a/docs/extensions/guides/generator.md b/docs/extensions/guides/generator.md index 64838c1bc4..fbc1723b0b 100644 --- a/docs/extensions/guides/generator.md +++ b/docs/extensions/guides/generator.md @@ -46,14 +46,14 @@ Open `my-first-lens-ext/renderer.tsx` and change the value of `title` from `"Hel ```typescript clusterPageMenus = [ - { - target: { pageId: "hello" }, - title: "Hello Lens", - components: { - Icon: ExampleIcon, - } - } -] + { + target: { pageId: "hello" }, + title: "Hello Lens", + components: { + Icon: ExampleIcon, + }, + }, +]; ``` Reload Lens and you will see that the menu item text has changed to "Hello Lens". @@ -70,6 +70,6 @@ To debug your extension, please see our instructions on [Testing Extensions](../ To dive deeper, consider looking at [Common Capabilities](../capabilities/common-capabilities.md), [Styling](../capabilities/styling.md), or [Extension Anatomy](anatomy.md). If you find problems with the Lens Extension Generator, or have feature requests, you are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues). -You can find the latest Lens contribution guidelines [here](https://docs.k8slens.dev/latest/contributing). +You can find the latest Lens contribution guidelines [here](https://docs.k8slens.dev/contributing). The Generator source code is hosted at [GitHub](https://github.com/lensapp/generator-lens-ext). diff --git a/docs/extensions/guides/renderer-extension.md b/docs/extensions/guides/renderer-extension.md index d90a343692..4cc6e338bf 100644 --- a/docs/extensions/guides/renderer-extension.md +++ b/docs/extensions/guides/renderer-extension.md @@ -771,7 +771,7 @@ Construct the table using the `Renderer.Component.Table` and related elements. For each pod the name, age, and status are obtained using the `Renderer.K8sApi.Pod` methods. The table is constructed using the `Renderer.Component.Table` and related elements. -See [Component documentation](https://docs.k8slens.dev/latest/extensions/api/modules/_renderer_api_components_/) for further details. +See [Component documentation](https://api-docs.k8slens.dev/latest/extensions/api/modules/Renderer.Component/) for further details. ### `kubeObjectStatusTexts` diff --git a/jsonnet/lens/custom-prometheus.jsonnet b/jsonnet/lens/custom-prometheus.jsonnet index 9c733766ba..10384fdaa5 100644 --- a/jsonnet/lens/custom-prometheus.jsonnet +++ b/jsonnet/lens/custom-prometheus.jsonnet @@ -24,7 +24,7 @@ }, }, }, - prometheus+:: { + kubernetesControlPlane+:: { serviceMonitorKubelet+: { spec+: { endpoints: std.map(function(endpoint) diff --git a/package.json b/package.json index a61e2e74bb..62803298b2 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "6.1.0", + "version": "6.2.0-alpha.0", "main": "static/build/main.js", "copyright": "© 2022 OpenLens Authors", "license": "MIT", @@ -246,7 +246,7 @@ "history": "^4.10.1", "http-proxy": "^1.18.1", "immer": "^9.0.15", - "joi": "^17.6.2", + "joi": "^17.6.3", "js-yaml": "^4.1.0", "jsdom": "^16.7.0", "lodash": "^4.17.15", @@ -259,7 +259,7 @@ "mobx-utils": "^6.0.4", "mock-fs": "^5.1.4", "moment": "^2.29.4", - "moment-timezone": "^0.5.37", + "moment-timezone": "^0.5.38", "monaco-editor": "^0.29.1", "monaco-editor-webpack-plugin": "^5.0.0", "node-fetch": "^2.6.7", @@ -298,10 +298,10 @@ "@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.7", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.8", "@sentry/types": "^6.19.7", "@swc/cli": "^0.1.57", - "@swc/core": "^1.3.5", + "@swc/core": "^1.3.9", "@swc/jest": "^0.2.23", "@testing-library/dom": "^7.31.2", "@testing-library/jest-dom": "^5.16.5", @@ -331,7 +331,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.64", + "@types/node": "^16.11.68", "@types/node-fetch": "^2.6.2", "@types/npm": "^2.0.32", "@types/proper-lockfile": "^4.1.2", @@ -350,7 +350,7 @@ "@types/semver": "^7.3.12", "@types/sharp": "^0.31.0", "@types/spdy": "^3.4.5", - "@types/tar": "^6.1.2", + "@types/tar": "^6.1.3", "@types/tar-stream": "^2.2.2", "@types/tcp-port-used": "^1.0.1", "@types/tempy": "^0.3.0", @@ -361,8 +361,8 @@ "@types/webpack-dev-server": "^4.7.2", "@types/webpack-env": "^1.18.0", "@types/webpack-node-externals": "^2.5.3", - "@typescript-eslint/eslint-plugin": "^5.39.0", - "@typescript-eslint/parser": "^5.39.0", + "@typescript-eslint/eslint-plugin": "^5.40.1", + "@typescript-eslint/parser": "^5.40.1", "adr": "^1.4.3", "ansi_up": "^5.1.0", "chart.js": "^2.9.4", @@ -374,18 +374,17 @@ "css-loader": "^6.7.1", "deepdash": "^5.3.9", "dompurify": "^2.4.0", - "electron": "^19.1.1", - "electron-builder": "^23.3.3", + "electron": "^19.1.3", + "electron-builder": "^23.6.0", "electron-notarize": "^0.3.0", - "esbuild": "^0.15.10", + "esbuild": "^0.15.12", "esbuild-loader": "^2.20.0", - "eslint": "^8.24.0", + "eslint": "^8.25.0", "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-react": "7.31.8", + "eslint-plugin-react": "7.31.10", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-unused-imports": "^2.0.0", - "flex.box": "^3.4.4", "fork-ts-checker-webpack-plugin": "^6.5.2", "gunzip-maybe": "^1.4.2", "html-webpack-plugin": "^5.5.0", @@ -402,8 +401,8 @@ "node-gyp": "^8.3.0", "node-loader": "^2.0.0", "nodemon": "^2.0.20", - "playwright": "^1.26.1", - "postcss": "^8.4.17", + "playwright": "^1.27.1", + "postcss": "^8.4.18", "postcss-loader": "^6.2.1", "query-string": "^7.1.1", "randomcolor": "^0.6.2", @@ -411,7 +410,7 @@ "react-refresh": "^0.14.0", "react-refresh-typescript": "^2.0.7", "react-router-dom": "^5.3.4", - "react-select": "^5.4.0", + "react-select": "^5.5.4", "react-select-event": "^5.5.1", "react-table": "^7.8.0", "react-window": "^1.8.7", @@ -419,13 +418,13 @@ "sass-loader": "^12.6.0", "sharp": "^0.31.1", "style-loader": "^3.3.1", - "tailwindcss": "^3.1.8", + "tailwindcss": "^3.2.0", "tar-stream": "^2.2.0", "ts-loader": "^9.4.1", "ts-node": "^10.9.1", "type-fest": "^2.14.0", "typed-emitter": "^1.4.0", - "typedoc": "0.23.15", + "typedoc": "0.23.17", "typedoc-plugin-markdown": "^3.13.6", "typescript": "^4.8.4", "typescript-plugin-css-modules": "^3.4.0", diff --git a/src/common/app-paths/app-paths-channel.injectable.ts b/src/common/app-paths/app-paths-channel.injectable.ts deleted file mode 100644 index 99fc738b41..0000000000 --- a/src/common/app-paths/app-paths-channel.injectable.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import 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/app-paths/app-paths-channel.ts b/src/common/app-paths/app-paths-channel.ts new file mode 100644 index 0000000000..4502569d3b --- /dev/null +++ b/src/common/app-paths/app-paths-channel.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { AppPaths } from "./app-path-injection-token"; +import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; + +export type AppPathsChannel = RequestChannel; + +export const appPathsChannel: AppPathsChannel = { + id: "app-paths", +}; + diff --git a/src/common/application-update/restart-and-install-update-channel.ts b/src/common/application-update/restart-and-install-update-channel.ts new file mode 100644 index 0000000000..1d45d6330f --- /dev/null +++ b/src/common/application-update/restart-and-install-update-channel.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; + +export type RestartAndInstallUpdateChannel = MessageChannel; + +export const restartAndInstallUpdateChannel: RestartAndInstallUpdateChannel = { + id: "restart-and-install-update-channel", +}; diff --git a/src/common/application-update/restart-and-install-update-channel/restart-and-install-update-channel.injectable.ts b/src/common/application-update/restart-and-install-update-channel/restart-and-install-update-channel.injectable.ts deleted file mode 100644 index b6d811e101..0000000000 --- a/src/common/application-update/restart-and-install-update-channel/restart-and-install-update-channel.injectable.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. - */ -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 RestartAndInstallUpdateChannel = MessageChannel; - -const restartAndInstallUpdateChannel = getInjectable({ - id: "restart-and-install-update-channel", - - instantiate: (): RestartAndInstallUpdateChannel => ({ - id: "restart-and-install-update-channel", - }), - - injectionToken: messageChannelInjectionToken, -}); - -export default restartAndInstallUpdateChannel; 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 index 8ca31e00aa..d11778b9bd 100644 --- 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 @@ -19,7 +19,7 @@ const selectedUpdateChannelInjectable = getInjectable({ instantiate: (di): SelectedUpdateChannel => { const defaultUpdateChannel = di.inject(defaultUpdateChannelInjectable); - const state = observable.box(); + const state = observable.box(undefined, { deep: false }); return { value: computed(() => state.get() ?? defaultUpdateChannel.get()), diff --git a/src/common/fetch/download-binary.injectable.ts b/src/common/fetch/download-binary.injectable.ts new file mode 100644 index 0000000000..27ef43d59b --- /dev/null +++ b/src/common/fetch/download-binary.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 type { RequestInit, Response } from "node-fetch"; +import type { AsyncResult } from "../utils/async-result"; +import fetchInjectable from "./fetch.injectable"; + +export interface DownloadBinaryOptions { + signal?: AbortSignal | null | undefined; +} + +export type DownloadBinary = (url: string, opts?: DownloadBinaryOptions) => Promise>; + +const downloadBinaryInjectable = getInjectable({ + id: "download-binary", + instantiate: (di): DownloadBinary => { + const fetch = di.inject(fetchInjectable); + + return async (url, opts) => { + let result: Response; + + try { + // TODO: upgrade node-fetch once we switch to ESM + result = await fetch(url, opts as RequestInit); + } catch (error) { + return { + callWasSuccessful: false, + error: String(error), + }; + } + + if (result.status < 200 || 300 <= result.status) { + return { + callWasSuccessful: false, + error: result.statusText, + }; + } + + try { + return { + callWasSuccessful: true, + response: await result.buffer(), + }; + } catch (error) { + return { + callWasSuccessful: false, + error: String(error), + }; + } + }; + }, +}); + +export default downloadBinaryInjectable; diff --git a/src/common/fetch/download-json.injectable.ts b/src/common/fetch/download-json.injectable.ts new file mode 100644 index 0000000000..503cd373ec --- /dev/null +++ b/src/common/fetch/download-json.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 type { RequestInit, Response } from "node-fetch"; +import type { JsonValue } from "type-fest"; +import type { AsyncResult } from "../utils/async-result"; +import fetchInjectable from "./fetch.injectable"; + +export interface DownloadJsonOptions { + signal?: AbortSignal | null | undefined; +} + +export type DownloadJson = (url: string, opts?: DownloadJsonOptions) => Promise>; + +const downloadJsonInjectable = getInjectable({ + id: "download-json", + instantiate: (di): DownloadJson => { + const fetch = di.inject(fetchInjectable); + + return async (url, opts) => { + let result: Response; + + try { + result = await fetch(url, opts as RequestInit); + } catch (error) { + return { + callWasSuccessful: false, + error: String(error), + }; + } + + if (result.status < 200 || 300 <= result.status) { + return { + callWasSuccessful: false, + error: result.statusText, + }; + } + + try { + return { + callWasSuccessful: true, + response: await result.json(), + }; + } catch (error) { + return { + callWasSuccessful: false, + error: String(error), + }; + } + }; + }, +}); + +export default downloadJsonInjectable; diff --git a/src/common/fetch/fetch.injectable.ts b/src/common/fetch/fetch.injectable.ts index c6d2a7e1af..c4c30bc2d8 100644 --- a/src/common/fetch/fetch.injectable.ts +++ b/src/common/fetch/fetch.injectable.ts @@ -3,10 +3,10 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { RequestInfo, RequestInit, Response } from "node-fetch"; import fetch from "node-fetch"; +import type { RequestInit, Response } from "node-fetch"; -export type Fetch = (url: RequestInfo, init?: RequestInit) => Promise; +export type Fetch = (url: string, init?: RequestInit) => Promise; const fetchInjectable = getInjectable({ id: "fetch", diff --git a/src/common/fetch/timeout-controller.ts b/src/common/fetch/timeout-controller.ts new file mode 100644 index 0000000000..702becdc9d --- /dev/null +++ b/src/common/fetch/timeout-controller.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +/** + * Creates an AbortController with an associated timeout + * @param timeout The number of milliseconds before this controller will auto abort + */ +export function withTimeout(timeout: number): AbortController { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + + controller.signal.addEventListener("abort", () => clearTimeout(id)); + + return controller; +} diff --git a/src/common/front-end-routing/app-navigation-channel.injectable.ts b/src/common/front-end-routing/app-navigation-channel.injectable.ts deleted file mode 100644 index 869fbfdecd..0000000000 --- a/src/common/front-end-routing/app-navigation-channel.injectable.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { 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/app-navigation-channel.ts b/src/common/front-end-routing/app-navigation-channel.ts new file mode 100644 index 0000000000..e0b881eff9 --- /dev/null +++ b/src/common/front-end-routing/app-navigation-channel.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; +import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; + +export type AppNavigationChannel = MessageChannel; + +export const appNavigationChannel: AppNavigationChannel = { + id: IpcRendererNavigationEvents.NAVIGATE_IN_APP, +}; 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 deleted file mode 100644 index 596bd6d351..0000000000 --- a/src/common/front-end-routing/cluster-frame-navigation-channel.injectable.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { 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/cluster-frame-navigation-channel.ts b/src/common/front-end-routing/cluster-frame-navigation-channel.ts new file mode 100644 index 0000000000..9d9a1904af --- /dev/null +++ b/src/common/front-end-routing/cluster-frame-navigation-channel.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; +import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; + +export type ClusterFrameNavigationChannel = MessageChannel; + +export const clusterFrameNavigationChannel: ClusterFrameNavigationChannel = { + id: IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER, +}; diff --git a/src/common/front-end-routing/routes/cluster/config/leases/leases-route.injectable.ts b/src/common/front-end-routing/routes/cluster/config/leases/leases-route.injectable.ts new file mode 100644 index 0000000000..65ee0e3ffa --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/leases/leases-route.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 isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token"; + +const leasesRouteInjectable = getInjectable({ + id: "leases", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "leases"); + + return { + path: "/leases", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: frontEndRouteInjectionToken, +}); + +export default leasesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/leases/navigate-to-leases.injectable.ts b/src/common/front-end-routing/routes/cluster/config/leases/navigate-to-leases.injectable.ts new file mode 100644 index 0000000000..5bf7d74ff1 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/leases/navigate-to-leases.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 leasesRouteInjectable from "./leases-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToLeasesInjectable = getInjectable({ + id: "navigate-to-leases", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(leasesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToLeasesInjectable; diff --git a/src/common/fs/exec-file.injectable.ts b/src/common/fs/exec-file.injectable.ts index 15d0ad48dc..8db31c602f 100644 --- a/src/common/fs/exec-file.injectable.ts +++ b/src/common/fs/exec-file.injectable.ts @@ -5,22 +5,33 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { ExecFileOptions } from "child_process"; import { execFile } from "child_process"; -import { promisify } from "util"; +import type { AsyncResult } from "../utils/async-result"; -export type ExecFile = (filePath: string, args: string[], options: ExecFileOptions) => Promise; +export interface ExecFile { + (filePath: string, args: string[], options: ExecFileOptions): Promise>; +} const execFileInjectable = getInjectable({ id: "exec-file", - instantiate: (): ExecFile => { - const asyncExecFile = promisify(execFile); - - return async (filePath, args, options) => { - const result = await asyncExecFile(filePath, args, options); - - return result.stdout; - }; - }, + instantiate: (): ExecFile => (filePath, args, options) => new Promise((resolve) => { + execFile(filePath, args, options, (error, stdout, stderr) => { + if (error) { + resolve({ + callWasSuccessful: false, + error: { + error, + stderr, + }, + }); + } else { + resolve({ + callWasSuccessful: true, + response: stdout, + }); + } + }); + }), causesSideEffects: true, }); diff --git a/src/common/helm/add-helm-repository-channel.injectable.ts b/src/common/helm/add-helm-repository-channel.ts similarity index 50% rename from src/common/helm/add-helm-repository-channel.injectable.ts rename to src/common/helm/add-helm-repository-channel.ts index 0bc10564ad..44471fe426 100644 --- a/src/common/helm/add-helm-repository-channel.injectable.ts +++ b/src/common/helm/add-helm-repository-channel.ts @@ -2,22 +2,12 @@ * 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 { HelmRepo } from "./helm-repo"; -import type { RequestChannel } from "../utils/channel/request-channel-injection-token"; -import { requestChannelInjectionToken } from "../utils/channel/request-channel-injection-token"; import type { AsyncResult } from "../utils/async-result"; +import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; export type AddHelmRepositoryChannel = RequestChannel>; -const addHelmRepositoryChannelInjectable = getInjectable({ +export const addHelmRepositoryChannel: AddHelmRepositoryChannel = { id: "add-helm-repository-channel", - - instantiate: (): AddHelmRepositoryChannel => ({ - id: "add-helm-repository-channel", - }), - - injectionToken: requestChannelInjectionToken, -}); - -export default addHelmRepositoryChannelInjectable; +}; diff --git a/src/common/helm/get-active-helm-repositories-channel.injectable.ts b/src/common/helm/get-active-helm-repositories-channel.injectable.ts deleted file mode 100644 index ac7355ace7..0000000000 --- a/src/common/helm/get-active-helm-repositories-channel.injectable.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import type { RequestChannel } from "../utils/channel/request-channel-injection-token"; -import type { HelmRepo } from "./helm-repo"; -import { requestChannelInjectionToken } from "../utils/channel/request-channel-injection-token"; -import type { AsyncResult } from "../utils/async-result"; - -export type GetHelmRepositoriesChannel = RequestChannel>; - -const getActiveHelmRepositoriesChannelInjectable = getInjectable({ - id: "get-active-helm-repositories-channel", - - instantiate: (): GetHelmRepositoriesChannel => ({ - id: "get-helm-active-list-repositories", - }), - - injectionToken: requestChannelInjectionToken, -}); - -export default getActiveHelmRepositoriesChannelInjectable; diff --git a/src/common/helm/get-active-helm-repositories-channel.ts b/src/common/helm/get-active-helm-repositories-channel.ts new file mode 100644 index 0000000000..26720e9a5f --- /dev/null +++ b/src/common/helm/get-active-helm-repositories-channel.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { HelmRepo } from "./helm-repo"; +import type { AsyncResult } from "../utils/async-result"; +import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; + +export type GetActiveHelmRepositoriesChannel = RequestChannel>; + +export const getActiveHelmRepositoriesChannel: GetActiveHelmRepositoriesChannel = { + id: "get-helm-active-list-repositories", +}; diff --git a/src/common/helm/remove-helm-repository-channel.injectable.ts b/src/common/helm/remove-helm-repository-channel.injectable.ts deleted file mode 100644 index eddecb1ccd..0000000000 --- a/src/common/helm/remove-helm-repository-channel.injectable.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import type { HelmRepo } from "./helm-repo"; -import type { RequestChannel } from "../utils/channel/request-channel-injection-token"; -import { requestChannelInjectionToken } from "../utils/channel/request-channel-injection-token"; - -export type RemoveHelmRepositoryChannel = RequestChannel; - -const removeHelmRepositoryChannelInjectable = getInjectable({ - id: "remove-helm-repository-channel", - - instantiate: (): RemoveHelmRepositoryChannel => ({ - id: "remove-helm-repository-channel", - }), - - injectionToken: requestChannelInjectionToken, -}); - -export default removeHelmRepositoryChannelInjectable; diff --git a/src/common/helm/remove-helm-repository-channel.ts b/src/common/helm/remove-helm-repository-channel.ts new file mode 100644 index 0000000000..7eda160159 --- /dev/null +++ b/src/common/helm/remove-helm-repository-channel.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { AsyncResult } from "../utils/async-result"; +import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; +import type { HelmRepo } from "./helm-repo"; + +export type RemoveHelmRepositoryChannel = RequestChannel>; + +export const removeHelmRepositoryChannel: RemoveHelmRepositoryChannel = { + id: "remove-helm-repository-channel", +}; diff --git a/src/common/k8s-api/endpoints/index.ts b/src/common/k8s-api/endpoints/index.ts index e13a969faa..c0493e6370 100644 --- a/src/common/k8s-api/endpoints/index.ts +++ b/src/common/k8s-api/endpoints/index.ts @@ -19,6 +19,7 @@ export * from "./events.api"; export * from "./horizontal-pod-autoscaler.api"; export * from "./ingress.api"; export * from "./job.api"; +export * from "./lease.api"; export * from "./limit-range.api"; export * from "./namespace.api"; export * from "./network-policy.api"; diff --git a/src/common/k8s-api/endpoints/lease.api.injectable.ts b/src/common/k8s-api/endpoints/lease.api.injectable.ts new file mode 100644 index 0000000000..41bd5e3935 --- /dev/null +++ b/src/common/k8s-api/endpoints/lease.api.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 assert from "assert"; +import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token"; +import { LeaseApi } from "./lease.api"; +import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token"; + +const leaseApiInjectable = getInjectable({ + id: "lease-api", + instantiate: (di) => { + assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "leaseApi is only available in certain environments"); + + return new LeaseApi(); + }, + + injectionToken: kubeApiInjectionToken, +}); + +export default leaseApiInjectable; diff --git a/src/common/k8s-api/endpoints/lease.api.ts b/src/common/k8s-api/endpoints/lease.api.ts new file mode 100644 index 0000000000..22cfb778e4 --- /dev/null +++ b/src/common/k8s-api/endpoints/lease.api.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 type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api"; +import { KubeApi } from "../kube-api"; +import type { NamespaceScopedMetadata } from "../kube-object"; +import { KubeObject } from "../kube-object"; + +export interface LeaseSpec { + acquireTime?: string; + holderIdentity: string; + leaseDurationSeconds: number; + leaseTransitions?: number; + renewTime: string; +} + +export class Lease extends KubeObject< + NamespaceScopedMetadata, + void, + LeaseSpec +> { + static readonly kind = "Lease"; + static readonly namespaced = true; + static readonly apiBase = "/apis/coordination.k8s.io/v1/leases"; + + getAcquireTime(): string { + return this.spec.acquireTime || ""; + } + + getHolderIdentity(): string { + return this.spec.holderIdentity; + } + + getLeaseDurationSeconds(): number { + return this.spec.leaseDurationSeconds; + } + + getLeaseTransitions(): number | undefined { + return this.spec.leaseTransitions; + } + + getRenewTime(): string { + return this.spec.renewTime; + } +} + +export class LeaseApi extends KubeApi { + constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) { + super({ + ...opts, + objectConstructor: Lease, + }); + } +} diff --git a/src/common/rbac.ts b/src/common/rbac.ts index 71c636613a..bfa04ef46b 100644 --- a/src/common/rbac.ts +++ b/src/common/rbac.ts @@ -4,7 +4,7 @@ */ export type KubeResource = - "namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" | + "namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" | "leases" | "secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" | "pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "jobs" | "cronjobs" | "endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets" | @@ -35,6 +35,7 @@ export const apiResourceRecord: Record = { "jobs": { kind: "Job", group: "batch" }, "namespaces": { kind: "Namespace" }, "limitranges": { kind: "LimitRange" }, + "leases": { kind: "Lease" }, "networkpolicies": { kind: "NetworkPolicy", group: "networking.k8s.io" }, "nodes": { kind: "Node" }, "persistentvolumes": { kind: "PersistentVolume" }, 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 deleted file mode 100644 index a7787c6cc4..0000000000 --- a/src/common/root-frame-rendered-channel/root-frame-rendered-channel.injectable.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. - */ -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/root-frame/root-frame-rendered-channel.ts b/src/common/root-frame/root-frame-rendered-channel.ts new file mode 100644 index 0000000000..060ae8735c --- /dev/null +++ b/src/common/root-frame/root-frame-rendered-channel.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; + +export type RootFrameHasRenderedChannel = MessageChannel; + +export const rootFrameHasRenderedChannel: RootFrameHasRenderedChannel = { + id: "root-frame-rendered", +}; diff --git a/src/common/user-store/resolved-shell.injectable.ts b/src/common/user-store/resolved-shell.injectable.ts new file mode 100644 index 0000000000..98c219feff --- /dev/null +++ b/src/common/user-store/resolved-shell.injectable.ts @@ -0,0 +1,13 @@ +/** + * 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 userStoreInjectable from "./user-store.injectable"; + +const resolvedShellInjectable = getInjectable({ + id: "resolved-shell", + instantiate: (di) => di.inject(userStoreInjectable).resolvedShell, +}); + +export default resolvedShellInjectable; diff --git a/src/common/utils/camelCase.ts b/src/common/utils/camelCase.ts index 3ef727a2e5..a37e4c7f5c 100644 --- a/src/common/utils/camelCase.ts +++ b/src/common/utils/camelCase.ts @@ -6,7 +6,8 @@ // Convert object's keys to camelCase format import { camelCase } from "lodash"; import type { SingleOrMany } from "./types"; -import { isObject } from "./type-narrowing"; +import { isObject, isString } from "./type-narrowing"; +import * as object from "./objects"; export function toCamelCase[]>(obj: T): T; export function toCamelCase>(obj: T): T; @@ -17,11 +18,10 @@ export function toCamelCase(obj: SingleOrMany | unknown> } if (isObject(obj)) { - return Object.fromEntries( - Object.entries(obj) - .map(([key, value]) => { - return [camelCase(key), toCamelCase(value)]; - }), + return object.fromEntries( + object.entries(obj) + .filter((pair): pair is [string, unknown] => isString(pair[0])) + .map(([key, value]) => [camelCase(key), isObject(value) ? toCamelCase(value) : value]), ); } diff --git a/src/common/utils/channel/channel.test.ts b/src/common/utils/channel/channel.test.ts index c5f52c0874..9a361b6770 100644 --- a/src/common/utils/channel/channel.test.ts +++ b/src/common/utils/channel/channel.test.ts @@ -4,31 +4,31 @@ */ import type { DiContainer } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable"; -import type { MessageToChannel } from "./message-to-channel-injection-token"; -import { messageToChannelInjectionToken } from "./message-to-channel-injection-token"; +import type { SendMessageToChannel } from "./message-to-channel-injection-token"; +import { sendMessageToChannelInjectionToken } from "./message-to-channel-injection-token"; import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable"; +import type { MessageChannel } from "./message-channel-listener-injection-token"; import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token"; -import type { 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 { RequestChannel } 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"; import { runInAction } from "mobx"; +import type { RequestChannelHandler } from "../../../main/utils/channel/channel-listeners/listener-tokens"; +import { getRequestChannelListenerInjectable } from "../../../main/utils/channel/channel-listeners/listener-tokens"; type TestMessageChannel = MessageChannel; type TestRequestChannel = RequestChannel; describe("channel", () => { describe("messaging from main to renderer, given listener for channel in a window and application has started", () => { - let testMessageChannel: TestMessageChannel; let messageListenerInWindowMock: jest.Mock; let mainDi: DiContainer; - let messageToChannel: MessageToChannel; + let messageToChannel: SendMessageToChannel; let builder: ApplicationBuilder; beforeEach(async () => { @@ -39,24 +39,17 @@ describe("channel", () => { const testChannelListenerInTestWindowInjectable = getInjectable({ id: "test-channel-listener-in-test-window", - instantiate: (di) => ({ - channel: di.inject(testMessageChannelInjectable), + instantiate: () => ({ + channel: testMessageChannel, handler: messageListenerInWindowMock, }), injectionToken: messageChannelListenerInjectionToken, }); - builder.beforeApplicationStart((mainDi) => { - runInAction(() => { - mainDi.register(testMessageChannelInjectable); - }); - }); - builder.beforeWindowStart((windowDi) => { runInAction(() => { windowDi.register(testChannelListenerInTestWindowInjectable); - windowDi.register(testMessageChannelInjectable); }); }); @@ -64,8 +57,7 @@ describe("channel", () => { await builder.startHidden(); - testMessageChannel = mainDi.inject(testMessageChannelInjectable); - messageToChannel = mainDi.inject(messageToChannelInjectionToken); + messageToChannel = mainDi.inject(sendMessageToChannelInjectionToken); }); describe("given window is started", () => { @@ -109,9 +101,8 @@ describe("channel", () => { }); 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 messageToChannel: MessageToChannel; + let messageToChannel: SendMessageToChannel; beforeEach(async () => { const applicationBuilder = getApplicationBuilder(); @@ -121,9 +112,8 @@ describe("channel", () => { const testChannelListenerInMainInjectable = getInjectable({ id: "test-channel-listener-in-main", - instantiate: (di) => ({ - channel: di.inject(testMessageChannelInjectable), - + instantiate: () => ({ + channel: testMessageChannel, handler: messageListenerInMainMock, }), @@ -133,13 +123,6 @@ describe("channel", () => { applicationBuilder.beforeApplicationStart((mainDi) => { runInAction(() => { mainDi.register(testChannelListenerInMainInjectable); - mainDi.register(testMessageChannelInjectable); - }); - }); - - applicationBuilder.beforeWindowStart((windowDi) => { - runInAction(() => { - windowDi.register(testMessageChannelInjectable); }); }); @@ -147,8 +130,7 @@ describe("channel", () => { const windowDi = applicationBuilder.applicationWindow.only.di; - testMessageChannel = windowDi.inject(testMessageChannelInjectable); - messageToChannel = windowDi.inject(messageToChannelInjectionToken); + messageToChannel = windowDi.inject(sendMessageToChannelInjectionToken); }); it("when sending message, triggers listener in main", () => { @@ -159,8 +141,7 @@ describe("channel", () => { }); 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 requestListenerInMainMock: AsyncFnMock>; let requestFromChannel: RequestFromChannel; beforeEach(async () => { @@ -168,28 +149,14 @@ describe("channel", () => { requestListenerInMainMock = asyncFn(); - const testChannelListenerInMainInjectable = getInjectable({ - id: "test-channel-listener-in-main", - - instantiate: (di) => ({ - channel: di.inject(testRequestChannelInjectable), - - handler: requestListenerInMainMock, - }), - - injectionToken: requestChannelListenerInjectionToken, + const testChannelListenerInMainInjectable = getRequestChannelListenerInjectable({ + channel: testRequestChannel, + handler: () => requestListenerInMainMock, }); applicationBuilder.beforeApplicationStart((mainDi) => { runInAction(() => { mainDi.register(testChannelListenerInMainInjectable); - mainDi.register(testRequestChannelInjectable); - }); - }); - - applicationBuilder.beforeWindowStart((windowDi) => { - runInAction(() => { - windowDi.register(testRequestChannelInjectable); }); }); @@ -197,8 +164,6 @@ describe("channel", () => { const windowDi = applicationBuilder.applicationWindow.only.di; - testRequestChannel = windowDi.inject(testRequestChannelInjectable); - requestFromChannel = windowDi.inject( requestFromChannelInjectionToken, ); @@ -230,21 +195,37 @@ describe("channel", () => { }); }); }); + + it("when registering multiple handlers for the same channel, throws", async () => { + const applicationBuilder = getApplicationBuilder(); + + const testChannelListenerInMainInjectable = getRequestChannelListenerInjectable({ + channel: testRequestChannel, + handler: () => () => "some-value", + }); + const testChannelListenerInMain2Injectable = getRequestChannelListenerInjectable({ + channel: testRequestChannel, + handler: () => () => "some-other-value", + }); + + testChannelListenerInMain2Injectable.id += "2"; + + applicationBuilder.beforeApplicationStart((mainDi) => { + runInAction(() => { + mainDi.register(testChannelListenerInMainInjectable); + mainDi.register(testChannelListenerInMain2Injectable); + }); + }); + + await expect(applicationBuilder.render()).rejects.toThrow('Tried to register a multiple channel handlers for "some-request-channel-id", only one handler is supported for a request channel.'); + }); }); -const testMessageChannelInjectable = getInjectable({ - id: "some-message-test-channel", +const testMessageChannel: TestMessageChannel = { + id: "some-message-channel-id", +}; - instantiate: (): TestMessageChannel => ({ - id: "some-message-channel-id", - }), -}); - -const testRequestChannelInjectable = getInjectable({ - id: "some-request-test-channel", - - instantiate: (): TestRequestChannel => ({ - id: "some-request-channel-id", - }), -}); +const testRequestChannel: TestRequestChannel = { + id: "some-request-channel-id", +}; 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 index fa6983e130..34f62d51d5 100644 --- a/src/common/utils/channel/enlist-message-channel-listener-injection-token.ts +++ b/src/common/utils/channel/enlist-message-channel-listener-injection-token.ts @@ -3,14 +3,11 @@ * 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"; +import type { Disposer } from "../disposer"; +import type { MessageChannel, MessageChannelListener } from "./message-channel-listener-injection-token"; -export type EnlistMessageChannelListener = < - TChannel extends MessageChannel, ->(listener: MessageChannelListener) => () => void; +export type EnlistMessageChannelListener = (listener: MessageChannelListener>) => Disposer; -export const enlistMessageChannelListenerInjectionToken = - getInjectionToken({ - id: "enlist-message-channel-listener", - }); +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 deleted file mode 100644 index f87082c466..0000000000 --- a/src/common/utils/channel/enlist-request-channel-listener-injection-token.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 { 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 deleted file mode 100644 index 30fee42fb9..0000000000 --- a/src/common/utils/channel/listening-of-channels.injectable.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { 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/listening-on-message-channels.injectable.ts b/src/common/utils/channel/listening-on-message-channels.injectable.ts new file mode 100644 index 0000000000..afe0c08f24 --- /dev/null +++ b/src/common/utils/channel/listening-on-message-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 { getStartableStoppable } from "../get-startable-stoppable"; +import { disposer } from "../index"; +import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token"; +import { enlistMessageChannelListenerInjectionToken } from "./enlist-message-channel-listener-injection-token"; + +const listeningOnMessageChannelsInjectable = getInjectable({ + id: "listening-on-message-channels", + + instantiate: (di) => { + const enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken); + const messageChannelListeners = di.injectMany(messageChannelListenerInjectionToken); + + return getStartableStoppable("listening-on-channels", () => ( + disposer(messageChannelListeners.map(enlistMessageChannelListener)) + )); + }, +}); + + +export default listeningOnMessageChannelsInjectable; diff --git a/src/common/utils/channel/message-channel-injection-token.ts b/src/common/utils/channel/message-channel-injection-token.ts deleted file mode 100644 index 3141acedf3..0000000000 --- a/src/common/utils/channel/message-channel-injection-token.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 { 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 index 8879e19013..5bfc45a82d 100644 --- a/src/common/utils/channel/message-channel-listener-injection-token.ts +++ b/src/common/utils/channel/message-channel-listener-injection-token.ts @@ -2,17 +2,50 @@ * 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"; +import type { DiContainerForInjection } from "@ogre-tools/injectable"; +import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; -export interface MessageChannelListener> { - channel: TChannel; - handler: (value: SetRequired["_messageSignature"]) => void; +export interface MessageChannel { + id: string; + _messageSignature?: Message; // only used to mark `Message` as used } -export const messageChannelListenerInjectionToken = getInjectionToken>>( +export type MessageChannelHandler = Channel extends MessageChannel + ? (message: Message) => void + : never; + +export interface MessageChannelListener { + channel: Channel; + handler: MessageChannelHandler; +} + +export const messageChannelListenerInjectionToken = getInjectionToken>>( { id: "message-channel-listener", }, ); + +export interface GetMessageChannelListenerInfo< + Channel extends MessageChannel, + Message, +> { + id: string; + channel: Channel; + handler: (di: DiContainerForInjection) => MessageChannelHandler; + causesSideEffects?: boolean; +} + +export function getMessageChannelListenerInjectable< + Channel extends MessageChannel, + Message, +>(info: GetMessageChannelListenerInfo) { + return getInjectable({ + id: `${info.channel.id}-listener-${info.id}`, + instantiate: (di) => ({ + channel: info.channel, + handler: info.handler(di), + }), + injectionToken: messageChannelListenerInjectionToken, + causesSideEffects: info.causesSideEffects, + }); +} diff --git a/src/common/utils/channel/message-to-channel-injection-token.ts b/src/common/utils/channel/message-to-channel-injection-token.ts index 8c5f03b9ee..d179ab9870 100644 --- a/src/common/utils/channel/message-to-channel-injection-token.ts +++ b/src/common/utils/channel/message-to-channel-injection-token.ts @@ -3,21 +3,13 @@ * 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"; +import type { MessageChannel } from "./message-channel-listener-injection-token"; -export interface MessageToChannel { - , TMessage extends void>( - channel: TChannel, - ): void; - - >( - channel: TChannel, - message: SetRequired["_messageSignature"], - ): void; +export interface SendMessageToChannel { + (channel: MessageChannel): void; + (channel: MessageChannel, message: Message): void; } -export const messageToChannelInjectionToken = - getInjectionToken({ - id: "message-to-message-channel", - }); +export const sendMessageToChannelInjectionToken = getInjectionToken({ + id: "send-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 deleted file mode 100644 index 67044db878..0000000000 --- a/src/common/utils/channel/request-channel-injection-token.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 { 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 index 690b96d9dc..2f0b84a3cc 100644 --- a/src/common/utils/channel/request-channel-listener-injection-token.ts +++ b/src/common/utils/channel/request-channel-listener-injection-token.ts @@ -2,24 +2,9 @@ * 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 interface RequestChannel { + id: string; + _requestSignature?: Request; // used only to mark `Request` as "used" + _responseSignature?: Response; // used only to mark `Response` as "used" } - -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 index f4d4fdbe0c..14e925f190 100644 --- a/src/common/utils/channel/request-from-channel-injection-token.ts +++ b/src/common/utils/channel/request-from-channel-injection-token.ts @@ -3,19 +3,13 @@ * 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"; +import type { RequestChannel } from "./request-channel-listener-injection-token"; -export type RequestFromChannel = < - TChannel extends RequestChannel, ->( - channel: TChannel, - ...request: TChannel["_requestSignature"] extends void - ? [] - : [SetRequired["_requestSignature"]] -) => Promise["_responseSignature"]>; +export interface RequestFromChannel { + (channel: RequestChannel, request: Request): Promise; + (channel: RequestChannel): Promise; +} -export const requestFromChannelInjectionToken = - getInjectionToken({ - id: "request-from-request-channel", - }); +export const requestFromChannelInjectionToken = getInjectionToken({ + id: "request-from-request-channel", +}); diff --git a/src/common/utils/disposer.ts b/src/common/utils/disposer.ts index 05a349ab4d..2949a7ae35 100644 --- a/src/common/utils/disposer.ts +++ b/src/common/utils/disposer.ts @@ -3,6 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +import type { SingleOrMany } from "./types"; + export interface Disposer { @@ -17,9 +19,9 @@ export interface ExtendableDisposer extends Disposer { push(...vals: (Disposer | ExtendableDisposer | Disposable)[]): void; } -export function disposer(...items: (Disposer | Disposable | undefined | null)[]): ExtendableDisposer { +export function disposer(...items: SingleOrMany[]): ExtendableDisposer { return Object.assign(() => { - for (const item of items) { + for (const item of items.flat()) { if (!item) { continue; } diff --git a/src/common/utils/downloadFile.ts b/src/common/utils/downloadFile.ts deleted file mode 100644 index 5f3e658aa6..0000000000 --- a/src/common/utils/downloadFile.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import request from "request"; -import type { JsonValue } from "type-fest"; -import { parse } from "./json"; - -export interface DownloadFileOptions { - url: string; - gzip?: boolean; - timeout?: number; -} - -export interface DownloadFileTicket { - url: string; - promise: Promise; - cancel(): void; -} - -export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions): DownloadFileTicket { - const fileChunks: Buffer[] = []; - const req = request(url, { gzip, timeout }); - const promise: Promise = new Promise((resolve, reject) => { - req.on("data", (chunk: Buffer) => { - fileChunks.push(chunk); - }); - req.once("error", err => { - reject({ url, err }); - }); - req.once("complete", () => { - resolve(Buffer.concat(fileChunks)); - }); - }); - - return { - url, - promise, - cancel() { - req.abort(); - }, - }; -} - -export function downloadJson(args: DownloadFileOptions): DownloadFileTicket { - const { promise, ...rest } = downloadFile(args); - - return { - promise: promise.then(res => parse(res.toString())), - ...rest, - }; -} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index a9acaede86..36e77c6e79 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -14,7 +14,6 @@ export * from "./convertMemory"; export * from "./debouncePromise"; export * from "./delay"; export * from "./disposer"; -export * from "./downloadFile"; export * from "./escapeRegExp"; export * from "./formatDuration"; export * from "./getRandId"; diff --git a/src/common/utils/resolve-system-proxy/resolve-system-proxy-channel.injectable.ts b/src/common/utils/resolve-system-proxy/resolve-system-proxy-channel.injectable.ts deleted file mode 100644 index 5dd17ddc6a..0000000000 --- a/src/common/utils/resolve-system-proxy/resolve-system-proxy-channel.injectable.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. - */ -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 ResolveSystemProxyChannel = RequestChannel; - -const resolveSystemProxyChannelInjectable = getInjectable({ - id: "resolve-system-proxy-channel", - - instantiate: (): ResolveSystemProxyChannel => ({ - id: "resolve-system-proxy-channel", - }), - - injectionToken: requestChannelInjectionToken, -}); - -export default resolveSystemProxyChannelInjectable; diff --git a/src/common/utils/resolve-system-proxy/resolve-system-proxy-channel.ts b/src/common/utils/resolve-system-proxy/resolve-system-proxy-channel.ts new file mode 100644 index 0000000000..c823a8a8f9 --- /dev/null +++ b/src/common/utils/resolve-system-proxy/resolve-system-proxy-channel.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { RequestChannel } from "../channel/request-channel-listener-injection-token"; + +export type ResolveSystemProxyChannel = RequestChannel; + +export const resolveSystemProxyChannel: ResolveSystemProxyChannel = { + id: "resolve-system-proxy-channel", +}; diff --git a/src/common/utils/sync-box/channel-listener.injectable.ts b/src/common/utils/sync-box/channel-listener.injectable.ts new file mode 100644 index 0000000000..a97d95d726 --- /dev/null +++ b/src/common/utils/sync-box/channel-listener.injectable.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 { syncBoxChannel } from "./channels"; +import { getMessageChannelListenerInjectable } from "../channel/message-channel-listener-injection-token"; +import syncBoxStateInjectable from "./sync-box-state.injectable"; + +const syncBoxChannelListenerInjectable = getMessageChannelListenerInjectable({ + id: "init", + channel: syncBoxChannel, + handler: (di) => ({ id, value }) => di.inject(syncBoxStateInjectable, id).set(value), +}); + +export default syncBoxChannelListenerInjectable; diff --git a/src/common/utils/sync-box/channels.ts b/src/common/utils/sync-box/channels.ts new file mode 100644 index 0000000000..4df0462dc3 --- /dev/null +++ b/src/common/utils/sync-box/channels.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 type { MessageChannel } from "../channel/message-channel-listener-injection-token"; +import type { RequestChannel } from "../channel/request-channel-listener-injection-token"; + +export type SyncBoxChannel = MessageChannel<{ id: string; value: any }>; + +export const syncBoxChannel: SyncBoxChannel = { + id: "sync-box-channel", +}; + +export type SyncBoxInitialValueChannel = RequestChannel< + void, + { id: string; value: any }[] +>; + +export const syncBoxInitialValueChannel: SyncBoxInitialValueChannel = { + id: "sync-box-initial-value-channel", +}; diff --git a/src/common/utils/sync-box/create-sync-box.injectable.ts b/src/common/utils/sync-box/create-sync-box.injectable.ts index 1328106e24..4a01fe71a0 100644 --- a/src/common/utils/sync-box/create-sync-box.injectable.ts +++ b/src/common/utils/sync-box/create-sync-box.injectable.ts @@ -5,17 +5,17 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { IObservableValue } from "mobx"; import { computed } from "mobx"; -import syncBoxChannelInjectable from "./sync-box-channel.injectable"; -import { messageToChannelInjectionToken } from "../channel/message-to-channel-injection-token"; +import { syncBoxChannel } from "./channels"; +import { sendMessageToChannelInjectionToken } from "../channel/message-to-channel-injection-token"; import syncBoxStateInjectable from "./sync-box-state.injectable"; import type { SyncBox } from "./sync-box-injection-token"; +import { toJS } from "../toJS"; const createSyncBoxInjectable = getInjectable({ id: "create-sync-box", instantiate: (di) => { - const syncBoxChannel = di.inject(syncBoxChannelInjectable); - const messageToChannel = di.inject(messageToChannelInjectionToken); + const messageToChannel = di.inject(sendMessageToChannelInjectionToken); const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id); return (id: string, initialValue: Value): SyncBox => { @@ -26,7 +26,7 @@ const createSyncBoxInjectable = getInjectable({ return { id, - value: computed(() => state.get()), + value: computed(() => toJS(state.get())), set: (value) => { state.set(value); diff --git a/src/common/utils/sync-box/handler.injectable.ts b/src/common/utils/sync-box/handler.injectable.ts new file mode 100644 index 0000000000..f520585474 --- /dev/null +++ b/src/common/utils/sync-box/handler.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 type { MessageChannelHandler } from "../channel/message-channel-listener-injection-token"; +import type { SyncBoxChannel } from "./channels"; +import syncBoxStateInjectable from "./sync-box-state.injectable"; + +const syncBoxChannelHandlerInjectable = getInjectable({ + id: "sync-box-channel-handler", + instantiate: (di): MessageChannelHandler => { + const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id); + + return ({ id, value }) => getSyncBoxState(id)?.set(value); + }, +}); + +export default syncBoxChannelHandlerInjectable; diff --git a/src/common/utils/sync-box/sync-box-channel-listener.injectable.ts b/src/common/utils/sync-box/sync-box-channel-listener.injectable.ts deleted file mode 100644 index b603c85997..0000000000 --- a/src/common/utils/sync-box/sync-box-channel-listener.injectable.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import type { 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 deleted file mode 100644 index 9389a99867..0000000000 --- a/src/common/utils/sync-box/sync-box-channel.injectable.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. - */ -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 deleted file mode 100644 index 89374c3565..0000000000 --- a/src/common/utils/sync-box/sync-box-initial-value-channel.injectable.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import type { 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/tar.ts b/src/common/utils/tar.ts index 58008dfe9d..3102098976 100644 --- a/src/common/utils/tar.ts +++ b/src/common/utils/tar.ts @@ -5,7 +5,6 @@ // Helper for working with tarball files (.tar, .tgz) // Docs: https://github.com/npm/node-tar -import type { FileStat } from "tar"; import tar from "tar"; import path from "path"; import { parse } from "./json"; @@ -35,7 +34,7 @@ export function readFileFromTar({ tarPath, filePath, file: tarPath, filter: entryPath => path.normalize(entryPath) === filePath, sync: true, - onentry(entry: FileStat) { + onentry(entry) { entry.on("data", chunk => { fileChunks.push(chunk); }); @@ -63,7 +62,7 @@ export async function listTarEntries(filePath: string): Promise { await tar.list({ file: filePath, onentry: (entry) => { - entries.push(path.normalize(entry.path as unknown as string)); + entries.push(path.normalize(entry.path)); }, }); diff --git a/src/common/utils/tentative-parse-json.ts b/src/common/utils/tentative-parse-json.ts deleted file mode 100644 index a0cb089a74..0000000000 --- a/src/common/utils/tentative-parse-json.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * 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 deleted file mode 100644 index dc7206be7c..0000000000 --- a/src/common/utils/tentative-stringify-json.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * 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/type-narrowing.ts b/src/common/utils/type-narrowing.ts index eb5b6996cf..d2d33929a2 100644 --- a/src/common/utils/type-narrowing.ts +++ b/src/common/utils/type-narrowing.ts @@ -103,7 +103,7 @@ export function isBoolean(val: unknown): val is boolean { * checks if val is of type object and isn't null * @param val the value to be checked */ -export function isObject(val: unknown): val is object { +export function isObject(val: unknown): val is Record { return typeof val === "object" && val !== null; } diff --git a/src/common/vars.ts b/src/common/vars.ts index 94ac631896..1b0cf1cd2e 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -123,7 +123,7 @@ export const apiKubePrefix = "/api-kube"; // k8s cluster apis // Links export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string; export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/zt-wcl8jq3k-68R5Wcmk1o95MLBE5igUDQ" as string; -export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string; +export const supportUrl = "https://docs.k8slens.dev/support/" as string; export const lensWebsiteWeblinkId = "lens-website-link"; export const lensDocumentationWeblinkId = "lens-documentation-link"; @@ -132,4 +132,4 @@ export const lensTwitterWeblinkId = "lens-twitter-link"; export const lensBlogWeblinkId = "lens-blog-link"; export const kubernetesDocumentationWeblinkId = "kubernetes-documentation-link"; -export const docsUrl = "https://docs.k8slens.dev/main" as string; +export const docsUrl = "https://docs.k8slens.dev" as string; diff --git a/src/common/vars/application-information.global-override-for-injectable.ts b/src/common/vars/application-information.global-override-for-injectable.ts index ac53b9f341..232a189ce1 100644 --- a/src/common/vars/application-information.global-override-for-injectable.ts +++ b/src/common/vars/application-information.global-override-for-injectable.ts @@ -7,6 +7,7 @@ import { getGlobalOverride } from "../test-utils/get-global-override"; import applicationInformationInjectable from "./application-information.injectable"; export default getGlobalOverride(applicationInformationInjectable, () => ({ + name: "some-product-name", productName: "some-product-name", version: "6.0.0", build: {}, diff --git a/src/common/vars/application-information.injectable.ts b/src/common/vars/application-information.injectable.ts index 559b15294b..56dfcf9d7e 100644 --- a/src/common/vars/application-information.injectable.ts +++ b/src/common/vars/application-information.injectable.ts @@ -5,16 +5,16 @@ import { getInjectable } from "@ogre-tools/injectable"; import packageJson from "../../../package.json"; -export type ApplicationInformation = Pick & { +export type ApplicationInformation = Pick & { build: Partial & { publish?: unknown[] }; }; const applicationInformationInjectable = getInjectable({ id: "application-information", instantiate: (): ApplicationInformation => { - const { version, config, productName, build, copyright, description } = packageJson; + const { version, config, productName, build, copyright, description, name } = packageJson; - return { version, config, productName, build, copyright, description }; + return { version, config, productName, build, copyright, description, name }; }, causesSideEffects: true, }); diff --git a/src/common/vars/build-semantic-version.injectable.ts b/src/common/vars/build-semantic-version.injectable.ts index a41efb0bd7..2a49327480 100644 --- a/src/common/vars/build-semantic-version.injectable.ts +++ b/src/common/vars/build-semantic-version.injectable.ts @@ -7,7 +7,7 @@ import { getInjectionToken } from "@ogre-tools/injectable"; import { SemVer } from "semver"; import type { InitializableState } from "../initializable-state/create"; import { createInitializableState } from "../initializable-state/create"; -import type { RequestChannel } from "../utils/channel/request-channel-injection-token"; +import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; export const buildVersionInjectionToken = getInjectionToken>({ id: "build-version-token", diff --git a/src/features/__snapshots__/extension-special-characters-in-page-registrations.test.tsx.snap b/src/features/__snapshots__/extension-special-characters-in-page-registrations.test.tsx.snap index dab11ecc9b..3eb46b81e9 100644 --- a/src/features/__snapshots__/extension-special-characters-in-page-registrations.test.tsx.snap +++ b/src/features/__snapshots__/extension-special-characters-in-page-registrations.test.tsx.snap @@ -182,6 +182,7 @@ exports[`extension special characters in page registrations renders 1`] = `
{ 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); + expect(setUpdateOnQuitMock).toHaveBeenLastCalledWith(true); }); }); }); diff --git a/src/features/cluster/delete-dialog/__snapshots__/delete-cluster-dialog.test.tsx.snap b/src/features/cluster/delete-dialog/__snapshots__/delete-cluster-dialog.test.tsx.snap index 907f60bb8f..39d6efa75c 100644 --- a/src/features/cluster/delete-dialog/__snapshots__/delete-cluster-dialog.test.tsx.snap +++ b/src/features/cluster/delete-dialog/__snapshots__/delete-cluster-dialog.test.tsx.snap @@ -312,6 +312,7 @@ exports[`Deleting a cluster when an internal kubeconfig cluster is used when the
; - -const clearClusterAsDeletingChannelInjectable = getInjectable({ - id: "clear-cluster-as-deleting-channel", - instantiate: (): ClearClusterAsDeletingChannel => ({ - id: "clear-cluster-as-deleting", - }), - injectionToken: requestChannelInjectionToken, -}); - -export default clearClusterAsDeletingChannelInjectable; diff --git a/src/features/cluster/delete-dialog/common/clear-as-deleting-channel.ts b/src/features/cluster/delete-dialog/common/clear-as-deleting-channel.ts new file mode 100644 index 0000000000..bf33a23165 --- /dev/null +++ b/src/features/cluster/delete-dialog/common/clear-as-deleting-channel.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ClusterId } from "../../../../common/cluster-types"; +import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; + +export type ClearClusterAsDeletingChannel = RequestChannel; + +export const clearClusterAsDeletingChannel: ClearClusterAsDeletingChannel = { + id: "clear-cluster-as-deleting", +}; diff --git a/src/features/cluster/delete-dialog/common/delete-channel.injectable.ts b/src/features/cluster/delete-dialog/common/delete-channel.injectable.ts deleted file mode 100644 index 9242b062c5..0000000000 --- a/src/features/cluster/delete-dialog/common/delete-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 type { ClusterId } from "../../../../common/cluster-types"; -import type { RequestChannel } from "../../../../common/utils/channel/request-channel-injection-token"; -import { requestChannelInjectionToken } from "../../../../common/utils/channel/request-channel-injection-token"; - -export type DeleteClusterChannel = RequestChannel; - -const deleteClusterChannelInjectable = getInjectable({ - id: "delete-cluster-channel", - instantiate: (): DeleteClusterChannel => ({ - id: "delete-cluster", - }), - injectionToken: requestChannelInjectionToken, -}); - -export default deleteClusterChannelInjectable; diff --git a/src/features/cluster/delete-dialog/common/delete-channel.ts b/src/features/cluster/delete-dialog/common/delete-channel.ts new file mode 100644 index 0000000000..0e9142fcd3 --- /dev/null +++ b/src/features/cluster/delete-dialog/common/delete-channel.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ClusterId } from "../../../../common/cluster-types"; +import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; + +export type DeleteClusterChannel = RequestChannel; + +export const deleteClusterChannel: DeleteClusterChannel = { + id: "delete-cluster", +}; diff --git a/src/features/cluster/delete-dialog/common/set-as-deleting-channel.injectable.ts b/src/features/cluster/delete-dialog/common/set-as-deleting-channel.injectable.ts deleted file mode 100644 index b625dfa14e..0000000000 --- a/src/features/cluster/delete-dialog/common/set-as-deleting-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 type { ClusterId } from "../../../../common/cluster-types"; -import type { RequestChannel } from "../../../../common/utils/channel/request-channel-injection-token"; -import { requestChannelInjectionToken } from "../../../../common/utils/channel/request-channel-injection-token"; - -export type SetClusterAsDeletingChannel = RequestChannel; - -const setClusterAsDeletingChannelInjectable = getInjectable({ - id: "set-cluster-as-deleting-channel", - instantiate: (): SetClusterAsDeletingChannel => ({ - id: "set-cluster-as-deleting", - }), - injectionToken: requestChannelInjectionToken, -}); - -export default setClusterAsDeletingChannelInjectable; diff --git a/src/features/cluster/delete-dialog/common/set-as-deleting-channel.ts b/src/features/cluster/delete-dialog/common/set-as-deleting-channel.ts new file mode 100644 index 0000000000..57ef2e3a8d --- /dev/null +++ b/src/features/cluster/delete-dialog/common/set-as-deleting-channel.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ClusterId } from "../../../../common/cluster-types"; +import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; + +export type SetClusterAsDeletingChannel = RequestChannel; + +export const setClusterAsDeletingChannel: SetClusterAsDeletingChannel = { + id: "set-cluster-as-deleting", +}; diff --git a/src/features/cluster/delete-dialog/main/clear-as-deleteing-channel-handler.injectable.ts b/src/features/cluster/delete-dialog/main/clear-as-deleteing-channel-handler.injectable.ts deleted file mode 100644 index f1b89573d8..0000000000 --- a/src/features/cluster/delete-dialog/main/clear-as-deleteing-channel-handler.injectable.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token"; -import clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable"; -import clearClusterAsDeletingChannelInjectable from "../common/clear-as-deleting-channel.injectable"; - -const clearClusterAsDeletingChannelHandlerInjectable = getInjectable({ - id: "clear-cluster-as-deleting-channel-handler", - instantiate: (di) => { - const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable); - - return { - channel: di.inject(clearClusterAsDeletingChannelInjectable), - handler: (clusterId) => clustersThatAreBeingDeleted.delete(clusterId), - }; - }, - injectionToken: requestChannelListenerInjectionToken, -}); - -export default clearClusterAsDeletingChannelHandlerInjectable; diff --git a/src/features/cluster/delete-dialog/main/clear-as-deleting-channel-listener.injectable.ts b/src/features/cluster/delete-dialog/main/clear-as-deleting-channel-listener.injectable.ts new file mode 100644 index 0000000000..a8dd2a80b0 --- /dev/null +++ b/src/features/cluster/delete-dialog/main/clear-as-deleting-channel-listener.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 clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable"; +import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; +import { clearClusterAsDeletingChannel } from "../common/clear-as-deleting-channel"; + +const clearClusterAsDeletingChannelListenerInjectable = getRequestChannelListenerInjectable({ + channel: clearClusterAsDeletingChannel, + handler: (di) => { + const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable); + + return (clusterId) => { + clustersThatAreBeingDeleted.delete(clusterId); + }; + }, +}); + +export default clearClusterAsDeletingChannelListenerInjectable; diff --git a/src/features/cluster/delete-dialog/main/delete-channel-handler.injectable.ts b/src/features/cluster/delete-dialog/main/delete-channel-handler.injectable.ts deleted file mode 100644 index 8945ffe625..0000000000 --- a/src/features/cluster/delete-dialog/main/delete-channel-handler.injectable.ts +++ /dev/null @@ -1,56 +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 appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable"; -import clusterFramesInjectable from "../../../../common/cluster-frames.injectable"; -import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable"; -import directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; -import deleteFileInjectable from "../../../../common/fs/delete-file.injectable"; -import joinPathsInjectable from "../../../../common/path/join-paths.injectable"; -import { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token"; -import deleteClusterChannelInjectable from "../common/delete-channel.injectable"; - -const deleteClusterChannelHandlerInjectable = getInjectable({ - id: "delete-cluster-channel-handler", - instantiate: (di) => { - const appEventBus = di.inject(appEventBusInjectable); - const clusterStore = di.inject(clusterStoreInjectable); - const clusterFrames = di.inject(clusterFramesInjectable); - const joinPaths = di.inject(joinPathsInjectable); - const directoryForLensLocalStorage = di.inject(directoryForLensLocalStorageInjectable); - const deleteFile = di.inject(deleteFileInjectable); - - return { - channel: di.inject(deleteClusterChannelInjectable), - handler: async (clusterId) =>{ - appEventBus.emit({ name: "cluster", action: "remove" }); - - const cluster = clusterStore.getById(clusterId); - - if (!cluster) { - return; - } - - cluster.disconnect(); - clusterFrames.delete(cluster.id); - - // Remove from the cluster store as well, this should clear any old settings - clusterStore.clusters.delete(cluster.id); - - try { - // remove the local storage file - const localStorageFilePath = joinPaths(directoryForLensLocalStorage, `${cluster.id}.json`); - - await deleteFile(localStorageFilePath); - } catch { - // ignore error - } - }, - }; - }, - injectionToken: requestChannelListenerInjectionToken, -}); - -export default deleteClusterChannelHandlerInjectable; diff --git a/src/features/cluster/delete-dialog/main/delete-channel-listener.injectable.ts b/src/features/cluster/delete-dialog/main/delete-channel-listener.injectable.ts new file mode 100644 index 0000000000..9b7617293a --- /dev/null +++ b/src/features/cluster/delete-dialog/main/delete-channel-listener.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 appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable"; +import clusterFramesInjectable from "../../../../common/cluster-frames.injectable"; +import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable"; +import directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; +import deleteFileInjectable from "../../../../common/fs/delete-file.injectable"; +import joinPathsInjectable from "../../../../common/path/join-paths.injectable"; +import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; +import { deleteClusterChannel } from "../common/delete-channel"; + +const deleteClusterChannelListenerInjectable = getRequestChannelListenerInjectable({ + channel: deleteClusterChannel, + handler: (di) => { + const appEventBus = di.inject(appEventBusInjectable); + const clusterStore = di.inject(clusterStoreInjectable); + const clusterFrames = di.inject(clusterFramesInjectable); + const joinPaths = di.inject(joinPathsInjectable); + const directoryForLensLocalStorage = di.inject(directoryForLensLocalStorageInjectable); + const deleteFile = di.inject(deleteFileInjectable); + + return async (clusterId) => { + appEventBus.emit({ name: "cluster", action: "remove" }); + + const cluster = clusterStore.getById(clusterId); + + if (!cluster) { + return; + } + + cluster.disconnect(); + clusterFrames.delete(cluster.id); + + // Remove from the cluster store as well, this should clear any old settings + clusterStore.clusters.delete(cluster.id); + + try { + // remove the local storage file + const localStorageFilePath = joinPaths(directoryForLensLocalStorage, `${cluster.id}.json`); + + await deleteFile(localStorageFilePath); + } catch { + // ignore error + } + }; + }, +}); + +export default deleteClusterChannelListenerInjectable; diff --git a/src/features/cluster/delete-dialog/main/set-as-deleteing-channel-handler.injectable.ts b/src/features/cluster/delete-dialog/main/set-as-deleteing-channel-handler.injectable.ts deleted file mode 100644 index 0b8862e384..0000000000 --- a/src/features/cluster/delete-dialog/main/set-as-deleteing-channel-handler.injectable.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token"; -import clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable"; -import setClusterAsDeletingChannelInjectable from "../common/set-as-deleting-channel.injectable"; - -const setClusterAsDeletingChannelHandlerInjectable = getInjectable({ - id: "set-cluster-as-deleting-channel-handler", - instantiate: (di) => { - const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable); - - return { - channel: di.inject(setClusterAsDeletingChannelInjectable), - handler: (clusterId) => clustersThatAreBeingDeleted.add(clusterId), - }; - }, - injectionToken: requestChannelListenerInjectionToken, -}); - -export default setClusterAsDeletingChannelHandlerInjectable; diff --git a/src/features/cluster/delete-dialog/main/set-as-deleteing-channel-listener.injectable.ts b/src/features/cluster/delete-dialog/main/set-as-deleteing-channel-listener.injectable.ts new file mode 100644 index 0000000000..f532b4a81f --- /dev/null +++ b/src/features/cluster/delete-dialog/main/set-as-deleteing-channel-listener.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 clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable"; +import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; +import { setClusterAsDeletingChannel } from "../common/set-as-deleting-channel"; + +const setClusterAsDeletingChannelHandlerInjectable = getRequestChannelListenerInjectable({ + channel: setClusterAsDeletingChannel, + handler: (di) => { + const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable); + + return (clusterId) => { + clustersThatAreBeingDeleted.add(clusterId); + }; + }, +}); + +export default setClusterAsDeletingChannelHandlerInjectable; diff --git a/src/features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable.ts b/src/features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable.ts index b480630f6c..e476998ca0 100644 --- a/src/features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable.ts +++ b/src/features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { ClusterId } from "../../../../common/cluster-types"; import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; -import clearClusterAsDeletingChannelInjectable from "../common/clear-as-deleting-channel.injectable"; +import { clearClusterAsDeletingChannel } from "../common/clear-as-deleting-channel"; export type RequestClearClusterAsDeleting = (clusterId: ClusterId) => Promise; @@ -13,7 +13,6 @@ const requestClearClusterAsDeletingInjectable = getInjectable({ id: "request-clear-cluster-as-deleting", instantiate: (di): RequestClearClusterAsDeleting => { const requestChannel = di.inject(requestFromChannelInjectable); - const clearClusterAsDeletingChannel = di.inject(clearClusterAsDeletingChannelInjectable); return (clusterId) => requestChannel(clearClusterAsDeletingChannel, clusterId); }, diff --git a/src/features/cluster/delete-dialog/renderer/request-delete.injectable.ts b/src/features/cluster/delete-dialog/renderer/request-delete.injectable.ts index 602923e60e..c1286e3103 100644 --- a/src/features/cluster/delete-dialog/renderer/request-delete.injectable.ts +++ b/src/features/cluster/delete-dialog/renderer/request-delete.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { ClusterId } from "../../../../common/cluster-types"; import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; -import deleteClusterChannelInjectable from "../common/delete-channel.injectable"; +import { deleteClusterChannel } from "../common/delete-channel"; export type RequestDeleteCluster = (clusterId: ClusterId) => Promise; @@ -13,7 +13,6 @@ const requestDeleteClusterInjectable = getInjectable({ id: "request-delete-cluster", instantiate: (di): RequestDeleteCluster => { const requestChannel = di.inject(requestFromChannelInjectable); - const deleteClusterChannel = di.inject(deleteClusterChannelInjectable); return (clusterId) => requestChannel(deleteClusterChannel, clusterId); }, diff --git a/src/features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable.ts b/src/features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable.ts index 997348d44d..de3a6393b3 100644 --- a/src/features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable.ts +++ b/src/features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { ClusterId } from "../../../../common/cluster-types"; import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; -import setClusterAsDeletingChannelInjectable from "../common/set-as-deleting-channel.injectable"; +import { setClusterAsDeletingChannel } from "../common/set-as-deleting-channel"; export type RequestSetClusterAsDeleting = (clusterId: ClusterId) => Promise; @@ -13,7 +13,6 @@ const requestSetClusterAsDeletingInjectable = getInjectable({ id: "request-set-cluster-as-deleting", instantiate: (di): RequestSetClusterAsDeleting => { const requestChannel = di.inject(requestFromChannelInjectable); - const setClusterAsDeletingChannel = di.inject(setClusterAsDeletingChannelInjectable); return (clusterId) => requestChannel(setClusterAsDeletingChannel, clusterId); }, diff --git a/src/features/command-pallet/__snapshots__/keyboard-shortcuts.test.tsx.snap b/src/features/command-pallet/__snapshots__/keyboard-shortcuts.test.tsx.snap index 6704a798ae..d9c8504408 100644 --- a/src/features/command-pallet/__snapshots__/keyboard-shortcuts.test.tsx.snap +++ b/src/features/command-pallet/__snapshots__/keyboard-shortcuts.test.tsx.snap @@ -264,6 +264,7 @@ exports[`Command Pallet: keyboard shortcut tests when on linux renders 1`] = `
{ let builder: ApplicationBuilder; let rendered: RenderResult; let focusWindowMock: jest.Mock; + let downloadJson: jest.MockedFunction; + let downloadBinary: jest.MockedFunction; beforeEach(async () => { builder = getApplicationBuilder(); builder.beforeWindowStart((windowDi) => { focusWindowMock = jest.fn(); + downloadJson = jest.fn().mockImplementation((url) => { throw new Error(`Unexpected call to downloadJson for url=${url}`); }); + downloadBinary = jest.fn().mockImplementation((url) => { throw new Error(`Unexpected call to downloadJson for url=${url}`); }); windowDi.override(focusWindowInjectable, () => focusWindowMock); + windowDi.override(downloadJsonInjectable, () => downloadJson); + windowDi.override(downloadBinaryInjectable, () => downloadBinary); }); rendered = await builder.render(); diff --git a/src/features/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap b/src/features/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap index 433cf01bac..b824016800 100644 --- a/src/features/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap +++ b/src/features/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap @@ -560,6 +560,7 @@ exports[`add custom helm repository in preferences when navigating to preference
{ let showSuccessNotificationMock: jest.Mock; let showErrorNotificationMock: jest.Mock; let rendered: RenderResult; - let execFileMock: AsyncFnMock< - ReturnType - >; + let execFileMock: AsyncFnMock; let getActiveHelmRepositoriesMock: AsyncFnMock<() => Promise>>; beforeEach(async () => { @@ -184,9 +183,13 @@ describe("add custom helm repository in preferences", () => { describe("when activation rejects", () => { beforeEach(async () => { - await execFileMock.reject( - "Some error", - ); + await execFileMock.resolve({ + callWasSuccessful: false, + error: { + error: new Error("Some error"), + stderr: "", + }, + }); }); it("renders", () => { @@ -219,8 +222,10 @@ describe("add custom helm repository in preferences", () => { "some-helm-binary-path", ["repo", "add", "some-custom-repository", "http://some.url"], ], - - "", + { + callWasSuccessful: true, + response: "", + }, ); }); diff --git a/src/features/helm-charts/add-helm-repository-from-list-in-preferences.test.ts b/src/features/helm-charts/add-helm-repository-from-list-in-preferences.test.ts index 98e94321bd..73831bec04 100644 --- a/src/features/helm-charts/add-helm-repository-from-list-in-preferences.test.ts +++ b/src/features/helm-charts/add-helm-repository-from-list-in-preferences.test.ts @@ -7,6 +7,7 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { AsyncFnMock } from "@async-fn/jest"; import asyncFn from "@async-fn/jest"; +import type { ExecFile } from "../../common/fs/exec-file.injectable"; import execFileInjectable from "../../common/fs/exec-file.injectable"; import helmBinaryPathInjectable from "../../main/helm/helm-binary-path.injectable"; import getActiveHelmRepositoriesInjectable from "../../main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories.injectable"; @@ -21,9 +22,7 @@ describe("add helm repository from list in preferences", () => { let showSuccessNotificationMock: jest.Mock; let showErrorNotificationMock: jest.Mock; let rendered: RenderResult; - let execFileMock: AsyncFnMock< - ReturnType - >; + let execFileMock: AsyncFnMock; let getActiveHelmRepositoriesMock: AsyncFnMock<() => Promise>>; let callForPublicHelmRepositoriesMock: AsyncFnMock<() => Promise>; @@ -129,9 +128,13 @@ describe("add helm repository from list in preferences", () => { describe("when adding rejects", () => { beforeEach(async () => { - await execFileMock.reject( - "Some error", - ); + await execFileMock.resolve({ + callWasSuccessful: false, + error: { + error: new Error("Some error"), + stderr: "", + }, + }); }); it("renders", () => { @@ -164,8 +167,10 @@ describe("add helm repository from list in preferences", () => { "some-helm-binary-path", ["repo", "add", "Some to be added repository", "some-other-url"], ], - - "", + { + callWasSuccessful: true, + response: "", + }, ); }); @@ -243,8 +248,10 @@ describe("add helm repository from list in preferences", () => { "some-helm-binary-path", ["repo", "remove", "Some already active repository"], ], - - "", + { + callWasSuccessful: true, + response: "", + }, ); }); diff --git a/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap b/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap index b37bc47f0f..c26b1578f5 100644 --- a/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap +++ b/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap @@ -3087,7 +3087,7 @@ exports[`installing helm chart from new tab given tab for installing chart was n
{ describe("when getting configuration rejects", () => { beforeEach(async () => { - await execFileMock.reject("some-error"); + await execFileMock.resolve({ + callWasSuccessful: false, + error: { + error: new Error("some error"), + stderr: "some-error", + }, + }); }); it("shows error notification", () => { @@ -115,7 +121,10 @@ describe("listing active helm repositories in preferences", () => { await execFileMock.resolveSpecific( ["some-helm-binary-path", ["env"]], - "HELM_REPOSITORY_CACHE=some-helm-repository-cache-path", + { + callWasSuccessful: true, + response: "HELM_REPOSITORY_CACHE=some-helm-repository-cache-path", + }, ); }); @@ -154,7 +163,10 @@ describe("listing active helm repositories in preferences", () => { await execFileMock.resolveSpecific( ["some-helm-binary-path", ["env"]], - "HELM_REPOSITORY_CONFIG=some-helm-repository-config-file.yaml", + { + callWasSuccessful: true, + response: "HELM_REPOSITORY_CONFIG=some-helm-repository-config-file.yaml", + }, ); }); @@ -193,11 +205,13 @@ describe("listing active helm repositories in preferences", () => { await execFileMock.resolveSpecific( ["some-helm-binary-path", ["env"]], - - [ - "HELM_REPOSITORY_CONFIG=some-helm-repository-config-file.yaml", - "HELM_REPOSITORY_CACHE=some-helm-repository-cache-path", - ].join("\n"), + { + callWasSuccessful: true, + response: [ + "HELM_REPOSITORY_CONFIG=some-helm-repository-config-file.yaml", + "HELM_REPOSITORY_CACHE=some-helm-repository-cache-path", + ].join("\n"), + }, ); }); @@ -219,7 +233,13 @@ describe("listing active helm repositories in preferences", () => { describe("when updating repositories reject with any other error", () => { beforeEach(async () => { - await execFileMock.reject("Some error"); + await execFileMock.resolve({ + callWasSuccessful: false, + error: { + error: new Error("Some error"), + stderr: "Some error", + }, + }); }); it("shows error notification", () => { @@ -249,9 +269,13 @@ describe("listing active helm repositories in preferences", () => { beforeEach(async () => { execFileMock.mockClear(); - await execFileMock.reject( - "Error: no repositories found. You must add one before updating", - ); + await execFileMock.resolve({ + callWasSuccessful: false, + error: { + error: new Error("no repositories found. You must add one before updating"), + stderr: "no repositories found. You must add one before updating", + }, + }); }); it("renders", () => { @@ -274,7 +298,13 @@ describe("listing active helm repositories in preferences", () => { describe("when adding default repository reject", () => { beforeEach(async () => { - await execFileMock.reject("Some error"); + await execFileMock.resolve({ + callWasSuccessful: false, + error: { + error: new Error("Some error"), + stderr: "Some error", + }, + }); }); it("shows error notification", () => { @@ -307,7 +337,6 @@ describe("listing active helm repositories in preferences", () => { await execFileMock.resolveSpecific( [ "some-helm-binary-path", - [ "repo", "add", @@ -315,8 +344,10 @@ describe("listing active helm repositories in preferences", () => { "https://charts.bitnami.com/bitnami", ], ], - - "", + { + callWasSuccessful: true, + response: "", + }, ); }); @@ -380,7 +411,10 @@ describe("listing active helm repositories in preferences", () => { await execFileMock.resolveSpecific( ["some-helm-binary-path", ["repo", "update"]], - "", + { + callWasSuccessful: true, + response: "", + }, ); }); diff --git a/src/features/helm-charts/remove-helm-repository-from-list-of-active-repository-in-preferences.test.ts b/src/features/helm-charts/remove-helm-repository-from-list-of-active-repository-in-preferences.test.ts index 7a3e787a53..8d029d3412 100644 --- a/src/features/helm-charts/remove-helm-repository-from-list-of-active-repository-in-preferences.test.ts +++ b/src/features/helm-charts/remove-helm-repository-from-list-of-active-repository-in-preferences.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 type { AsyncFnMock } from "@async-fn/jest"; import asyncFn from "@async-fn/jest"; +import type { ExecFile } from "../../common/fs/exec-file.injectable"; import execFileInjectable from "../../common/fs/exec-file.injectable"; import helmBinaryPathInjectable from "../../main/helm/helm-binary-path.injectable"; import getActiveHelmRepositoriesInjectable from "../../main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories.injectable"; @@ -19,9 +20,7 @@ describe("remove helm repository from list of active repositories in preferences let builder: ApplicationBuilder; let rendered: RenderResult; let getActiveHelmRepositoriesMock: AsyncFnMock<() => Promise>>; - let execFileMock: AsyncFnMock< - ReturnType - >; + let execFileMock: AsyncFnMock; beforeEach(async () => { builder = getApplicationBuilder(); @@ -101,8 +100,10 @@ describe("remove helm repository from list of active repositories in preferences "some-helm-binary-path", ["repo", "remove", "some-active-repository"], ], - - "", + { + callWasSuccessful: true, + response: "", + }, ); }); diff --git a/src/features/navigation/reload-page/common/channel.ts b/src/features/navigation/reload-page/common/channel.ts new file mode 100644 index 0000000000..b920067a4d --- /dev/null +++ b/src/features/navigation/reload-page/common/channel.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { MessageChannel } from "../../../../common/utils/channel/message-channel-listener-injection-token"; + +export type ReloadPageChannel = MessageChannel; + +export const reloadPageChannel: ReloadPageChannel = { + id: "reload-page-channel", +}; diff --git a/src/features/navigation/reload-page/renderer/register-listener.global-override-for-injectable.ts b/src/features/navigation/reload-page/renderer/register-listener.global-override-for-injectable.ts new file mode 100644 index 0000000000..c0a43713b7 --- /dev/null +++ b/src/features/navigation/reload-page/renderer/register-listener.global-override-for-injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../../../../common/test-utils/get-global-override"; +import { reloadPageChannel } from "../common/channel"; +import reloadPageChannelListenerInjectable from "./register-listener.injectable"; + +export default getGlobalOverride(reloadPageChannelListenerInjectable, () => ({ + channel: reloadPageChannel, + handler: () => {}, +})); diff --git a/src/features/navigation/reload-page/renderer/register-listener.injectable.ts b/src/features/navigation/reload-page/renderer/register-listener.injectable.ts new file mode 100644 index 0000000000..a42d818729 --- /dev/null +++ b/src/features/navigation/reload-page/renderer/register-listener.injectable.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 { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import { reloadPageChannel } from "../common/channel"; + +const reloadPageChannelListenerInjectable = getMessageChannelListenerInjectable({ + id: "handler", + channel: reloadPageChannel, + handler: () => () => location.reload(), + causesSideEffects: true, +}); + +export default reloadPageChannelListenerInjectable; diff --git a/src/features/preferences/__snapshots__/closing-preferences.test.tsx.snap b/src/features/preferences/__snapshots__/closing-preferences.test.tsx.snap index a6619bfd26..720d17ab66 100644 --- a/src/features/preferences/__snapshots__/closing-preferences.test.tsx.snap +++ b/src/features/preferences/__snapshots__/closing-preferences.test.tsx.snap @@ -633,6 +633,7 @@ exports[`preferences - closing-preferences given accessing preferences directly
{ it("should not touch non kubeconfig keys", () => { - expect(clearKubeconfigEnvVars({ a: 1 })).toStrictEqual({ a: 1 }); + expect(clearKubeconfigEnvVars({ a: "22" })).toStrictEqual({ a: "22" }); }); it("should remove a single kubeconfig key", () => { - expect(clearKubeconfigEnvVars({ a: 1, kubeconfig: "1" })).toStrictEqual({ a: 1 }); + expect(clearKubeconfigEnvVars({ a: "22", kubeconfig: "1" })).toStrictEqual({ a: "22" }); }); it("should remove a two kubeconfig key", () => { - expect(clearKubeconfigEnvVars({ a: 1, kubeconfig: "1", kUbeconfig: "1" })).toStrictEqual({ a: 1 }); + expect(clearKubeconfigEnvVars({ a: "22", kubeconfig: "1", kUbeconfig: "1" })).toStrictEqual({ a: "22" }); }); }); 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 index 3bd0c95bf7..568d63f1ea 100644 --- a/src/main/app-paths/app-paths-request-channel-listener.injectable.ts +++ b/src/main/app-paths/app-paths-request-channel-listener.injectable.ts @@ -2,26 +2,17 @@ * 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 { appPathsChannel } from "../../common/app-paths/app-paths-channel"; import appPathsInjectable from "../../common/app-paths/app-paths.injectable"; +import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens"; -const appPathsRequestChannelListenerInjectable = getInjectable({ - id: "app-paths-request-channel-listener", - - instantiate: (di): RequestChannelListener => { - const channel = di.inject(appPathsChannelInjectable); +const appPathsRequestChannelListenerInjectable = getRequestChannelListenerInjectable({ + channel: appPathsChannel, + handler: (di) => { const appPaths = di.inject(appPathsInjectable); - return { - channel, - handler: () => appPaths, - }; + return () => appPaths; }, - injectionToken: requestChannelListenerInjectionToken, }); export default appPathsRequestChannelListenerInjectable; diff --git a/src/main/application-update/restart-and-install-update/restart-and-install-update-listener.injectable.ts b/src/main/application-update/restart-and-install-update/restart-and-install-update-listener.injectable.ts index 0580114885..87f232d9b1 100644 --- a/src/main/application-update/restart-and-install-update/restart-and-install-update-listener.injectable.ts +++ b/src/main/application-update/restart-and-install-update/restart-and-install-update-listener.injectable.ts @@ -2,25 +2,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 restartAndInstallUpdateChannel from "../../../common/application-update/restart-and-install-update-channel/restart-and-install-update-channel.injectable"; -import { messageChannelListenerInjectionToken } from "../../../common/utils/channel/message-channel-listener-injection-token"; +import { restartAndInstallUpdateChannel } from "../../../common/application-update/restart-and-install-update-channel"; +import { getMessageChannelListenerInjectable } from "../../../common/utils/channel/message-channel-listener-injection-token"; import quitAndInstallUpdateInjectable from "../quit-and-install-update.injectable"; -const restartAndInstallUpdateListenerInjectable = getInjectable({ - id: "restart-and-install-update-listener", - - instantiate: (di) => { - const quitAndInstall = di.inject(quitAndInstallUpdateInjectable); - const channel = di.inject(restartAndInstallUpdateChannel); - - return { - channel, - handler: quitAndInstall, - }; - }, - - injectionToken: messageChannelListenerInjectionToken, +const restartAndInstallUpdateListenerInjectable = getMessageChannelListenerInjectable({ + id: "restart", + channel: restartAndInstallUpdateChannel, + handler: (di) => di.inject(quitAndInstallUpdateInjectable), }); export default restartAndInstallUpdateListenerInjectable; 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 index 12ec2d7c6e..9be8fdd18f 100644 --- 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 @@ -7,7 +7,7 @@ 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 type { ReleaseChannel, UpdateChannel } from "../../../common/application-update/update-channels"; import discoveredUpdateVersionInjectable from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable"; const watchIfUpdateShouldHappenOnQuitInjectable = getInjectable({ @@ -20,33 +20,26 @@ const watchIfUpdateShouldHappenOnQuitInjectable = getInjectable({ return getStartableStoppable("watch-if-update-should-happen-on-quit", () => autorun(() => { - const sufficientlyStableUpdateChannels = - getSufficientlyStableUpdateChannels(selectedUpdateChannel.value.get()); + const sufficientlyStableUpdateChannels = getSufficientlyStableUpdateChannels(selectedUpdateChannel.value.get()); + const updateIsDiscoveredFromChannel = discoveredVersionState.value.get()?.updateChannel; - const discoveredVersion = discoveredVersionState.value.get(); - - const updateIsDiscoveredFromChannel = discoveredVersion?.updateChannel; - - const updateOnQuit = updateIsDiscoveredFromChannel - ? sufficientlyStableUpdateChannels.includes( - updateIsDiscoveredFromChannel, - ) - : false; - - setUpdateOnQuit(updateOnQuit); + setUpdateOnQuit(( + updateIsDiscoveredFromChannel + ? sufficientlyStableUpdateChannels.includes(updateIsDiscoveredFromChannel.id) + : false + )); }), ); }, }); -const getSufficientlyStableUpdateChannels = (updateChannel: UpdateChannel): UpdateChannel[] => { +const getSufficientlyStableUpdateChannels = (updateChannel: UpdateChannel): ReleaseChannel[] => { if (!updateChannel.moreStableUpdateChannel) { - return [updateChannel]; + return [updateChannel.id]; } return [ - updateChannel, - + updateChannel.id, ...getSufficientlyStableUpdateChannels(updateChannel.moreStableUpdateChannel), ]; }; diff --git a/src/main/build-version/setup-channel.injectable.ts b/src/main/build-version/setup-channel.injectable.ts index a34edc0eac..05da92db6e 100644 --- a/src/main/build-version/setup-channel.injectable.ts +++ b/src/main/build-version/setup-channel.injectable.ts @@ -2,22 +2,17 @@ * 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 { requestChannelListenerInjectionToken } from "../../common/utils/channel/request-channel-listener-injection-token"; import { buildVersionChannel } from "../../common/vars/build-semantic-version.injectable"; +import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens"; import buildVersionInjectable from "../vars/build-version/build-version.injectable"; -const setupBuildVersionRequestChannelInjectable = getInjectable({ - id: "setup-build-version-request-channel", - instantiate: (di) => { +const buildVersionChannelListenerInjectable = getRequestChannelListenerInjectable({ + channel: buildVersionChannel, + handler: (di) => { const buildVersion = di.inject(buildVersionInjectable); - return { - channel: buildVersionChannel, - handler: () => buildVersion.get(), - }; + return () => buildVersion.get(); }, - injectionToken: requestChannelListenerInjectionToken, }); -export default setupBuildVersionRequestChannelInjectable; +export default buildVersionChannelListenerInjectable; diff --git a/src/main/helm/exec-helm/exec-helm.injectable.ts b/src/main/helm/exec-helm/exec-helm.injectable.ts index dbe65eda34..d81da0c8b8 100644 --- a/src/main/helm/exec-helm/exec-helm.injectable.ts +++ b/src/main/helm/exec-helm/exec-helm.injectable.ts @@ -6,9 +6,8 @@ import { getInjectable } from "@ogre-tools/injectable"; import execFileInjectable from "../../../common/fs/exec-file.injectable"; import helmBinaryPathInjectable from "../helm-binary-path.injectable"; import type { AsyncResult } from "../../../common/utils/async-result"; -import { getErrorMessage } from "../../../common/utils/get-error-message"; -export type ExecHelm = (args: string[]) => Promise>; +export type ExecHelm = (args: string[]) => Promise>; const execHelmInjectable = getInjectable({ id: "exec-helm", @@ -18,15 +17,18 @@ const execHelmInjectable = getInjectable({ const helmBinaryPath = di.inject(helmBinaryPathInjectable); return async (args) => { - try { - const response = await execFile(helmBinaryPath, args, { - maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB - }); + const response = await execFile(helmBinaryPath, args, { + maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB + }); - return { callWasSuccessful: true, response }; - } catch (error) { - return { callWasSuccessful: false, error: getErrorMessage(error) }; + if (response.callWasSuccessful) { + return response; } + + return { + callWasSuccessful: false, + error: response.error.stderr || response.error.error.message, + }; }; }, }); diff --git a/src/main/helm/repositories/add-helm-repository/add-helm-repository-channel-listener.injectable.ts b/src/main/helm/repositories/add-helm-repository/add-helm-repository-channel-listener.injectable.ts index f4effd1a62..1c68bf6fd7 100644 --- a/src/main/helm/repositories/add-helm-repository/add-helm-repository-channel-listener.injectable.ts +++ b/src/main/helm/repositories/add-helm-repository/add-helm-repository-channel-listener.injectable.ts @@ -2,25 +2,13 @@ * 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 addHelmRepositoryChannelInjectable from "../../../../common/helm/add-helm-repository-channel.injectable"; +import { addHelmRepositoryChannel } from "../../../../common/helm/add-helm-repository-channel"; +import { getRequestChannelListenerInjectable } from "../../../utils/channel/channel-listeners/listener-tokens"; import addHelmRepositoryInjectable from "./add-helm-repository.injectable"; -import { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token"; -const addHelmRepositoryChannelListenerInjectable = getInjectable({ - id: "add-helm-repository-channel-listener", - - instantiate: (di) => { - const addHelmRepository = di.inject(addHelmRepositoryInjectable); - const channel = di.inject(addHelmRepositoryChannelInjectable); - - return { - channel, - handler: addHelmRepository, - }; - }, - - injectionToken: requestChannelListenerInjectionToken, +const addHelmRepositoryChannelListenerInjectable = getRequestChannelListenerInjectable({ + channel: addHelmRepositoryChannel, + handler: (di) => di.inject(addHelmRepositoryInjectable), }); export default addHelmRepositoryChannelListenerInjectable; diff --git a/src/main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories-channel-listener.injectable.ts b/src/main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories-channel-listener.injectable.ts index e48ec7665e..5ef0f2b5a4 100644 --- a/src/main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories-channel-listener.injectable.ts +++ b/src/main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories-channel-listener.injectable.ts @@ -2,25 +2,13 @@ * 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 { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token"; -import getActiveHelmRepositoriesChannelInjectable from "../../../../common/helm/get-active-helm-repositories-channel.injectable"; +import { getActiveHelmRepositoriesChannel } from "../../../../common/helm/get-active-helm-repositories-channel"; +import { getRequestChannelListenerInjectable } from "../../../utils/channel/channel-listeners/listener-tokens"; import getActiveHelmRepositoriesInjectable from "./get-active-helm-repositories.injectable"; -const getActiveHelmRepositoriesChannelListenerInjectable = getInjectable({ - id: "get-active-helm-repositories-channel-listener", - - instantiate: (di) => { - const getActiveHelmRepositories = di.inject(getActiveHelmRepositoriesInjectable); - - return { - channel: di.inject(getActiveHelmRepositoriesChannelInjectable), - - handler: getActiveHelmRepositories, - }; - }, - - injectionToken: requestChannelListenerInjectionToken, +const getActiveHelmRepositoriesChannelListenerInjectable = getRequestChannelListenerInjectable({ + channel: getActiveHelmRepositoriesChannel, + handler: (di) => di.inject(getActiveHelmRepositoriesInjectable), }); export default getActiveHelmRepositoriesChannelListenerInjectable; diff --git a/src/main/helm/repositories/remove-helm-repository/remove-helm-repository-channel-listener.injectable.ts b/src/main/helm/repositories/remove-helm-repository/remove-helm-repository-channel-listener.injectable.ts index b9b2f11777..8a4e04b87c 100644 --- a/src/main/helm/repositories/remove-helm-repository/remove-helm-repository-channel-listener.injectable.ts +++ b/src/main/helm/repositories/remove-helm-repository/remove-helm-repository-channel-listener.injectable.ts @@ -2,25 +2,13 @@ * 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 { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token"; import removeHelmRepositoryInjectable from "./remove-helm-repository.injectable"; -import removeHelmRepositoryChannelInjectable from "../../../../common/helm/remove-helm-repository-channel.injectable"; +import { removeHelmRepositoryChannel } from "../../../../common/helm/remove-helm-repository-channel"; +import { getRequestChannelListenerInjectable } from "../../../utils/channel/channel-listeners/listener-tokens"; -const removeHelmRepositoryChannelListenerInjectable = getInjectable({ - id: "remove-helm-repository-channel-listener", - - instantiate: (di) => { - const removeHelmRepository = di.inject(removeHelmRepositoryInjectable); - const channel = di.inject(removeHelmRepositoryChannelInjectable); - - return { - channel, - handler: removeHelmRepository, - }; - }, - - injectionToken: requestChannelListenerInjectionToken, +const removeHelmRepositoryChannelListenerInjectable = getRequestChannelListenerInjectable({ + channel: removeHelmRepositoryChannel, + handler: (di) => di.inject(removeHelmRepositoryInjectable), }); export default removeHelmRepositoryChannelListenerInjectable; diff --git a/src/main/index.ts b/src/main/index.ts index 8a95ad02db..870ca7490c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -6,6 +6,7 @@ // Main process import * as Mobx from "mobx"; +import { spawn } from "node-pty"; import * as LensExtensionsCommonApi from "../extensions/common-api"; import * as LensExtensionsMainApi from "../extensions/main-api"; import { getDi } from "./getDi"; @@ -30,4 +31,8 @@ const LensExtensions = { Main: LensExtensionsMainApi, }; -export { Mobx, LensExtensions }; +const Pty = { + spawn, +}; + +export { Mobx, LensExtensions, Pty }; 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 c13bbeb5a2..49e6dd059f 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 @@ -36,11 +36,8 @@ export class LocalShellSession extends ShellSession { } public async open() { - let env = await this.getCachedShellEnv(); - // extensions can modify the env - env = this.dependencies.modifyTerminalShellEnv(this.cluster.id, env); - + const env = this.dependencies.modifyTerminalShellEnv(this.cluster.id, await this.getCachedShellEnv()); const shell = env.PTYSHELL; if (!shell) { diff --git a/src/main/shell-session/local-shell-session/open.injectable.ts b/src/main/shell-session/local-shell-session/open.injectable.ts index 0187ba97e7..bfb72911ec 100644 --- a/src/main/shell-session/local-shell-session/open.injectable.ts +++ b/src/main/shell-session/local-shell-session/open.injectable.ts @@ -17,6 +17,11 @@ import type WebSocket from "ws"; import getDirnameOfPathInjectable from "../../../common/path/get-dirname.injectable"; import joinPathsInjectable from "../../../common/path/join-paths.injectable"; import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; +import computeShellEnvironmentInjectable from "../../utils/shell-env/compute-shell-environment.injectable"; +import spawnPtyInjectable from "../spawn-pty.injectable"; +import resolvedShellInjectable from "../../../common/user-store/resolved-shell.injectable"; +import appNameInjectable from "../../../common/vars/app-name.injectable"; +import buildVersionInjectable from "../../vars/build-version/build-version.injectable"; export interface OpenLocalShellSessionArgs { websocket: WebSocket; @@ -34,13 +39,18 @@ const openLocalShellSessionInjectable = getInjectable({ const dependencies: LocalShellSessionDependencies = { directoryForBinaries: di.inject(directoryForBinariesInjectable), isMac: di.inject(isMacInjectable), - modifyTerminalShellEnv: di.inject(modifyTerminalShellEnvInjectable), isWindows: di.inject(isWindowsInjectable), logger: di.inject(loggerInjectable), userStore: di.inject(userStoreInjectable), + resolvedShell: di.inject(resolvedShellInjectable), + appName: di.inject(appNameInjectable), + buildVersion: di.inject(buildVersionInjectable), + modifyTerminalShellEnv: di.inject(modifyTerminalShellEnvInjectable), getDirnameOfPath: di.inject(getDirnameOfPathInjectable), joinPaths: di.inject(joinPathsInjectable), getBasenameOfPath: di.inject(getBasenameOfPathInjectable), + computeShellEnvironment: di.inject(computeShellEnvironmentInjectable), + spawnPty: di.inject(spawnPtyInjectable), }; return (args) => { diff --git a/src/main/shell-session/local-shell-session/techincal.test.ts b/src/main/shell-session/local-shell-session/techincal.test.ts new file mode 100644 index 0000000000..fbe2ccaea3 --- /dev/null +++ b/src/main/shell-session/local-shell-session/techincal.test.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 type { DiContainer } from "@ogre-tools/injectable"; +import { WebSocket } from "ws"; +import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import type { Cluster } from "../../../common/cluster/cluster"; +import platformInjectable from "../../../common/vars/platform.injectable"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import createKubectlInjectable from "../../kubectl/create-kubectl.injectable"; +import type { Kubectl } from "../../kubectl/kubectl"; +import buildVersionInjectable from "../../vars/build-version/build-version.injectable"; +import type { OpenShellSession } from "../create-shell-session.injectable"; +import type { SpawnPty } from "../spawn-pty.injectable"; +import spawnPtyInjectable from "../spawn-pty.injectable"; +import openLocalShellSessionInjectable from "./open.injectable"; + +describe("technical unit tests for local shell sessions", () => { + let di: DiContainer; + + beforeEach(() => { + di = getDiForUnitTesting({ + doGeneralOverrides: true, + }); + + di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); + di.override(buildVersionInjectable, () => ({ + get: () => "1.1.1", + })); + }); + + describe("when on windows", () => { + let openLocalShellSession: OpenShellSession; + let spawnPtyMock: jest.MockedFunction; + + beforeEach(() => { + di.override(platformInjectable, () => "win32"); + + spawnPtyMock = jest.fn(); + di.override(spawnPtyInjectable, () => spawnPtyMock); + + di.override(createKubectlInjectable, () => () => ({ + binDir: async () => "/some-kubectl-binary-dir", + getBundledPath: () => "/some-bundled-kubectl-path", + }) as Partial as Kubectl); + + openLocalShellSession = di.inject(openLocalShellSessionInjectable); + }); + + describe("when opening a local shell session", () => { + it("should pass through all environment variables to shell", async () => { + process.env.MY_TEST_ENV_VAR = "true"; + + spawnPtyMock.mockImplementationOnce((file, args, options) => { + expect(options.env).toMatchObject({ + MY_TEST_ENV_VAR: "true", + }); + + return { + cols: 80, + rows: 40, + pid: 12343, + handleFlowControl: false, + kill: jest.fn(), + onData: jest.fn(), + onExit: jest.fn(), + pause: jest.fn(), + process: "my-pty", + resize: jest.fn(), + resume: jest.fn(), + write: jest.fn(), + on: jest.fn(), + + }; + }); + + await openLocalShellSession({ + cluster: { + getProxyKubeconfigPath: async () => "/some-proxy-kubeconfig", + preferences: {}, + } as Partial as Cluster, + tabId: "my-tab-id", + websocket: new WebSocket(null), + }); + }); + }); + }); +}); diff --git a/src/main/shell-session/node-shell-session/open.injectable.ts b/src/main/shell-session/node-shell-session/open.injectable.ts index 9bc418d812..6080129d6a 100644 --- a/src/main/shell-session/node-shell-session/open.injectable.ts +++ b/src/main/shell-session/node-shell-session/open.injectable.ts @@ -12,6 +12,11 @@ import isMacInjectable from "../../../common/vars/is-mac.injectable"; import isWindowsInjectable from "../../../common/vars/is-windows.injectable"; import loggerInjectable from "../../../common/logger.injectable"; import createKubeJsonApiForClusterInjectable from "../../../common/k8s-api/create-kube-json-api-for-cluster.injectable"; +import computeShellEnvironmentInjectable from "../../utils/shell-env/compute-shell-environment.injectable"; +import spawnPtyInjectable from "../spawn-pty.injectable"; +import resolvedShellInjectable from "../../../common/user-store/resolved-shell.injectable"; +import appNameInjectable from "../../../common/vars/app-name.injectable"; +import buildVersionInjectable from "../../vars/build-version/build-version.injectable"; export interface NodeShellSessionArgs { websocket: WebSocket; @@ -28,7 +33,12 @@ const openNodeShellSessionInjectable = getInjectable({ isMac: di.inject(isMacInjectable), isWindows: di.inject(isWindowsInjectable), logger: di.inject(loggerInjectable), + resolvedShell: di.inject(resolvedShellInjectable), + appName: di.inject(appNameInjectable), + buildVersion: di.inject(buildVersionInjectable), createKubeJsonApiForCluster: di.inject(createKubeJsonApiForClusterInjectable), + computeShellEnvironment: di.inject(computeShellEnvironmentInjectable), + spawnPty: di.inject(spawnPtyInjectable), }; const kubectl = createKubectl(params.cluster.version); const session = new NodeShellSession(dependencies, { kubectl, ...params }); diff --git a/src/main/shell-session/shell-session.ts b/src/main/shell-session/shell-session.ts index 6514b0cf85..e17f533ccb 100644 --- a/src/main/shell-session/shell-session.ts +++ b/src/main/shell-session/shell-session.ts @@ -6,18 +6,18 @@ import type { Cluster } from "../../common/cluster/cluster"; import type { Kubectl } from "../kubectl/kubectl"; import type WebSocket from "ws"; -import { shellEnv } from "../utils/shell-env"; -import { app } from "electron"; import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars"; import path from "path"; import os, { userInfo } from "os"; -import { UserStore } from "../../common/user-store"; -import * as pty from "node-pty"; +import type * as pty from "node-pty"; import { appEventBus } from "../../common/app-event-bus/event-bus"; import { stat } from "fs/promises"; import { getOrInsertWith } from "../../common/utils"; import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels"; import type { Logger } from "../../common/logger"; +import type { ComputeShellEnvironment } from "../utils/shell-env/compute-shell-environment.injectable"; +import type { SpawnPty } from "./spawn-pty.injectable"; +import type { InitializableState } from "../../common/initializable-state/create"; export class ShellOpenError extends Error { constructor(message: string, options?: ErrorOptions) { @@ -107,6 +107,11 @@ export interface ShellSessionDependencies { readonly isWindows: boolean; readonly isMac: boolean; readonly logger: Logger; + readonly resolvedShell: string | undefined; + readonly appName: string; + readonly buildVersion: InitializableState; + computeShellEnvironment: ComputeShellEnvironment; + spawnPty: SpawnPty; } export interface ShellSessionArgs { @@ -148,14 +153,14 @@ export abstract class ShellSession { protected abstract get cwd(): string | undefined; - protected ensureShellProcess(shell: string, args: string[], env: Record, cwd: string): { shellProcess: pty.IPty; resume: boolean } { + protected ensureShellProcess(shell: string, args: string[], env: Partial>, cwd: string): { shellProcess: pty.IPty; resume: boolean } { const resume = ShellSession.processes.has(this.terminalId); const shellProcess = getOrInsertWith(ShellSession.processes, this.terminalId, () => ( - pty.spawn(shell, args, { + this.dependencies.spawnPty(shell, args, { rows: 30, cols: 80, cwd, - env: env as Record, + env, name: "xterm-256color", // TODO: Something else is broken here so we need to force the use of winPty on windows useConpty: false, @@ -331,19 +336,27 @@ export abstract class ShellSession { } protected async getShellEnv() { - const shell = UserStore.getInstance().resolvedShell; - const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv(shell || userInfo().shell)))); - const pathStr = [await this.kubectlBinDirP, ...this.getPathEntries(), process.env.PATH].join(path.delimiter); + const shell = this.dependencies.resolvedShell || userInfo().shell; + const result = await this.dependencies.computeShellEnvironment(shell); + const rawEnv = (() => { + if (result.callWasSuccessful) { + return result.response ?? process.env; + } + + return process.env; + })(); + + const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(rawEnv))); + const pathStr = [await this.kubectlBinDirP, ...this.getPathEntries(), env.PATH].join(path.delimiter); delete env.DEBUG; // don't pass DEBUG into shells if (this.dependencies.isWindows) { - env.SystemRoot = process.env.SystemRoot; env.PTYSHELL = shell || "powershell.exe"; env.PATH = pathStr; env.LENS_SESSION = "true"; env.WSLENV = [ - process.env.WSLENV, + env.WSLENV, "KUBECONFIG/up:LENS_SESSION/u", ] .filter(Boolean) @@ -363,8 +376,8 @@ export abstract class ShellSession { env.PTYPID = process.pid.toString(); env.KUBECONFIG = await this.kubeconfigPathP; - env.TERM_PROGRAM = app.getName(); - env.TERM_PROGRAM_VERSION = app.getVersion(); + env.TERM_PROGRAM = this.dependencies.appName; + env.TERM_PROGRAM_VERSION = this.dependencies.buildVersion.get(); if (this.cluster.preferences.httpsProxy) { env.HTTPS_PROXY = this.cluster.preferences.httpsProxy; diff --git a/src/main/shell-session/spawn-pty.global-override-for-injectable.ts b/src/main/shell-session/spawn-pty.global-override-for-injectable.ts new file mode 100644 index 0000000000..239f7c5e47 --- /dev/null +++ b/src/main/shell-session/spawn-pty.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getGlobalOverride } from "../../common/test-utils/get-global-override"; +import spawnPtyInjectable from "./spawn-pty.injectable"; + +export default getGlobalOverride(spawnPtyInjectable, () => () => { + throw new Error("Tried to spawn a PTY without an override"); +}); diff --git a/src/main/shell-session/spawn-pty.injectable.ts b/src/main/shell-session/spawn-pty.injectable.ts new file mode 100644 index 0000000000..0639c7307b --- /dev/null +++ b/src/main/shell-session/spawn-pty.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 { IPty, IPtyForkOptions, IWindowsPtyForkOptions } from "node-pty"; +import { spawn } from "node-pty"; + +export type WindowsSpawnPtyOptions = Omit & { env?: Partial> }; +export type UnixSpawnPtyOptions = Omit & { env?: Partial> }; +export type SpawnPtyOptions = UnixSpawnPtyOptions | WindowsSpawnPtyOptions; + +export type SpawnPty = (file: string, args: string[], options: SpawnPtyOptions) => IPty; + +const spawnPtyInjectable = getInjectable({ + id: "spawn-pty", + instantiate: () => spawn as SpawnPty, + causesSideEffects: true, +}); + +export default spawnPtyInjectable; 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 252db16a1f..5e44c9d3ad 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,15 @@ import { getInjectable } from "@ogre-tools/injectable"; import loggerInjectable from "../../../../common/logger.injectable"; import applicationWindowStateInjectable from "./application-window-state.injectable"; import { BrowserWindow } from "electron"; -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"; +import getAbsolutePathInjectable from "../../../../common/path/get-absolute-path.injectable"; +import lensResourcesDirInjectable from "../../../../common/vars/lens-resources-dir.injectable"; +import isLinuxInjectable from "../../../../common/vars/is-linux.injectable"; +import fsInjectable from "../../../../common/fs/fs.injectable"; +import applicationInformationInjectable from "../../../../common/vars/application-information.injectable"; + export type ElectronWindowTitleBarStyle = "hiddenInset" | "hidden" | "default" | "customButtonsOnHover"; @@ -45,8 +50,11 @@ const createElectronWindowInjectable = getInjectable({ instantiate: (di): CreateElectronWindow => { const logger = di.inject(loggerInjectable); - const sendToChannelInLensWindow = di.inject(sendToChannelInElectronBrowserWindowInjectable); const openLinkInBrowser = di.inject(openLinkInBrowserInjectable); + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const lensResourcesDir = di.inject(lensResourcesDirInjectable); + const isLinux = di.inject(isLinuxInjectable); + const applicationInformation = di.inject(applicationInformationInjectable); return (configuration) => { const applicationWindowState = di.inject( @@ -81,6 +89,23 @@ const createElectronWindowInjectable = getInjectable({ }, }); + if (isLinux) { + const iconFileName = [ + getAbsolutePath(lensResourcesDir, `../${applicationInformation.name}.png`), + `/usr/share/icons/hicolor/512x512/apps/${applicationInformation.name}.png`, + ].find(di.inject(fsInjectable).existsSync); + + if (iconFileName != null) { + try { + browserWindow.setIcon(iconFileName); + } catch (err) { + logger.warn(`Error while setting window icon ${err}`); + } + } else { + logger.warn(`No suitable icon found for task bar.`); + } + } + applicationWindowState.manage(browserWindow); browserWindow @@ -140,7 +165,17 @@ const createElectronWindowInjectable = getInjectable({ show: () => browserWindow.show(), close: () => browserWindow.close(), - send: (args) => sendToChannelInLensWindow(configuration.id, browserWindow, args), + send: ({ channel, data, frameInfo }) => { + if (frameInfo) { + browserWindow.webContents.sendToFrame( + [frameInfo.processId, frameInfo.frameId], + channel, + data, + ); + } else { + browserWindow.webContents.send(channel, data); + } + }, reload: () => { const wc = browserWindow.webContents; 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 6dcf6b0174..b44e5774bd 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 @@ -20,7 +20,7 @@ export interface ElectronWindow { export interface SendToViewArgs { channel: string; frameInfo?: ClusterFrameInfo; - data?: unknown[]; + data?: unknown; } export interface LensWindow { diff --git a/src/main/start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable.ts deleted file mode 100644 index 08de6fec46..0000000000 --- a/src/main/start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable.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 { getInjectable } from "@ogre-tools/injectable"; -import type { BrowserWindow } from "electron"; -import type { SendToViewArgs } from "./create-lens-window.injectable"; - -const sendToChannelInElectronBrowserWindowInjectable = getInjectable({ - id: "send-to-channel-in-electron-browser-window", - - instantiate: - () => - ( - windowId: string, - browserWindow: BrowserWindow, - { channel, frameInfo, data = [] }: SendToViewArgs, - ) => { - if (frameInfo) { - browserWindow.webContents.sendToFrame( - [frameInfo.processId, frameInfo.frameId], - channel, - ...data, - ); - } else { - browserWindow.webContents.send(channel, ...data); - } - }, - - causesSideEffects: true, -}); - -export default sendToChannelInElectronBrowserWindowInjectable; 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 1916b8205f..8b204be9b3 100644 --- a/src/main/start-main-application/lens-window/navigate.injectable.ts +++ b/src/main/start-main-application/lens-window/navigate.injectable.ts @@ -37,7 +37,7 @@ const navigateInjectable = getInjectable({ applicationWindow.send({ channel, frameInfo, - data: [url], + data: url, }); }; }, diff --git a/src/main/start-main-application/lens-window/reload-current-application-window.injectable.ts b/src/main/start-main-application/lens-window/reload-current-application-window.injectable.ts index daaf88a24c..9b0d04fe9f 100644 --- a/src/main/start-main-application/lens-window/reload-current-application-window.injectable.ts +++ b/src/main/start-main-application/lens-window/reload-current-application-window.injectable.ts @@ -3,9 +3,9 @@ * 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 currentClusterFrameInjectable from "./current-cluster-frame/current-cluster-frame.injectable"; import getCurrentApplicationWindowInjectable from "./application-window/get-current-application-window.injectable"; +import { reloadPageChannel } from "../../../features/navigation/reload-page/common/channel"; const reloadCurrentApplicationWindowInjectable = getInjectable({ id: "reload-current-application-window", @@ -25,7 +25,7 @@ const reloadCurrentApplicationWindowInjectable = getInjectable({ if (frameInfo) { lensWindow.send({ - channel: IpcRendererNavigationEvents.RELOAD_PAGE, + channel: reloadPageChannel.id, frameInfo, }); } else { diff --git a/src/main/start-main-application/runnables/root-frame-has-rendered/channel-listener.injectable.ts b/src/main/start-main-application/runnables/root-frame-has-rendered/channel-listener.injectable.ts new file mode 100644 index 0000000000..40f5ceb788 --- /dev/null +++ b/src/main/start-main-application/runnables/root-frame-has-rendered/channel-listener.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 { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import { rootFrameHasRenderedChannel } from "../../../../common/root-frame/root-frame-rendered-channel"; +import { runManyFor } from "../../../../common/runnable/run-many-for"; +import { afterRootFrameIsReadyInjectionToken } from "../../runnable-tokens/after-root-frame-is-ready-injection-token"; + +const rootFrameRenderedChannelListenerInjectable = getMessageChannelListenerInjectable({ + id: "action", + channel: rootFrameHasRenderedChannel, + handler: (di) => { + const runMany = runManyFor(di); + + return runMany(afterRootFrameIsReadyInjectionToken); + }, +}); + +export default rootFrameRenderedChannelListenerInjectable; 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 deleted file mode 100644 index 83ebf7bf91..0000000000 --- a/src/main/start-main-application/runnables/root-frame-rendered-channel-listener/root-frame-rendered-channel-listener.injectable.ts +++ /dev/null @@ -1,35 +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 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-shell.injectable.ts b/src/main/start-main-application/runnables/setup-shell.injectable.ts index 147d2f24ab..9eb736b2db 100644 --- a/src/main/start-main-application/runnables/setup-shell.injectable.ts +++ b/src/main/start-main-application/runnables/setup-shell.injectable.ts @@ -5,11 +5,11 @@ import { getInjectable } from "@ogre-tools/injectable"; import loggerInjectable from "../../../common/logger.injectable"; import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; -import { shellEnv } from "../../utils/shell-env"; import os from "os"; import { unionPATHs } from "../../../common/utils/union-env-path"; import isSnapPackageInjectable from "../../../common/vars/is-snap-package.injectable"; import electronAppInjectable from "../../electron-app/electron-app.injectable"; +import computeShellEnvironmentInjectable from "../../utils/shell-env/compute-shell-environment.injectable"; const setupShellInjectable = getInjectable({ id: "setup-shell", @@ -18,13 +18,24 @@ const setupShellInjectable = getInjectable({ const logger = di.inject(loggerInjectable); const isSnapPackage = di.inject(isSnapPackageInjectable); const electronApp = di.inject(electronAppInjectable); + const computeShellEnvironment = di.inject(computeShellEnvironmentInjectable); return { id: "setup-shell", - run: async () => { + run: async (): Promise => { logger.info("🐚 Syncing shell environment"); - const env = await shellEnv(os.userInfo().shell); + const result = await computeShellEnvironment(os.userInfo().shell); + + if (!result.callWasSuccessful) { + return void logger.error(`[SHELL-SYNC]: ${result.error}`); + } + + const env = result.response; + + if (!env) { + return void logger.debug("[SHELL-SYNC]: nothing to do, env not special in shells"); + } if (!env.LANG) { // the LANG env var expects an underscore instead of electron's dash 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 index 6b7fa9b8df..6ace54c845 100644 --- 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 @@ -6,8 +6,6 @@ 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", @@ -17,11 +15,7 @@ const enlistMessageChannelListenerInjectable = getInjectable({ return ({ channel, handler }) => { const nativeOnCallback = (_: IpcMainEvent, message: unknown) => { - pipeline( - message, - tentativeParseJson, - handler, - ); + handler(message); }; ipcMain.on(channel.id, nativeOnCallback); 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 index 3bd0398d8e..407ffabd8f 100644 --- 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 @@ -88,8 +88,8 @@ describe("enlist message channel listener in main", () => { 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" })); + it("given object as message, when message arrives, calls the handler with the message", () => { + onMock.mock.calls[0][1]({} as IpcMainEvent, { 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 index 6f118288f3..d72e4afcaf 100644 --- 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 @@ -5,20 +5,20 @@ 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"; +import type { Disposer } from "../../../../common/utils"; +import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; +import type { RequestChannelListener } from "./listener-tokens"; + +export type EnlistRequestChannelListener = >(listener: RequestChannelListener) => Disposer; const enlistRequestChannelListenerInjectable = getInjectable({ id: "enlist-request-channel-listener-for-main", - instantiate: (di) => { + instantiate: (di): EnlistRequestChannelListener => { const ipcMain = di.inject(ipcMainInjectable); return ({ channel, handler }) => { - const nativeHandleCallback = (_: IpcMainInvokeEvent, request: unknown) => - pipeline(request, tentativeParseJson, handler, tentativeStringifyJson); + const nativeHandleCallback = (_: IpcMainInvokeEvent, request: unknown) => handler(request); ipcMain.handle(channel.id, nativeHandleCallback); @@ -27,8 +27,6 @@ const enlistRequestChannelListenerInjectable = getInjectable({ }; }; }, - - 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 index 12a5e9af74..2c13bbd8f9 100644 --- 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 @@ -5,11 +5,19 @@ 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"; +import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; +import type { EnlistRequestChannelListener } from "./enlist-request-channel-listener.injectable"; +import enlistRequestChannelListenerInjectable from "./enlist-request-channel-listener.injectable"; +import type { RequestChannelHandler } from "./listener-tokens"; + +type TestRequestChannel = RequestChannel; + +const testRequestChannel: TestRequestChannel = { + id: "some-channel-id", +}; describe("enlist request channel listener in main", () => { let enlistRequestChannelListener: EnlistRequestChannelListener; @@ -30,20 +38,18 @@ describe("enlist request channel listener in main", () => { di.override(ipcMainInjectable, () => ipcMainStub); - enlistRequestChannelListener = di.inject( - enlistRequestChannelListenerInjectionToken, - ); + enlistRequestChannelListener = di.inject(enlistRequestChannelListenerInjectable); }); describe("when called", () => { - let handlerMock: AsyncFnMock<(message: any) => any>; + let handlerMock: AsyncFnMock>; let disposer: () => void; beforeEach(() => { handlerMock = asyncFn(); disposer = enlistRequestChannelListener({ - channel: { id: "some-channel-id" }, + channel: testRequestChannel, handler: handlerMock, }); }); @@ -91,7 +97,7 @@ describe("enlist request channel listener in main", () => { it("resolves with the response", async () => { const actual = await actualPromise; - expect(actual).toBe('"some-response"'); + expect(actual).toBe("some-response"); }); it("when disposing the listener, de-registers the listener", () => { @@ -106,7 +112,7 @@ describe("enlist request channel listener in main", () => { const actual = await actualPromise; - expect(actual).toBe("42"); + expect(actual).toBe(42); }); it("given boolean as response, when handler resolves with response, listener resolves with stringified response", async () => { @@ -114,15 +120,15 @@ describe("enlist request channel listener in main", () => { const actual = await actualPromise; - expect(actual).toBe("true"); + expect(actual).toBe(true); }); - it("given object as response, when handler resolves with response, listener resolves with stringified response", async () => { + it("given object as response, when handler resolves with response, listener resolves with response", async () => { await handlerMock.resolve({ some: "object" }); const actual = await actualPromise; - expect(actual).toBe(JSON.stringify({ some: "object" })); + expect(actual).toEqual({ some: "object" }); }); }); @@ -138,8 +144,8 @@ describe("enlist request channel listener in main", () => { 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" })); + it("given object as request, when request arrives, calls the handler with the request", () => { + handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, { some: "object" }); expect(handlerMock).toHaveBeenCalledWith({ some: "object" }); }); diff --git a/src/main/utils/channel/channel-listeners/listener-tokens.ts b/src/main/utils/channel/channel-listeners/listener-tokens.ts new file mode 100644 index 0000000000..a3cd5af4f4 --- /dev/null +++ b/src/main/utils/channel/channel-listeners/listener-tokens.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { DiContainerForInjection } from "@ogre-tools/injectable"; +import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; +import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; + +export type RequestChannelHandler = Channel extends RequestChannel + ? (req: Request) => Promise | Response + : never; + +export interface RequestChannelListener { + channel: Channel; + handler: RequestChannelHandler; +} + + +export const requestChannelListenerInjectionToken = getInjectionToken>>( { + id: "request-channel-listener", +}); + +export interface GetRequestChannelListenerInjectableInfo< + Channel extends RequestChannel, + Request, + Response, +> { + channel: Channel; + handler: (di: DiContainerForInjection) => RequestChannelHandler; +} + +export function getRequestChannelListenerInjectable< + Channel extends RequestChannel, + Request, + Response, +>(info: GetRequestChannelListenerInjectableInfo) { + return getInjectable({ + id: `${info.channel.id}-listener`, + instantiate: (di) => ({ + channel: info.channel, + handler: info.handler(di), + }), + injectionToken: requestChannelListenerInjectionToken, + }); +} diff --git a/src/main/utils/channel/channel-listeners/listening-on-request-channels.injectable.ts b/src/main/utils/channel/channel-listeners/listening-on-request-channels.injectable.ts new file mode 100644 index 0000000000..80b94fbe0e --- /dev/null +++ b/src/main/utils/channel/channel-listeners/listening-on-request-channels.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 { disposer } from "../../../../common/utils"; +import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; +import { getStartableStoppable } from "../../../../common/utils/get-startable-stoppable"; +import enlistRequestChannelListenerInjectable from "./enlist-request-channel-listener.injectable"; +import { requestChannelListenerInjectionToken } from "./listener-tokens"; + +const listeningOnRequestChannelsInjectable = getInjectable({ + id: "listening-on-request-channels", + instantiate: (di) => { + const enlistRequestChannelListener = di.inject(enlistRequestChannelListenerInjectable); + const requestChannelListeners = di.injectMany(requestChannelListenerInjectionToken); + + return getStartableStoppable("listening-on-request-channels", () => { + const seenChannels = new Set>(); + + for (const listener of requestChannelListeners) { + if (seenChannels.has(listener.channel)) { + throw new Error(`Tried to register a multiple channel handlers for "${listener.channel.id}", only one handler is supported for a request channel.`); + } + + seenChannels.add(listener.channel); + } + + return disposer(requestChannelListeners.map(enlistRequestChannelListener)); + }); + }, +}); + +export default listeningOnRequestChannelsInjectable; diff --git a/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 deleted file mode 100644 index 78d73044e9..0000000000 --- a/src/main/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { onLoadOfApplicationInjectionToken } from "../../../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 { - id: "start-listening-of-channels-main", - run: async () => { - await listeningOfChannels.start(); - }, - }; - }, - - injectionToken: onLoadOfApplicationInjectionToken, -}); - -export default startListeningOfChannelsInjectable; diff --git a/src/main/utils/channel/channel-listeners/start-listening-on-channels.injectable.ts b/src/main/utils/channel/channel-listeners/start-listening-on-channels.injectable.ts new file mode 100644 index 0000000000..d1cf5d68d6 --- /dev/null +++ b/src/main/utils/channel/channel-listeners/start-listening-on-channels.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 { onLoadOfApplicationInjectionToken } from "../../../start-main-application/runnable-tokens/on-load-of-application-injection-token"; +import listeningOnMessageChannelsInjectable from "../../../../common/utils/channel/listening-on-message-channels.injectable"; +import listeningOnRequestChannelsInjectable from "./listening-on-request-channels.injectable"; + +const startListeningOnChannelsInjectable = getInjectable({ + id: "start-listening-on-channels-main", + + instantiate: (di) => { + const listeningOnMessageChannels = di.inject(listeningOnMessageChannelsInjectable); + const listeningOnRequestChannels = di.inject(listeningOnRequestChannelsInjectable); + + return { + id: "start-listening-on-channels-main", + run: async () => { + await listeningOnMessageChannels.start(); + await listeningOnRequestChannels.start(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default startListeningOnChannelsInjectable; diff --git a/src/main/utils/channel/message-to-channel.injectable.ts b/src/main/utils/channel/message-to-channel.injectable.ts index cbcdc2badd..1435ea59b5 100644 --- a/src/main/utils/channel/message-to-channel.injectable.ts +++ b/src/main/utils/channel/message-to-channel.injectable.ts @@ -3,9 +3,8 @@ * 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 type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token"; -import { tentativeStringifyJson } from "../../../common/utils/tentative-stringify-json"; +import type { SendMessageToChannel } from "../../../common/utils/channel/message-to-channel-injection-token"; +import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; import getVisibleWindowsInjectable from "../../start-main-application/lens-window/get-visible-windows.injectable"; const messageToChannelInjectable = getInjectable({ @@ -14,18 +13,14 @@ const messageToChannelInjectable = getInjectable({ instantiate: (di) => { const getVisibleWindows = di.inject(getVisibleWindowsInjectable); - // 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); - - getVisibleWindows().forEach((lensWindow) => - lensWindow.send({ channel: channel.id, data: stringifiedMessage ? [stringifiedMessage] : [] }), - ); - }; + return ((channel, data) => { + for (const window of getVisibleWindows()) { + window.send({ channel: channel.id, data }); + } + }) as SendMessageToChannel; }, - injectionToken: messageToChannelInjectionToken, + injectionToken: sendMessageToChannelInjectionToken, }); 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 deleted file mode 100644 index 6d5e83a548..0000000000 --- a/src/main/utils/channel/message-to-channel.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * 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 type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token"; -import type { LensWindow } from "../../start-main-application/lens-window/application-window/create-lens-window.injectable"; -import sendToChannelInElectronBrowserWindowInjectable from "../../start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable"; -import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; - -describe("message to channel from main", () => { - let messageToChannel: MessageToChannel; - let someTestWindow: LensWindow; - let someOtherTestWindow: LensWindow; - let sendToChannelInBrowserMock: jest.Mock; - - beforeEach(async () => { - const builder = getApplicationBuilder(); - - sendToChannelInBrowserMock = jest.fn(); - - builder.beforeApplicationStart(mainDi => { - mainDi.override(sendToChannelInElectronBrowserWindowInjectable, () => sendToChannelInBrowserMock); - }); - - await builder.startHidden(); - - someTestWindow = builder.applicationWindow.create("some-test-window-id"); - someOtherTestWindow = builder.applicationWindow.create("some-other-test-window-id"); - - messageToChannel = builder.mainDi.inject(messageToChannelInjectionToken); - }); - - 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 started window", () => { - beforeEach(async () => { - await someTestWindow.start(); - }); - - it("when messaging to channel, messages to window", () => { - messageToChannel(someChannel, "some-message"); - - expect(sendToChannelInBrowserMock.mock.calls).toEqual([ - [ - "some-test-window-id", - - 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([ - [ - "some-test-window-id", - - 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([ - [ - "some-test-window-id", - - 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([ - [ - "some-test-window-id", - - null, - - { - channel: "some-channel", - data: [JSON.stringify({ some: "object" })], - }, - ], - ]); - }); - }); - - it("given multiple started windows, when messaging to channel, messages to window", async () => { - await someTestWindow.start(); - await someOtherTestWindow.start(); - - messageToChannel(someChannel, "some-message"); - - expect(sendToChannelInBrowserMock.mock.calls).toEqual([ - [ - "some-test-window-id", - - null, - - { - channel: "some-channel", - data: ['"some-message"'], - }, - ], - - [ - "some-other-test-window-id", - - null, - - { - channel: "some-channel", - data: ['"some-message"'], - }, - ], - ]); - }); -}); - -const someChannel: MessageChannel = { id: "some-channel" }; diff --git a/src/main/utils/clear-kube-env-vars.ts b/src/main/utils/clear-kube-env-vars.ts index fab11be4e1..f678f44807 100644 --- a/src/main/utils/clear-kube-env-vars.ts +++ b/src/main/utils/clear-kube-env-vars.ts @@ -13,7 +13,7 @@ const anyKubeconfig = /^kubeconfig$/i; * before KUBECONFIG and we only set KUBECONFIG. * @param env The current copy of env */ -export function clearKubeconfigEnvVars(env: Record): Record { +export function clearKubeconfigEnvVars(env: Partial>): Partial> { return Object.fromEntries( Object.entries(env) .filter(([key]) => anyKubeconfig.exec(key) === null), diff --git a/src/main/utils/resolve-system-proxy/resolve-system-proxy-channel-responder.injectable.ts b/src/main/utils/resolve-system-proxy/resolve-system-proxy-channel-responder.injectable.ts index c7a1747770..a2cd605633 100644 --- a/src/main/utils/resolve-system-proxy/resolve-system-proxy-channel-responder.injectable.ts +++ b/src/main/utils/resolve-system-proxy/resolve-system-proxy-channel-responder.injectable.ts @@ -2,20 +2,13 @@ * 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 resolveSystemProxyChannelInjectable from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-channel.injectable"; +import { resolveSystemProxyChannel } from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-channel"; +import { getRequestChannelListenerInjectable } from "../channel/channel-listeners/listener-tokens"; import resolveSystemProxyInjectable from "./resolve-system-proxy.injectable"; -import { requestChannelListenerInjectionToken } from "../../../common/utils/channel/request-channel-listener-injection-token"; -const resolveSystemProxyChannelResponderInjectable = getInjectable({ - id: "resolve-system-proxy-channel-responder", - - instantiate: (di) => ({ - channel: di.inject(resolveSystemProxyChannelInjectable), - handler: di.inject(resolveSystemProxyInjectable), - }), - - injectionToken: requestChannelListenerInjectionToken, +const resolveSystemProxyChannelResponderInjectable = getRequestChannelListenerInjectable({ + channel: resolveSystemProxyChannel, + handler: (di) => di.inject(resolveSystemProxyInjectable), }); export default resolveSystemProxyChannelResponderInjectable; diff --git a/src/main/utils/shell-env.ts b/src/main/utils/shell-env.ts deleted file mode 100644 index 6f4cce0be8..0000000000 --- a/src/main/utils/shell-env.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { spawn } from "child_process"; -import { randomUUID } from "crypto"; -import { basename } from "path"; -import { isWindows } from "../../common/vars"; -import logger from "../logger"; - -export type EnvironmentVariables = Partial>; - - -async function unixShellEnvironment(shell: string): Promise { - const runAsNode = process.env["ELECTRON_RUN_AS_NODE"]; - const noAttach = process.env["ELECTRON_NO_ATTACH_CONSOLE"]; - const env = { - ...process.env, - ELECTRON_RUN_AS_NODE: "1", - ELECTRON_NO_ATTACH_CONSOLE: "1", - }; - const mark = randomUUID().replace(/-/g, ""); - const regex = new RegExp(`${mark}(.*)${mark}`); - const shellName = basename(shell); - let command: string; - let shellArgs: string[]; - - if (/^pwsh(-preview)?$/.test(shellName)) { - // Older versions of PowerShell removes double quotes sometimes so we use "double single quotes" which is how - // you escape single quotes inside of a single quoted string. - command = `& '${process.execPath}' -p '''${mark}'' + JSON.stringify(process.env) + ''${mark}'''`; - shellArgs = ["-Login", "-Command"]; - } else { - command = `'${process.execPath}' -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`; - - if (shellName === "tcsh") { - shellArgs = ["-ic"]; - } else { - shellArgs = ["-ilc"]; - } - } - - return new Promise((resolve, reject) => { - const shellProcess = spawn(shell, [...shellArgs, command], { - detached: true, - stdio: ["ignore", "pipe", "pipe"], - env, - }); - const stdout: Buffer[] = []; - - shellProcess.on("error", (err) => reject(err)); - shellProcess.stdout.on("data", b => stdout.push(b)); - shellProcess.on("close", (code, signal) => { - if (code || signal) { - return reject(new Error(`Unexpected return code from spawned shell (code: ${code}, signal: ${signal})`)); - } - - try { - const rawOutput = Buffer.concat(stdout).toString("utf-8"); - const match = regex.exec(rawOutput); - const strippedRawOutput = match ? match[1] : "{}"; - const resolvedEnv = JSON.parse(strippedRawOutput); - - if (runAsNode) { - resolvedEnv["ELECTRON_RUN_AS_NODE"] = runAsNode; - } else { - delete resolvedEnv["ELECTRON_RUN_AS_NODE"]; - } - - if (noAttach) { - resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"] = noAttach; - } else { - delete resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"]; - } - - resolve(resolvedEnv); - } catch(err) { - reject(err); - } - }); - }); -} - -let shellSyncFailed = false; - -/** - * Attempts to get the shell environment per the user's existing startup scripts. - * If the environment can't be retrieved after 5 seconds an error message is logged. - * Subsequent calls after such a timeout simply log an error message without trying - * to get the environment, unless forceRetry is true. - * @param shell the shell to get the environment from - * @returns object containing the shell's environment variables. An empty object is - * returned if the call fails. - */ -export async function shellEnv(shell: string) : Promise { - if (isWindows) { - return {}; - } - - if (!shellSyncFailed) { - try { - return await Promise.race([ - unixShellEnvironment(shell), - new Promise((_resolve, reject) => setTimeout(() => { - reject(new Error("Resolving shell environment is taking very long. Please review your shell configuration.")); - }, 30_000)), - ]); - } catch (error) { - logger.error(`shellEnv: ${error}`); - shellSyncFailed = true; - } - } else { - logger.error("shellSync(): Resolving shell environment took too long. Please review your shell configuration."); - } - - return {}; -} diff --git a/src/main/utils/shell-env/compute-shell-environment.injectable.ts b/src/main/utils/shell-env/compute-shell-environment.injectable.ts new file mode 100644 index 0000000000..e3a60fcd03 --- /dev/null +++ b/src/main/utils/shell-env/compute-shell-environment.injectable.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AsyncResult } from "../../../common/utils/async-result"; +import { getInjectable } from "@ogre-tools/injectable"; +import isWindowsInjectable from "../../../common/vars/is-windows.injectable"; +import { disposer } from "../../../common/utils"; +import computeUnixShellEnvironmentInjectable from "./compute-unix-shell-environment.injectable"; + +export type EnvironmentVariables = Partial>; +export type ComputeShellEnvironment = (shell: string) => Promise>; + +const computeShellEnvironmentInjectable = getInjectable({ + id: "compute-shell-environment", + instantiate: (di): ComputeShellEnvironment => { + const isWindows = di.inject(isWindowsInjectable); + const computeUnixShellEnvironment = di.inject(computeUnixShellEnvironmentInjectable); + + if (isWindows) { + return async () => ({ + callWasSuccessful: true, + response: undefined, + }); + } + + return async (shell) => { + const controller = new AbortController(); + const shellEnv = computeUnixShellEnvironment(shell, { signal: controller.signal }); + const cleanup = disposer(); + + const timeoutHandle = setTimeout(() => controller.abort(), 30_000); + + cleanup.push(() => clearTimeout(timeoutHandle)); + + try { + return { + callWasSuccessful: true, + response: await shellEnv, + }; + } catch (error) { + if (controller.signal.aborted) { + return { + callWasSuccessful: false, + error: "Resolving shell environment is taking very long. Please review your shell configuration.", + }; + } + + return { + callWasSuccessful: false, + error: String(error), + }; + } finally { + cleanup(); + } + }; + }, +}); + +export default computeShellEnvironmentInjectable; + diff --git a/src/main/utils/shell-env/compute-unix-shell-environment.global-override-for-injectable.ts b/src/main/utils/shell-env/compute-unix-shell-environment.global-override-for-injectable.ts new file mode 100644 index 0000000000..aac810aa62 --- /dev/null +++ b/src/main/utils/shell-env/compute-unix-shell-environment.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../../../common/test-utils/get-global-override"; +import computeUnixShellEnvironmentInjectable from "./compute-unix-shell-environment.injectable"; + +export default getGlobalOverride(computeUnixShellEnvironmentInjectable, () => async () => { + throw new Error("Tried to get unix shell env without override"); +}); diff --git a/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts b/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts new file mode 100644 index 0000000000..7186e96a88 --- /dev/null +++ b/src/main/utils/shell-env/compute-unix-shell-environment.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 { spawn } from "child_process"; +import { randomUUID } from "crypto"; +import { basename } from "path"; +import type { EnvironmentVariables } from "./compute-shell-environment.injectable"; +import { getInjectable } from "@ogre-tools/injectable"; + +interface UnixShellEnvOptions { + signal?: AbortSignal; +} + +export type ComputeUnixShellEnvironment = (shell: string, opts?: UnixShellEnvOptions) => Promise; + +const computeUnixShellEnvironmentInjectable = getInjectable({ + id: "compute-unix-shell-environment", + instantiate: (): ComputeUnixShellEnvironment => async (shell, opts) => { + const runAsNode = process.env["ELECTRON_RUN_AS_NODE"]; + const noAttach = process.env["ELECTRON_NO_ATTACH_CONSOLE"]; + const env = { + ...process.env, + ELECTRON_RUN_AS_NODE: "1", + ELECTRON_NO_ATTACH_CONSOLE: "1", + }; + const mark = randomUUID().replace(/-/g, ""); + const regex = new RegExp(`${mark}(.*)${mark}`); + const shellName = basename(shell); + let command: string; + let shellArgs: string[]; + + if (/^pwsh(-preview)?$/.test(shellName)) { + // Older versions of PowerShell removes double quotes sometimes so we use "double single quotes" which is how + // you escape single quotes inside of a single quoted string. + command = `& '${process.execPath}' -p '''${mark}'' + JSON.stringify(process.env) + ''${mark}'''`; + shellArgs = ["-Login", "-Command"]; + } else { + command = `'${process.execPath}' -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`; + + if (shellName === "tcsh") { + shellArgs = ["-ic"]; + } else { + shellArgs = ["-ilc"]; + } + } + + return new Promise((resolve, reject) => { + const shellProcess = spawn(shell, [...shellArgs, command], { + detached: true, + stdio: ["ignore", "pipe", "pipe"], + env, + }); + const stdout: Buffer[] = []; + + opts?.signal?.addEventListener("abort", () => shellProcess.kill()); + + shellProcess.on("error", (err) => reject(err)); + shellProcess.stdout.on("data", b => stdout.push(b)); + shellProcess.on("close", (code, signal) => { + if (code || signal) { + return reject(new Error(`Unexpected return code from spawned shell (code: ${code}, signal: ${signal})`)); + } + + try { + const rawOutput = Buffer.concat(stdout).toString("utf-8"); + const match = regex.exec(rawOutput); + const strippedRawOutput = match ? match[1] : "{}"; + const resolvedEnv = JSON.parse(strippedRawOutput); + + if (runAsNode) { + resolvedEnv["ELECTRON_RUN_AS_NODE"] = runAsNode; + } else { + delete resolvedEnv["ELECTRON_RUN_AS_NODE"]; + } + + if (noAttach) { + resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"] = noAttach; + } else { + delete resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"]; + } + + resolve(resolvedEnv); + } catch(err) { + reject(err); + } + }); + }); + }, + causesSideEffects: true, +}); + +export default computeUnixShellEnvironmentInjectable; 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 index 5eb043291a..da78d76c36 100644 --- 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 @@ -2,30 +2,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 syncBoxInitialValueChannelInjectable from "../../../common/utils/sync-box/sync-box-initial-value-channel.injectable"; +import { syncBoxInitialValueChannel } from "../../../common/utils/sync-box/channels"; import { syncBoxInjectionToken } from "../../../common/utils/sync-box/sync-box-injection-token"; -import { requestChannelListenerInjectionToken } from "../../../common/utils/channel/request-channel-listener-injection-token"; +import { getRequestChannelListenerInjectable } from "../channel/channel-listeners/listener-tokens"; -const syncBoxInitialValueChannelListenerInjectable = getInjectable({ - id: "sync-box-initial-value-channel-listener", - - instantiate: (di) => { - const channel = di.inject(syncBoxInitialValueChannelInjectable); +const syncBoxInitialValueChannelListenerInjectable = getRequestChannelListenerInjectable({ + channel: syncBoxInitialValueChannel, + handler: (di) => { const syncBoxes = di.injectMany(syncBoxInjectionToken); - return { - channel, - - handler: () => - syncBoxes.map((box) => ({ - id: box.id, - value: box.value.get(), - })), - }; + return () => syncBoxes.map((box) => ({ + id: box.id, + value: box.value.get(), + })); }, - - injectionToken: requestChannelListenerInjectionToken, }); export default syncBoxInitialValueChannelListenerInjectable; diff --git a/src/migrations/weblinks-store/currentVersion.ts b/src/migrations/weblinks-store/currentVersion.ts index 40025e64cb..afa404847b 100644 --- a/src/migrations/weblinks-store/currentVersion.ts +++ b/src/migrations/weblinks-store/currentVersion.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { lensSlackWeblinkId, slackUrl } from "../../common/vars"; +import { docsUrl, lensDocumentationWeblinkId, lensSlackWeblinkId, slackUrl } from "../../common/vars"; import type { WeblinkData } from "../../common/weblink-store"; import type { MigrationDeclaration } from "../helpers"; import packageJson from "../../../package.json"; @@ -20,6 +20,12 @@ export default { slackWeblink.url = slackUrl; } + const docsWeblink = weblinks.find(weblink => weblink.id === lensDocumentationWeblinkId); + + if (docsWeblink) { + docsWeblink.url = docsUrl; + } + store.set("weblinks", weblinks); }, } as MigrationDeclaration; diff --git a/src/renderer/app-paths/setup-app-paths.injectable.ts b/src/renderer/app-paths/setup-app-paths.injectable.ts index 3660ad9db8..50ad4df34a 100644 --- a/src/renderer/app-paths/setup-app-paths.injectable.ts +++ b/src/renderer/app-paths/setup-app-paths.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/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 { appPathsChannel } from "../../common/app-paths/app-paths-channel"; import { requestFromChannelInjectionToken } from "../../common/utils/channel/request-from-channel-injection-token"; const setupAppPathsInjectable = getInjectable({ @@ -13,15 +13,12 @@ const setupAppPathsInjectable = getInjectable({ instantiate: (di) => { const requestFromChannel = di.inject(requestFromChannelInjectionToken); - const appPathsChannel = di.inject(appPathsChannelInjectable); const appPathsState = di.inject(appPathsStateInjectable); return { id: "setup-app-paths", run: async () => { - const appPaths = await requestFromChannel( - appPathsChannel, - ); + const appPaths = await requestFromChannel(appPathsChannel); appPathsState.set(appPaths); }, diff --git a/src/renderer/components/+extensions/attempt-install/install-request.ts b/src/renderer/components/+config-leases/index.ts similarity index 62% rename from src/renderer/components/+extensions/attempt-install/install-request.ts rename to src/renderer/components/+config-leases/index.ts index c5af4e5f93..dcef127fb2 100644 --- a/src/renderer/components/+extensions/attempt-install/install-request.ts +++ b/src/renderer/components/+config-leases/index.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 InstallRequest { - fileName: string; - dataP: Promise; -} + +export * from "./leases"; +export * from "./lease-details"; diff --git a/src/renderer/components/+config-leases/lease-details.scss b/src/renderer/components/+config-leases/lease-details.scss new file mode 100644 index 0000000000..33628f74dd --- /dev/null +++ b/src/renderer/components/+config-leases/lease-details.scss @@ -0,0 +1,6 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +.LeaseDetails {} \ No newline at end of file diff --git a/src/renderer/components/+config-leases/lease-details.tsx b/src/renderer/components/+config-leases/lease-details.tsx new file mode 100644 index 0000000000..617021edbf --- /dev/null +++ b/src/renderer/components/+config-leases/lease-details.tsx @@ -0,0 +1,51 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./lease-details.scss"; + +import React from "react"; +import { observer } from "mobx-react"; +import { DrawerItem } from "../drawer"; +import type { KubeObjectDetailsProps } from "../kube-object-details"; +import { KubeObjectMeta } from "../kube-object-meta"; +import type { Lease } from "../../../common/k8s-api/endpoints"; + +export interface LeaseDetailsProps extends KubeObjectDetailsProps { +} + +@observer +export class LeaseDetails extends React.Component { + + render() { + const { object: lease } = this.props; + + return ( +
+ + + + {lease.getHolderIdentity()} + + + + {lease.getLeaseDurationSeconds()} + + + + + + + + {lease.getRenewTime()} + + +
+ ); + } +} diff --git a/src/renderer/components/+config-leases/leases-route-component.injectable.ts b/src/renderer/components/+config-leases/leases-route-component.injectable.ts new file mode 100644 index 0000000000..ef0ffbf37d --- /dev/null +++ b/src/renderer/components/+config-leases/leases-route-component.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 { Leases } from "./leases"; +import leasesRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/leases/leases-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const leasesRouteComponentInjectable = getInjectable({ + id: "leases-route-component", + + instantiate: (di) => ({ + route: di.inject(leasesRouteInjectable), + Component: Leases, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default leasesRouteComponentInjectable; diff --git a/src/renderer/components/+config-leases/leases-sidebar-items.injectable.tsx b/src/renderer/components/+config-leases/leases-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..178cf8a172 --- /dev/null +++ b/src/renderer/components/+config-leases/leases-sidebar-items.injectable.tsx @@ -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 { computed } from "mobx"; + +import leasesRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/leases/leases-route.injectable"; +import { configSidebarItemId } from "../+config/config-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToLeasesInjectable from "../../../common/front-end-routing/routes/cluster/config/leases/navigate-to-leases.injectable"; + +const leasesSidebarItemsInjectable = getInjectable({ + id: "leases-sidebar-items", + + instantiate: (di) => { + const route = di.inject(leasesRouteInjectable); + const navigateToLeases = di.inject(navigateToLeasesInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "leases", + parentId: configSidebarItemId, + title: "Leases", + onClick: navigateToLeases, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 80, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default leasesSidebarItemsInjectable; diff --git a/src/renderer/components/+config-leases/leases.scss b/src/renderer/components/+config-leases/leases.scss new file mode 100644 index 0000000000..ab7cbce494 --- /dev/null +++ b/src/renderer/components/+config-leases/leases.scss @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +.Leases { + .TableCell { + &.name { + flex: 2; + } + + &.warning { + @include table-cell-warning; + } + + &.keys { + flex: 2.5; + } + + &.age { + flex: .5; + } + } +} \ No newline at end of file diff --git a/src/renderer/components/+config-leases/leases.tsx b/src/renderer/components/+config-leases/leases.tsx new file mode 100644 index 0000000000..1c665762ad --- /dev/null +++ b/src/renderer/components/+config-leases/leases.tsx @@ -0,0 +1,87 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./leases.scss"; + +import * as React from "react"; +import { observer } from "mobx-react"; +import type { Lease } from "../../../common/k8s-api/endpoints/lease.api"; +import { KubeObjectStatusIcon } from "../kube-object-status-icon"; +import type { KubeObjectDetailsProps } from "../kube-object-details"; +import { KubeObjectListLayout } from "../kube-object-list-layout"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; +import { KubeObjectAge } from "../kube-object/age"; +import { autoBind } from "../../../common/utils"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import leaseStoreInjectable from "./store.injectable"; +import type { LeaseStore } from "./store"; + +enum columnId { + name = "name", + namespace = "namespace", + holder = "holder", + age = "age", +} + +export interface LeaseProps extends KubeObjectDetailsProps { +} + +interface Dependencies { + leaseStore: LeaseStore; +} + +@observer +class NonInjectedLease extends React.Component { + constructor(props: LeaseProps & Dependencies) { + super(props); + autoBind(this); + } + + render() { + const { leaseStore } = this.props; + + return ( + + lease.getName(), + [columnId.namespace]: lease => lease.getNs(), + [columnId.holder]: lease => lease.getHolderIdentity(), + [columnId.age]: lease => -lease.getCreationTimestamp(), + }} + searchFilters={[ + lease => lease.getSearchFields(), + ]} + renderHeaderTitle="Leases" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Holder", className: "holder", sortBy: columnId.holder, id: columnId.holder }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={lease => [ + lease.getName(), + , + lease.getNs(), + lease.getHolderIdentity(), + , + ]} + /> + + ); + } +} + +export const Leases = withInjectables(NonInjectedLease, { + getProps: (di, props) => ({ + ...props, + leaseStore: di.inject(leaseStoreInjectable), + }), +}); diff --git a/src/renderer/components/+config-leases/store.injectable.ts b/src/renderer/components/+config-leases/store.injectable.ts new file mode 100644 index 0000000000..b9dda24b07 --- /dev/null +++ b/src/renderer/components/+config-leases/store.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 assert from "assert"; +import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/manager.injectable"; +import leaseApiInjectable from "../../../common/k8s-api/endpoints/lease.api.injectable"; +import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; +import { LeaseStore } from "./store"; + +const leaseStoreInjectable = getInjectable({ + id: "lease-store", + instantiate: (di) => { + assert(di.inject(storesAndApisCanBeCreatedInjectable), "leaseStore is only available in certain environments"); + + const api = di.inject(leaseApiInjectable); + + return new LeaseStore(api); + }, + injectionToken: kubeObjectStoreInjectionToken, +}); + +export default leaseStoreInjectable; diff --git a/src/renderer/components/+config-leases/store.ts b/src/renderer/components/+config-leases/store.ts new file mode 100644 index 0000000000..db19aed8dd --- /dev/null +++ b/src/renderer/components/+config-leases/store.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { Lease, LeaseApi } from "../../../common/k8s-api/endpoints/lease.api"; + +export class LeaseStore extends KubeObjectStore { +} diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 52843d8b3d..27db8da81a 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -25,23 +25,10 @@ import extensionInstallationStateStoreInjectable from "../../../../extensions/ex import { observable, when } from "mobx"; import type { DeleteFile } from "../../../../common/fs/delete-file.injectable"; import deleteFileInjectable from "../../../../common/fs/delete-file.injectable"; - -jest.mock("../../notifications"); - -jest.mock("../../../../common/utils/downloadFile", () => ({ - downloadFile: jest.fn(({ url }) => ({ - promise: Promise.resolve(), - url, - cancel: () => {}, - })), - downloadJson: jest.fn(({ url }) => ({ - promise: Promise.resolve({}), - url, - cancel: () => { }, - })), -})); - -jest.mock("../../../../common/utils/tar"); +import type { DownloadJson } from "../../../../common/fetch/download-json.injectable"; +import type { DownloadBinary } from "../../../../common/fetch/download-binary.injectable"; +import downloadJsonInjectable from "../../../../common/fetch/download-json.injectable"; +import downloadBinaryInjectable from "../../../../common/fetch/download-binary.injectable"; describe("Extensions", () => { let extensionLoader: ExtensionLoader; @@ -50,6 +37,8 @@ describe("Extensions", () => { let extensionInstallationStateStore: ExtensionInstallationStateStore; let render: DiRender; let deleteFileMock: jest.MockedFunction; + let downloadJson: jest.MockedFunction; + let downloadBinary: jest.MockedFunction; beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); @@ -65,6 +54,12 @@ describe("Extensions", () => { deleteFileMock = jest.fn(); di.override(deleteFileInjectable, () => deleteFileMock); + downloadJson = jest.fn().mockImplementation((url) => { throw new Error(`Unexpected call to downloadJson for url=${url}`); }); + di.override(downloadJsonInjectable, () => downloadJson); + + downloadBinary = jest.fn().mockImplementation((url) => { throw new Error(`Unexpected call to downloadJson for url=${url}`); }); + di.override(downloadBinaryInjectable, () => downloadBinary); + extensionLoader = di.inject(extensionLoaderInjectable); extensionDiscovery = di.inject(extensionDiscoveryInjectable); extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); @@ -110,7 +105,7 @@ describe("Extensions", () => { // Approve confirm dialog fireEvent.click(await screen.findByText("Yes")); - await waitFor(() => { + await waitFor(async () => { expect(extensionDiscovery.uninstallExtension).toHaveBeenCalled(); fireEvent.click(menuTrigger); expect(screen.getByText("Disable")).toHaveAttribute("aria-disabled", "true"); @@ -124,6 +119,7 @@ describe("Extensions", () => { render(); const resolveInstall = observable.box(false); + const url = "https://test.extensionurl/package.tgz"; deleteFileMock.mockReturnValue(Promise.resolve()); installExtensionFromInput.mockImplementation(async (input) => { @@ -139,13 +135,26 @@ describe("Extensions", () => { exact: false, }), { target: { - value: "https://test.extensionurl/package.tgz", + value: url, }, }); + const doResolve = observable.box(false); + + downloadBinary.mockImplementation(async (targetUrl) => { + expect(targetUrl).toBe(url); + + await when(() => doResolve.get()); + + return { + callWasSuccessful: false, + error: "unknown location", + }; + }); + fireEvent.click(await screen.findByText("Install")); expect((await screen.findByText("Install")).closest("button")).toBeDisabled(); - resolveInstall.set(true); + doResolve.set(true); }); it("displays spinner while extensions are loading", () => { diff --git a/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx b/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx index 36f6b2afc4..317162cc9f 100644 --- a/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx +++ b/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx @@ -2,8 +2,7 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { downloadFile, downloadJson } from "../../../common/utils"; -import { Notifications } from "../notifications"; +import { isObject } from "../../../common/utils"; import React from "react"; import { SemVer } from "semver"; import URLParse from "url-parse"; @@ -14,6 +13,12 @@ import extensionInstallationStateStoreInjectable from "../../../extensions/exten import confirmInjectable from "../confirm-dialog/confirm.injectable"; import { reduce } from "lodash"; import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; +import { withTimeout } from "../../../common/fetch/timeout-controller"; +import downloadBinaryInjectable from "../../../common/fetch/download-binary.injectable"; +import downloadJsonInjectable from "../../../common/fetch/download-json.injectable"; +import type { PackageJson } from "type-fest"; +import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; export interface ExtensionInfo { name: string; @@ -21,6 +26,19 @@ export interface ExtensionInfo { requireConfirmation?: boolean; } +interface NpmPackageVersionDescriptor extends PackageJson { + dist: { + integrity: string; + shasum: string; + tarball: string; + }; +} + +interface NpmRegistryPackageDescriptor { + versions: Partial>; + "dist-tags"?: Partial>; +} + export type AttemptInstallByInfo = (info: ExtensionInfo) => Promise; const attemptInstallByInfoInjectable = getInjectable({ @@ -31,84 +49,131 @@ const attemptInstallByInfoInjectable = getInjectable({ const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); const confirm = di.inject(confirmInjectable); const getBasenameOfPath = di.inject(getBasenameOfPathInjectable); + const downloadJson = di.inject(downloadJsonInjectable); + const downloadBinary = di.inject(downloadBinaryInjectable); + const showErrorNotification = di.inject(showErrorNotificationInjectable); + const logger = di.inject(loggerInjectable); return async (info) => { - const { name, version, requireConfirmation = false } = info; + const { name, version: versionOrTagName, requireConfirmation = false } = info; const disposer = extensionInstallationStateStore.startPreInstall(); const baseUrl = await getBaseRegistryUrl(); const registryUrl = new URLParse(baseUrl).set("pathname", name).toString(); - let json: any; - let finalVersion = version; + let json: NpmRegistryPackageDescriptor; try { - json = await downloadJson({ url: registryUrl }).promise; + const result = await downloadJson(registryUrl); - if (!json || json.error || typeof json.versions !== "object" || !json.versions) { - const message = json?.error ? `: ${json.error}` : ""; - - Notifications.error(`Failed to get registry information for that extension${message}`); + if (!result.callWasSuccessful) { + showErrorNotification(`Failed to get registry information for extension: ${result.error}`); return disposer(); } + + if (!isObject(result.response) || Array.isArray(result.response)) { + showErrorNotification("Failed to get registry information for extension"); + + return disposer(); + } + + if (result.response.error || !isObject(result.response.versions)) { + const message = result.response.error ? `: ${result.response.error}` : ""; + + showErrorNotification(`Failed to get registry information for extension${message}`); + + return disposer(); + } + + json = result.response as unknown as NpmRegistryPackageDescriptor; } catch (error) { if (error instanceof SyntaxError) { - // assume invalid JSON - console.warn("Set registry has invalid json", { url: baseUrl }, error); - Notifications.error("Failed to get valid registry information for that extension. Registry did not return valid JSON"); + // assume invalid JSON + logger.warn("Set registry has invalid json", { url: baseUrl }, error); + showErrorNotification("Failed to get valid registry information for extension. Registry did not return valid JSON"); } else { - console.error("Failed to download registry information", error); - Notifications.error(`Failed to get valid registry information for that extension. ${error}`); + logger.error("Failed to download registry information", error); + showErrorNotification(`Failed to get valid registry information for extension. ${error}`); } return disposer(); } - if (version) { - if (!json.versions[version]) { - if (json["dist-tags"][version]) { - finalVersion = json["dist-tags"][version]; - } else { - Notifications.error(( -

- {"The "} - {name} - {" extension does not have a version or tag "} - {version} - . -

- )); + let version = versionOrTagName; - return disposer(); + if (versionOrTagName) { + validDistTagName: + if (!json.versions[versionOrTagName]) { + if (json["dist-tags"]) { + const potentialVersion = json["dist-tags"][versionOrTagName]; + + if (potentialVersion) { + if (!json.versions[potentialVersion]) { + showErrorNotification(( +

+ Configured registry claims to have tag + {" "} + {versionOrTagName} + . + {" "} + But does not have version infomation for the reference. +

+ )); + + return disposer(); + } + + version = potentialVersion; + break validDistTagName; + } } + + showErrorNotification(( +

+ {"The "} + {name} + {" extension does not have a version or tag "} + {versionOrTagName} + . +

+ )); + + return disposer(); } } else { const versions = Object.keys(json.versions) .map(version => new SemVer(version, { loose: true })) - // ignore pre-releases for auto picking the version + // ignore pre-releases for auto picking the version .filter(version => version.prerelease.length === 0); const latestVersion = reduce(versions, (prev, curr) => prev.compareMain(curr) === -1 ? curr : prev); - if (!latestVersion) { - console.error("No versions supplied for that extension", { name }); - Notifications.error(`No versions found for ${name}`); + version = latestVersion?.format(); + } - return disposer(); - } + if (!version) { + logger.error("No versions supplied for extension", { name }); + showErrorNotification(`No versions found for ${name}`); - finalVersion = latestVersion.format(); + return disposer(); + } + + const versionInfo = json.versions[version]; + const tarballUrl = versionInfo?.dist.tarball; + + if (!tarballUrl) { + showErrorNotification("Configured registry has invalid data model. Please verify that it is like NPM's."); + logger.warn(`[ATTEMPT-INSTALL-BY-INFO]: registry returned unexpected data, final version is ${version} but the versions object is missing .dist.tarball as a string`, versionInfo); + + return disposer(); } if (requireConfirmation) { const proceed = await confirm({ message: (

- Are you sure you want to install - {" "} + {"Are you sure you want to install "} - {name} - @ - {finalVersion} + {`${name}@${version}`} ?

@@ -122,12 +187,17 @@ const attemptInstallByInfoInjectable = getInjectable({ } } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const url = json.versions[finalVersion!].dist.tarball; - const fileName = getBasenameOfPath(url); - const { promise: dataP } = downloadFile({ url, timeout: 10 * 60 * 1000 }); + const fileName = getBasenameOfPath(tarballUrl); + const { signal } = withTimeout(10 * 60 * 1000); + const request = await downloadBinary(tarballUrl, { signal }); - return attemptInstall({ fileName, dataP }, disposer); + if (!request.callWasSuccessful) { + showErrorNotification(`Failed to download extension: ${request.error}`); + + return disposer(); + } + + return attemptInstall({ fileName, data: request.response }, disposer); }; }, }); diff --git a/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.ts b/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.ts deleted file mode 100644 index 21ceb73ecf..0000000000 --- a/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.ts +++ /dev/null @@ -1,30 +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 extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; -import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable"; -import { attemptInstall } from "./attempt-install"; -import unpackExtensionInjectable from "./unpack-extension/unpack-extension.injectable"; -import getExtensionDestFolderInjectable - from "./get-extension-dest-folder/get-extension-dest-folder.injectable"; -import createTempFilesAndValidateInjectable from "./create-temp-files-and-validate/create-temp-files-and-validate.injectable"; -import extensionInstallationStateStoreInjectable - from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; - -const attemptInstallInjectable = getInjectable({ - id: "attempt-install", - - instantiate: (di) => - attemptInstall({ - extensionLoader: di.inject(extensionLoaderInjectable), - uninstallExtension: di.inject(uninstallExtensionInjectable), - unpackExtension: di.inject(unpackExtensionInjectable), - createTempFilesAndValidate: di.inject(createTempFilesAndValidateInjectable), - getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable), - extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), - }), -}); - -export default attemptInstallInjectable; diff --git a/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx b/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx new file mode 100644 index 0000000000..cb73df7237 --- /dev/null +++ b/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx @@ -0,0 +1,148 @@ +/** + * 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 extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; +import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable"; +import type { UnpackExtension } from "./unpack-extension.injectable"; +import unpackExtensionInjectable from "./unpack-extension.injectable"; +import type { GetExtensionDestFolder } from "./get-extension-dest-folder.injectable"; +import getExtensionDestFolderInjectable from "./get-extension-dest-folder.injectable"; +import type { CreateTempFilesAndValidate } from "./create-temp-files-and-validate.injectable"; +import createTempFilesAndValidateInjectable from "./create-temp-files-and-validate.injectable"; +import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import type { Disposer } from "../../../../common/utils"; +import { disposer } from "../../../../common/utils"; +import { Notifications } from "../../notifications"; +import { Button } from "../../button"; +import type { ExtensionLoader } from "../../../../extensions/extension-loader"; +import type { LensExtensionId } from "../../../../extensions/lens-extension"; +import React from "react"; +import { remove as removeDir } from "fs-extra"; +import { shell } from "electron"; +import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; +import { ExtensionInstallationState } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; + +export interface InstallRequest { + fileName: string; + data: Buffer; +} + +interface Dependencies { + extensionLoader: ExtensionLoader; + uninstallExtension: (id: LensExtensionId) => Promise; + unpackExtension: UnpackExtension; + createTempFilesAndValidate: CreateTempFilesAndValidate; + getExtensionDestFolder: GetExtensionDestFolder; + installStateStore: ExtensionInstallationStateStore; +} + +export type AttemptInstall = (request: InstallRequest, cleanup?: Disposer) => Promise; + +const attemptInstall = ({ + extensionLoader, + uninstallExtension, + unpackExtension, + createTempFilesAndValidate, + getExtensionDestFolder, + installStateStore, +}: Dependencies): AttemptInstall => + async (request, cleanup) => { + const dispose = disposer( + installStateStore.startPreInstall(), + cleanup, + ); + + const validatedRequest = await createTempFilesAndValidate(request); + + if (!validatedRequest) { + return dispose(); + } + + const { name, version, description } = validatedRequest.manifest; + const curState = installStateStore.getInstallationState(validatedRequest.id); + + if (curState !== ExtensionInstallationState.IDLE) { + dispose(); + + return void Notifications.error( +
+ Extension Install Collision: +

+ {"The "} + {name} + {` extension is currently ${curState.toLowerCase()}.`} +

+

Will not proceed with this current install request.

+
, + ); + } + + const extensionFolder = getExtensionDestFolder(name); + const installedExtension = extensionLoader.getExtension(validatedRequest.id); + + if (installedExtension) { + const { version: oldVersion } = installedExtension.manifest; + + // confirm to uninstall old version before installing new version + const removeNotification = Notifications.info( +
+
+

+ {"Install extension "} + {`${name}@${version}`} + ? +

+

+ {"Description: "} + {description} +

+
shell.openPath(extensionFolder)} + > + Warning: + {` ${name}@${oldVersion} will be removed before installation.`} +
+
+
, + { + onClose: dispose, + }, + ); + } else { + // clean up old data if still around + await removeDir(extensionFolder); + + // install extension if not yet exists + await unpackExtension(validatedRequest, dispose); + } + }; + +const attemptInstallInjectable = getInjectable({ + id: "attempt-install", + instantiate: (di) => attemptInstall({ + extensionLoader: di.inject(extensionLoaderInjectable), + uninstallExtension: di.inject(uninstallExtensionInjectable), + unpackExtension: di.inject(unpackExtensionInjectable), + createTempFilesAndValidate: di.inject(createTempFilesAndValidateInjectable), + getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable), + installStateStore: di.inject(extensionInstallationStateStoreInjectable), + }), +}); + +export default attemptInstallInjectable; diff --git a/src/renderer/components/+extensions/attempt-install/attempt-install.tsx b/src/renderer/components/+extensions/attempt-install/attempt-install.tsx deleted file mode 100644 index de8c968201..0000000000 --- a/src/renderer/components/+extensions/attempt-install/attempt-install.tsx +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { ExtendableDisposer } from "../../../../common/utils"; -import { disposer } from "../../../../common/utils"; -import { Notifications } from "../../notifications"; -import { Button } from "../../button"; -import type { ExtensionLoader } from "../../../../extensions/extension-loader"; -import type { LensExtensionId } from "../../../../extensions/lens-extension"; -import React from "react"; -import { remove as removeDir } from "fs-extra"; -import { shell } from "electron"; -import type { InstallRequest } from "./install-request"; -import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; -import { ExtensionInstallationState } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; -import type { UnpackExtension } from "./unpack-extension/unpack-extension.injectable"; -import type { CreateTempFilesAndValidate } from "./create-temp-files-and-validate/create-temp-files-and-validate.injectable"; -import type { GetExtensionDestFolder } from "./get-extension-dest-folder/get-extension-dest-folder.injectable"; - -interface Dependencies { - extensionLoader: ExtensionLoader; - uninstallExtension: (id: LensExtensionId) => Promise; - unpackExtension: UnpackExtension; - createTempFilesAndValidate: CreateTempFilesAndValidate; - getExtensionDestFolder: GetExtensionDestFolder; - extensionInstallationStateStore: ExtensionInstallationStateStore; -} - -export const attemptInstall = - ({ - extensionLoader, - uninstallExtension, - unpackExtension, - createTempFilesAndValidate, - getExtensionDestFolder, - extensionInstallationStateStore, - }: Dependencies) => - async (request: InstallRequest, d?: ExtendableDisposer): Promise => { - console.log("Attempting to install extension"); - - const dispose = disposer( - extensionInstallationStateStore.startPreInstall(), - d, - ); - - const validatedRequest = await createTempFilesAndValidate(request); - - if (!validatedRequest) { - return dispose(); - } - - const { name, version, description } = validatedRequest.manifest; - const curState = extensionInstallationStateStore.getInstallationState( - validatedRequest.id, - ); - - if (curState !== ExtensionInstallationState.IDLE) { - dispose(); - - return void Notifications.error( -
- Extension Install Collision: -

- {"The "} - {name} - {` extension is currently ${curState.toLowerCase()}.`} -

-

Will not proceed with this current install request.

-
, - ); - } - - const extensionFolder = getExtensionDestFolder(name); - const installedExtension = extensionLoader.getExtension(validatedRequest.id); - - if (installedExtension) { - const { version: oldVersion } = installedExtension.manifest; - - // confirm to uninstall old version before installing new version - const removeNotification = Notifications.info( -
-
-

- {"Install extension "} - {`${name}@${version}`} - ? -

-

- {"Description: "} - {description} -

-
shell.openPath(extensionFolder)} - > - Warning: - {` ${name}@${oldVersion} will be removed before installation.`} -
-
-
, - { - onClose: dispose, - }, - ); - } else { - // clean up old data if still around - await removeDir(extensionFolder); - - // install extension if not yet exists - await unpackExtension(validatedRequest, dispose); - } - }; diff --git a/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate.injectable.tsx b/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate.injectable.tsx new file mode 100644 index 0000000000..e9fea96e44 --- /dev/null +++ b/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate.injectable.tsx @@ -0,0 +1,97 @@ +/** + * 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 extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable"; +import { validatePackage } from "./validate-package"; +import type { ExtensionDiscovery } from "../../../../extensions/extension-discovery/extension-discovery"; +import { getMessageFromError } from "../get-message-from-error/get-message-from-error"; +import logger from "../../../../main/logger"; +import { Notifications } from "../../notifications"; +import path from "path"; +import fse from "fs-extra"; +import React from "react"; +import os from "os"; +import type { LensExtensionId, LensExtensionManifest } from "../../../../extensions/lens-extension"; +import type { InstallRequest } from "./attempt-install.injectable"; + +export interface InstallRequestValidated { + fileName: string; + data: Buffer; + id: LensExtensionId; + manifest: LensExtensionManifest; + tempFile: string; // temp system path to packed extension for unpacking +} + +interface Dependencies { + extensionDiscovery: ExtensionDiscovery; +} + +export type CreateTempFilesAndValidate = (request: InstallRequest) => Promise; + +const createTempFilesAndValidate = ({ + extensionDiscovery, +}: Dependencies): CreateTempFilesAndValidate => ( + async ({ fileName, data }) => { + // copy files to temp + await fse.ensureDir(getExtensionPackageTemp()); + + // validate packages + const tempFile = getExtensionPackageTemp(fileName); + + try { + await fse.writeFile(tempFile, data); + const manifest = await validatePackage(tempFile); + const id = path.join( + extensionDiscovery.nodeModulesPath, + manifest.name, + "package.json", + ); + + return { + fileName, + data, + manifest, + tempFile, + id, + }; + } catch (error) { + const message = getMessageFromError(error); + + logger.info( + `[EXTENSION-INSTALLATION]: installing ${fileName} has failed: ${message}`, + { error }, + ); + Notifications.error(( +
+

+ {"Installing "} + {fileName} + {" has failed, skipping."} +

+

+ {"Reason: "} + {message} +

+
+ )); + } + + return null; + } +); + + +function getExtensionPackageTemp(fileName = "") { + return path.join(os.tmpdir(), "lens-extensions", fileName); +} + +const createTempFilesAndValidateInjectable = getInjectable({ + id: "create-temp-files-and-validate", + instantiate: (di) => createTempFilesAndValidate({ + extensionDiscovery: di.inject(extensionDiscoveryInjectable), + }), +}); + +export default createTempFilesAndValidateInjectable; diff --git a/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.injectable.tsx b/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.injectable.tsx deleted file mode 100644 index 951c45d246..0000000000 --- a/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.injectable.tsx +++ /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 { getInjectable } from "@ogre-tools/injectable"; -import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable"; -import React from "react"; -import type { LensExtensionId, LensExtensionManifest } from "../../../../../extensions/lens-extension"; -import { getMessageFromError } from "../../get-message-from-error/get-message-from-error"; -import type { InstallRequest } from "../install-request"; -import { validatePackage } from "../validate-package/validate-package"; -import joinPathsInjectable from "../../../../../common/path/join-paths.injectable"; -import tempDirectoryPathInjectable from "../../../../../common/os/temp-directory-path.injectable"; -import ensureDirInjectable from "../../../../../common/fs/ensure-dir.injectable"; -import writeFileInjectable from "../../../../../common/fs/write-file.injectable"; -import loggerInjectable from "../../../../../common/logger.injectable"; -import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable"; - -export interface InstallRequestValidated { - fileName: string; - data: Buffer; - id: LensExtensionId; - manifest: LensExtensionManifest; - tempFile: string; // temp system path to packed extension for unpacking -} - -export type CreateTempFilesAndValidate = (req: InstallRequest) => Promise; - -const createTempFilesAndValidateInjectable = getInjectable({ - id: "create-temp-files-and-validate", - - instantiate: (di) => { - const extensionDiscovery = di.inject(extensionDiscoveryInjectable); - const joinPaths = di.inject(joinPathsInjectable); - const tempDirectoryPath = di.inject(tempDirectoryPathInjectable); - const ensureDir = di.inject(ensureDirInjectable); - const writeFile = di.inject(writeFileInjectable); - const logger = di.inject(loggerInjectable); - const showErrorNotification = di.inject(showErrorNotificationInjectable); - - const baseTempExtensionsDirectory = joinPaths(tempDirectoryPath, "lens-extensions"); - const getExtensionPackageTemp = (fileName: string) => joinPaths(baseTempExtensionsDirectory, fileName); - - return async ({ - fileName, - dataP, - }: InstallRequest): Promise => { - // copy files to temp - await ensureDir(baseTempExtensionsDirectory); - - // validate packages - const tempFile = getExtensionPackageTemp(fileName); - - try { - const data = await dataP; - - if (!data) { - return null; - } - - await writeFile(tempFile, data); - logger.info("validating package", tempFile); - const manifest = await validatePackage(tempFile); - const id = joinPaths( - extensionDiscovery.nodeModulesPath, - manifest.name, - "package.json", - ); - - return { - fileName, - data, - manifest, - tempFile, - id, - }; - } catch (error) { - const message = getMessageFromError(error); - - logger.info( - `[EXTENSION-INSTALLATION]: installing ${fileName} has failed: ${message}`, - { error }, - ); - showErrorNotification( -
-

- {"Installing "} - {fileName} - {" has failed, skipping."} -

-

- {"Reason: "} - {message} -

-
, - ); - } - - return null; - }; - }, -}); - -export default createTempFilesAndValidateInjectable; diff --git a/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.injectable.ts b/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder.injectable.ts similarity index 51% rename from src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.injectable.ts rename to src/renderer/components/+extensions/attempt-install/get-extension-dest-folder.injectable.ts index 2c46b595a4..6d6dbfbef3 100644 --- a/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.injectable.ts +++ b/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder.injectable.ts @@ -3,20 +3,19 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import joinPathsInjectable from "../../../../../common/path/join-paths.injectable"; -import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable"; -import { sanitizeExtensionName } from "../../../../../extensions/lens-extension"; +import extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable"; +import { sanitizeExtensionName } from "../../../../extensions/lens-extension"; +import path from "path"; -export type GetExtensionDestFolder = (extensionName: string) => string; +export type GetExtensionDestFolder = (name: string) => string; const getExtensionDestFolderInjectable = getInjectable({ id: "get-extension-dest-folder", instantiate: (di): GetExtensionDestFolder => { const extensionDiscovery = di.inject(extensionDiscoveryInjectable); - const joinPaths = di.inject(joinPathsInjectable); - return (name) => joinPaths(extensionDiscovery.localFolderPath, sanitizeExtensionName(name)); + return (name) => path.join(extensionDiscovery.localFolderPath, sanitizeExtensionName(name)); }, }); diff --git a/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx b/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx new file mode 100644 index 0000000000..c81a69f9b0 --- /dev/null +++ b/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx @@ -0,0 +1,115 @@ +/** + * 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 extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; +import getExtensionDestFolderInjectable from "./get-extension-dest-folder.injectable"; +import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import type { Disposer } from "../../../../common/utils"; +import { noop } from "../../../../common/utils"; +import { extensionDisplayName } from "../../../../extensions/lens-extension"; +import { getMessageFromError } from "../get-message-from-error/get-message-from-error"; +import path from "path"; +import fse from "fs-extra"; +import { when } from "mobx"; +import React from "react"; +import type { InstallRequestValidated } from "./create-temp-files-and-validate.injectable"; +import extractTarInjectable from "../../../../common/fs/extract-tar.injectable"; +import loggerInjectable from "../../../../common/logger.injectable"; +import showInfoNotificationInjectable from "../../notifications/show-info-notification.injectable"; +import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable"; + +export type UnpackExtension = (request: InstallRequestValidated, disposeDownloading?: Disposer) => Promise; + +const unpackExtensionInjectable = getInjectable({ + id: "unpack-extension", + instantiate: (di): UnpackExtension => { + const extensionLoader = di.inject(extensionLoaderInjectable); + const getExtensionDestFolder = di.inject(getExtensionDestFolderInjectable); + const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); + const extractTar = di.inject(extractTarInjectable); + const logger = di.inject(loggerInjectable); + const showInfoNotification = di.inject(showInfoNotificationInjectable); + const showErrorNotification = di.inject(showErrorNotificationInjectable); + + return async (request, disposeDownloading) => { + const { + id, + fileName, + tempFile, + manifest: { name, version }, + } = request; + + extensionInstallationStateStore.setInstalling(id); + disposeDownloading?.(); + + const displayName = extensionDisplayName(name, version); + const extensionFolder = getExtensionDestFolder(name); + const unpackingTempFolder = path.join( + path.dirname(tempFile), + `${path.basename(tempFile)}-unpacked`, + ); + + logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile }); + + try { + // extract to temp folder first + await fse.remove(unpackingTempFolder).catch(noop); + await fse.ensureDir(unpackingTempFolder); + await extractTar(tempFile, { cwd: unpackingTempFolder }); + + // move contents to extensions folder + const unpackedFiles = await fse.readdir(unpackingTempFolder); + let unpackedRootFolder = unpackingTempFolder; + + if (unpackedFiles.length === 1) { + // check if %extension.tgz was packed with single top folder, + // e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball + unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]); + } + + await fse.ensureDir(extensionFolder); + await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true }); + + // wait for the loader has actually install it + await when(() => extensionLoader.userExtensions.has(id)); + + // Enable installed extensions by default. + extensionLoader.setIsEnabled(id, true); + + showInfoNotification(( +

+ {"Extension "} + {displayName} + {" successfully installed!"} +

+ )); + } catch (error) { + const message = getMessageFromError(error); + + logger.info( + `[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`, + { error }, + ); + showErrorNotification(( +

+ {"Installing extension "} + {displayName} + {" has failed: "} + {message} +

+ )); + } finally { + // Remove install state once finished + extensionInstallationStateStore.clearInstalling(id); + + // clean up + fse.remove(unpackingTempFolder).catch(noop); + fse.unlink(tempFile).catch(noop); + } + }; + }, +}); + +export default unpackExtensionInjectable; diff --git a/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx b/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx deleted file mode 100644 index 9a10062f7e..0000000000 --- a/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import React from "react"; -import { getInjectable } from "@ogre-tools/injectable"; -import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable"; -import getExtensionDestFolderInjectable from "../get-extension-dest-folder/get-extension-dest-folder.injectable"; -import extensionInstallationStateStoreInjectable from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; -import type { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate.injectable"; -import type { Disposer } from "../../../../utils"; -import { noop } from "../../../../utils"; -import { extensionDisplayName } from "../../../../../extensions/lens-extension"; -import joinPathsInjectable from "../../../../../common/path/join-paths.injectable"; -import loggerInjectable from "../../../../../common/logger.injectable"; -import { when } from "mobx"; -import { getMessageFromError } from "../../get-message-from-error/get-message-from-error"; -import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable"; -import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable"; -import getDirnameOfPathInjectable from "../../../../../common/path/get-dirname.injectable"; -import getBasenameOfPathInjectable from "../../../../../common/path/get-basename.injectable"; -import extractTarInjectable from "../../../../../common/fs/extract-tar.injectable"; -import ensureDirInjectable from "../../../../../common/fs/ensure-dir.injectable"; -import removePathInjectable from "../../../../../common/fs/remove-path.injectable"; -import deleteFileInjectable from "../../../../../common/fs/delete-file.injectable"; -import readDirectoryInjectable from "../../../../../common/fs/read-directory.injectable"; -import moveInjectable from "../../../../../common/fs/move.injectable"; - -export type UnpackExtension = (request: InstallRequestValidated, disposeDownloading?: Disposer) => Promise; - -const unpackExtensionInjectable = getInjectable({ - id: "unpack-extension", - - instantiate: (di): UnpackExtension => { - const extensionLoader = di.inject(extensionLoaderInjectable); - const getExtensionDestFolder = di.inject(getExtensionDestFolderInjectable); - const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); - const joinPaths = di.inject(joinPathsInjectable); - const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); - const getBasenameOfPath = di.inject(getBasenameOfPathInjectable); - const logger = di.inject(loggerInjectable); - const showOkNotification = di.inject(showSuccessNotificationInjectable); - const showErrorNotification = di.inject(showErrorNotificationInjectable); - const extractTar = di.inject(extractTarInjectable); - const ensureDir = di.inject(ensureDirInjectable); - const removePath = di.inject(removePathInjectable); - const deleteFile = di.inject(deleteFileInjectable); - const readDirectory = di.inject(readDirectoryInjectable); - const move = di.inject(moveInjectable); - - return async (request, disposeDownloading) => { - const { - id, - fileName, - tempFile, - manifest: { name, version }, - } = request; - - extensionInstallationStateStore.setInstalling(id); - disposeDownloading?.(); - - const displayName = extensionDisplayName(name, version); - const extensionFolder = getExtensionDestFolder(name); - const unpackingTempFolder = joinPaths( - getDirnameOfPath(tempFile), - `${getBasenameOfPath(tempFile)}-unpacked`, - ); - - logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile }); - - try { - // extract to temp folder first - await removePath(unpackingTempFolder).catch(noop); - await ensureDir(unpackingTempFolder); - await extractTar(tempFile, { cwd: unpackingTempFolder }); - - // move contents to extensions folder - const unpackedFiles = await readDirectory(unpackingTempFolder); - let unpackedRootFolder = unpackingTempFolder; - - if (unpackedFiles.length === 1) { - // check if %extension.tgz was packed with single top folder, - // e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball - unpackedRootFolder = joinPaths(unpackingTempFolder, unpackedFiles[0]); - } - - await ensureDir(extensionFolder); - await move(unpackedRootFolder, extensionFolder, { overwrite: true }); - - // wait for the loader has actually install it - await when(() => extensionLoader.userExtensions.has(id)); - - // Enable installed extensions by default. - extensionLoader.setIsEnabled(id, true); - - showOkNotification( -

- {"Extension "} - {displayName} - {" successfully installed!"} -

, - ); - } catch (error) { - const message = getMessageFromError(error); - - logger.info( - `[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`, - { error }, - ); - showErrorNotification( -

- {"Installing extension "} - {displayName} - {" has failed: "} - {message} -

, - ); - } finally { - // Remove install state once finished - extensionInstallationStateStore.clearInstalling(id); - - // clean up - removePath(unpackingTempFolder).catch(noop); - deleteFile(tempFile).catch(noop); - } - }; - }, -}); - -export default unpackExtensionInjectable; diff --git a/src/renderer/components/+extensions/attempt-install/validate-package/validate-package.tsx b/src/renderer/components/+extensions/attempt-install/validate-package.tsx similarity index 69% rename from src/renderer/components/+extensions/attempt-install/validate-package/validate-package.tsx rename to src/renderer/components/+extensions/attempt-install/validate-package.tsx index ad3432574b..e9597b2d10 100644 --- a/src/renderer/components/+extensions/attempt-install/validate-package/validate-package.tsx +++ b/src/renderer/components/+extensions/attempt-install/validate-package.tsx @@ -2,27 +2,23 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { LensExtensionManifest } from "../../../../../extensions/lens-extension"; -import { hasTypedProperty, isObject, isString, listTarEntries, readFileFromTar } from "../../../../../common/utils"; -import { manifestFilename } from "../../../../../extensions/extension-discovery/extension-discovery"; +import type { LensExtensionManifest } from "../../../../extensions/lens-extension"; +import { hasTypedProperty, isObject, isString, listTarEntries, readFileFromTar } from "../../../../common/utils"; +import { manifestFilename } from "../../../../extensions/extension-discovery/extension-discovery"; import path from "path"; -export const validatePackage = async ( - filePath: string, -): Promise => { +export async function validatePackage(filePath: string): Promise { const tarFiles = await listTarEntries(filePath); // tarball from npm contains single root folder "package/*" const firstFile = tarFiles[0]; if (!firstFile) { - throw new Error(`invalid extension bundle, ${manifestFilename} not found`); + throw new Error(`invalid extension bundle, ${manifestFilename} not found`); } const rootFolder = path.normalize(firstFile).split(path.sep)[0]; - const packedInRootFolder = tarFiles.every(entry => - entry.startsWith(rootFolder), - ); + const packedInRootFolder = tarFiles.every(entry => entry.startsWith(rootFolder)); const manifestLocation = packedInRootFolder ? path.join(rootFolder, manifestFilename) : manifestFilename; @@ -48,4 +44,4 @@ export const validatePackage = async ( } throw new Error(`${manifestFilename} must specify "main" and/or "renderer" fields`); -}; +} diff --git a/src/renderer/components/+extensions/attempt-installs/attempt-installs.injectable.ts b/src/renderer/components/+extensions/attempt-installs.injectable.ts similarity index 53% rename from src/renderer/components/+extensions/attempt-installs/attempt-installs.injectable.ts rename to src/renderer/components/+extensions/attempt-installs.injectable.ts index 78939ef264..8f2a066492 100644 --- a/src/renderer/components/+extensions/attempt-installs/attempt-installs.injectable.ts +++ b/src/renderer/components/+extensions/attempt-installs.injectable.ts @@ -3,9 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import getBasenameOfPathInjectable from "../../../../common/path/get-basename.injectable"; -import attemptInstallInjectable from "../attempt-install/attempt-install.injectable"; -import readFileNotifyInjectable from "../read-file-notify/read-file-notify.injectable"; +import attemptInstallInjectable from "./attempt-install/attempt-install.injectable"; +import path from "path"; +import readFileNotifyInjectable from "./read-file-notify/read-file-notify.injectable"; export type AttemptInstalls = (filePaths: string[]) => Promise; @@ -14,16 +14,23 @@ const attemptInstallsInjectable = getInjectable({ instantiate: (di): AttemptInstalls => { const attemptInstall = di.inject(attemptInstallInjectable); - const getBasenameOfPath = di.inject(getBasenameOfPathInjectable); const readFileNotify = di.inject(readFileNotifyInjectable); return async (filePaths) => { - await Promise.allSettled(filePaths.map(filePath => ( - attemptInstall({ - fileName: getBasenameOfPath(filePath), - dataP: readFileNotify(filePath), - }) - ))); + await Promise.allSettled( + filePaths.map(async filePath => { + const data = await readFileNotify(filePath); + + if (!data) { + return; + } + + return attemptInstall({ + fileName: path.basename(filePath), + data, + }); + }), + ); }; }, }); diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 933dab8a9a..b9dfdb791b 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -32,7 +32,8 @@ import type { InstallExtensionFromInput } from "./install-extension-from-input.i import installExtensionFromInputInjectable from "./install-extension-from-input.injectable"; import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable"; import type { LensExtensionId } from "../../../extensions/lens-extension"; -import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable"; +import type { InstallOnDrop } from "./install-on-drop.injectable"; +import installOnDropInjectable from "./install-on-drop.injectable"; import { supportedExtensionFormats } from "./supported-extension-formats"; import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store"; @@ -44,7 +45,7 @@ interface Dependencies { confirmUninstallExtension: ConfirmUninstallExtension; installExtensionFromInput: InstallExtensionFromInput; installFromSelectFileDialog: () => Promise; - installOnDrop: (files: File[]) => Promise; + installOnDrop: InstallOnDrop; extensionInstallationStateStore: ExtensionInstallationStateStore; } diff --git a/src/renderer/components/+extensions/install-extension-from-input.injectable.tsx b/src/renderer/components/+extensions/install-extension-from-input.injectable.tsx index 7b7d783283..3f9e5b0949 100644 --- a/src/renderer/components/+extensions/install-extension-from-input.injectable.tsx +++ b/src/renderer/components/+extensions/install-extension-from-input.injectable.tsx @@ -4,7 +4,6 @@ */ import React from "react"; import type { ExtendableDisposer } from "../../../common/utils"; -import { downloadFile } from "../../../common/utils"; import { InputValidators } from "../input"; import { getMessageFromError } from "./get-message-from-error/get-message-from-error"; import { getInjectable } from "@ogre-tools/injectable"; @@ -15,6 +14,8 @@ import readFileNotifyInjectable from "./read-file-notify/read-file-notify.inject import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable"; import loggerInjectable from "../../../common/logger.injectable"; +import downloadBinaryInjectable from "../../../common/fetch/download-binary.injectable"; +import { withTimeout } from "../../../common/fetch/timeout-controller"; export type InstallExtensionFromInput = (input: string) => Promise; @@ -29,19 +30,28 @@ const installExtensionFromInputInjectable = getInjectable({ const getBasenameOfPath = di.inject(getBasenameOfPathInjectable); const showErrorNotification = di.inject(showErrorNotificationInjectable); const logger = di.inject(loggerInjectable); + const downloadBinary = di.inject(downloadBinaryInjectable); return async (input) => { let disposer: ExtendableDisposer | undefined = undefined; try { - // fixme: improve error messages for non-tar-file URLs + // fixme: improve error messages for non-tar-file URLs if (InputValidators.isUrl.validate(input)) { - // install via url + // install via url disposer = extensionInstallationStateStore.startPreInstall(); - const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 }); + const { signal } = withTimeout(10 * 60 * 1000); + const result = await downloadBinary(input, { signal }); + + if (!result.callWasSuccessful) { + showErrorNotification(`Failed to download extension: ${result.error}`); + + return disposer(); + } + const fileName = getBasenameOfPath(input); - return await attemptInstall({ fileName, dataP: promise }, disposer); + return await attemptInstall({ fileName, data: result.response }, disposer); } try { @@ -49,8 +59,13 @@ const installExtensionFromInputInjectable = getInjectable({ // install from system path const fileName = getBasenameOfPath(input); + const data = await readFileNotify(input); - return await attemptInstall({ fileName, dataP: readFileNotify(input) }); + if (!data) { + return; + } + + return await attemptInstall({ fileName, data }); } catch (error) { const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input); diff --git a/src/renderer/components/+extensions/install-from-select-file-dialog.injectable.ts b/src/renderer/components/+extensions/install-from-select-file-dialog.injectable.ts index 1ae77d664a..ae9dddc299 100644 --- a/src/renderer/components/+extensions/install-from-select-file-dialog.injectable.ts +++ b/src/renderer/components/+extensions/install-from-select-file-dialog.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import { requestOpenFilePickingDialog } from "../../ipc"; import { supportedExtensionFormats } from "./supported-extension-formats"; -import attemptInstallsInjectable from "./attempt-installs/attempt-installs.injectable"; +import attemptInstallsInjectable from "./attempt-installs.injectable"; import directoryForDownloadsInjectable from "../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable"; interface Dependencies { diff --git a/src/renderer/components/+extensions/install-on-drop.injectable.ts b/src/renderer/components/+extensions/install-on-drop.injectable.ts new file mode 100644 index 0000000000..e1779eb223 --- /dev/null +++ b/src/renderer/components/+extensions/install-on-drop.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 loggerInjectable from "../../../common/logger.injectable"; +import attemptInstallsInjectable from "./attempt-installs.injectable"; + +export type InstallOnDrop = (files: File[]) => Promise; + +const installOnDropInjectable = getInjectable({ + id: "install-on-drop", + + instantiate: (di): InstallOnDrop => { + const attemptInstalls = di.inject(attemptInstallsInjectable); + const logger = di.inject(loggerInjectable); + + return (files) => { + logger.info("Install from D&D"); + + return attemptInstalls(files.map(({ path }) => path)); + }; + }, +}); + +export default installOnDropInjectable; diff --git a/src/renderer/components/+extensions/install-on-drop/install-on-drop.injectable.ts b/src/renderer/components/+extensions/install-on-drop/install-on-drop.injectable.ts deleted file mode 100644 index 1a693fa7a6..0000000000 --- a/src/renderer/components/+extensions/install-on-drop/install-on-drop.injectable.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 { getInjectable } from "@ogre-tools/injectable"; -import { installOnDrop } from "./install-on-drop"; -import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable"; - -const installOnDropInjectable = getInjectable({ - id: "install-on-drop", - - instantiate: (di) => - installOnDrop({ - attemptInstalls: di.inject(attemptInstallsInjectable), - }), -}); - -export default installOnDropInjectable; diff --git a/src/renderer/components/+extensions/install-on-drop/install-on-drop.tsx b/src/renderer/components/+extensions/install-on-drop/install-on-drop.tsx deleted file mode 100644 index 0a887f7c6f..0000000000 --- a/src/renderer/components/+extensions/install-on-drop/install-on-drop.tsx +++ /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 logger from "../../../../main/logger"; - -interface Dependencies { - attemptInstalls: (filePaths: string[]) => Promise; -} - -export const installOnDrop = - ({ attemptInstalls }: Dependencies) => - async (files: File[]) => { - logger.info("Install from D&D"); - await attemptInstalls(files.map(({ path }) => path)); - }; diff --git a/src/renderer/components/+extensions/install.tsx b/src/renderer/components/+extensions/install.tsx index b3d33dc7dd..607ed74aaa 100644 --- a/src/renderer/components/+extensions/install.tsx +++ b/src/renderer/components/+extensions/install.tsx @@ -56,9 +56,7 @@ const NonInjectedInstall: React.FC = ({
when clicked renders 1`] = `
when clicked when 'test-2' is clicked when cl
when clicked when 'test-2' is clicked when cl
{ const requestFromChannel = di.inject(requestFromChannelInjectionToken); - const getHelmRepositoriesChannel = di.inject(getActiveHelmRepositoriesChannelInjectable); const showErrorNotification = di.inject(showErrorNotificationInjectable); const helmRepositoriesErrorState = di.inject(helmRepositoriesErrorStateInjectable); return asyncComputed(async () => { - const result = await requestFromChannel(getHelmRepositoriesChannel); + const result = await requestFromChannel(getActiveHelmRepositoriesChannel); if (result.callWasSuccessful) { return result.response; diff --git a/src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/select-helm-repository/add-helm-repository.injectable.ts b/src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/select-helm-repository/add-helm-repository.injectable.ts index 50e02f4235..9a2370635b 100644 --- a/src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/select-helm-repository/add-helm-repository.injectable.ts +++ b/src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/select-helm-repository/add-helm-repository.injectable.ts @@ -3,19 +3,18 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import addHelmRepositoryChannelInjectable from "../../../../../../../common/helm/add-helm-repository-channel.injectable"; import type { HelmRepo } from "../../../../../../../common/helm/helm-repo"; import { requestFromChannelInjectionToken } from "../../../../../../../common/utils/channel/request-from-channel-injection-token"; import activeHelmRepositoriesInjectable from "../../active-helm-repositories.injectable"; import showErrorNotificationInjectable from "../../../../../notifications/show-error-notification.injectable"; import showSuccessNotificationInjectable from "../../../../../notifications/show-success-notification.injectable"; +import { addHelmRepositoryChannel } from "../../../../../../../common/helm/add-helm-repository-channel"; const addHelmRepositoryInjectable = getInjectable({ id: "add-public-helm-repository", instantiate: (di) => { const requestFromChannel = di.inject(requestFromChannelInjectionToken); - const addHelmRepositoryChannel = di.inject(addHelmRepositoryChannelInjectable); const activeHelmRepositories = di.inject(activeHelmRepositoriesInjectable); const showErrorNotification = di.inject(showErrorNotificationInjectable); const showSuccessNotification = di.inject(showSuccessNotificationInjectable); diff --git a/src/renderer/components/+preferences/kubernetes/helm-charts/remove-helm-repository.injectable.ts b/src/renderer/components/+preferences/kubernetes/helm-charts/remove-helm-repository.injectable.ts index 4f316baed0..3aa56a4b04 100644 --- a/src/renderer/components/+preferences/kubernetes/helm-charts/remove-helm-repository.injectable.ts +++ b/src/renderer/components/+preferences/kubernetes/helm-charts/remove-helm-repository.injectable.ts @@ -6,14 +6,13 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { HelmRepo } from "../../../../../common/helm/helm-repo"; import { requestFromChannelInjectionToken } from "../../../../../common/utils/channel/request-from-channel-injection-token"; import activeHelmRepositoriesInjectable from "./active-helm-repositories.injectable"; -import removeHelmRepositoryChannelInjectable from "../../../../../common/helm/remove-helm-repository-channel.injectable"; +import { removeHelmRepositoryChannel } from "../../../../../common/helm/remove-helm-repository-channel"; const removePublicHelmRepositoryInjectable = getInjectable({ id: "remove-public-helm-repository", instantiate: (di) => { const requestFromChannel = di.inject(requestFromChannelInjectionToken); - const removeHelmRepositoryChannel = di.inject(removeHelmRepositoryChannelInjectable); const activeHelmRepositories = di.inject(activeHelmRepositoriesInjectable); return async (repository: HelmRepo) => { diff --git a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx index 4cb013e5d6..250ab2e713 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx @@ -14,7 +14,7 @@ import { Link } from "react-router-dom"; import { ResourceMetrics } from "../resource-metrics"; import { VolumeClaimDiskChart } from "./volume-claim-disk-chart"; import type { KubeObjectDetailsProps } from "../kube-object-details"; -import { PersistentVolumeClaim } from "../../../common/k8s-api/endpoints"; +import { PersistentVolumeClaim, storageClassApi } from "../../../common/k8s-api/endpoints"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { KubeObjectMeta } from "../kube-object-meta"; import logger from "../../../common/logger"; @@ -27,6 +27,7 @@ import type { PodStore } from "../+workloads-pods/store"; import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable"; import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable"; import podStoreInjectable from "../+workloads-pods/store.injectable"; +import { stopPropagation } from "../../../renderer/utils"; export interface PersistentVolumeClaimDetailsProps extends KubeObjectDetailsProps { } @@ -78,6 +79,10 @@ class NonInjectedPersistentVolumeClaimDetails extends React.Component {!isMetricHidden && ( @@ -89,15 +94,21 @@ class NonInjectedPersistentVolumeClaimDetails extends React.Component - + )} - + {accessModes?.join(", ")} - {storageClassName} + + {storageClassName} + {volumeClaim.getStorage()} @@ -116,7 +127,7 @@ class NonInjectedPersistentVolumeClaimDetails extends React.ComponentSelector - {volumeClaim.getMatchLabels().map(label => )} + {volumeClaim.getMatchLabels().map(label => )} diff --git a/src/renderer/components/+storage-volumes/volume-details.tsx b/src/renderer/components/+storage-volumes/volume-details.tsx index 4e2a436b91..32ab0c5307 100644 --- a/src/renderer/components/+storage-volumes/volume-details.tsx +++ b/src/renderer/components/+storage-volumes/volume-details.tsx @@ -11,11 +11,12 @@ import { Link } from "react-router-dom"; import { observer } from "mobx-react"; import { DrawerItem, DrawerTitle } from "../drawer"; import { Badge } from "../badge"; -import { PersistentVolume, persistentVolumeClaimApi } from "../../../common/k8s-api/endpoints"; +import { PersistentVolume, persistentVolumeClaimApi, storageClassApi } from "../../../common/k8s-api/endpoints"; import type { KubeObjectDetailsProps } from "../kube-object-details"; import { KubeObjectMeta } from "../kube-object-meta"; import { getDetailsUrl } from "../kube-detail-params"; import logger from "../../../common/logger"; +import { stopPropagation } from "../../../renderer/utils"; export interface PersistentVolumeDetailsProps extends KubeObjectDetailsProps { } @@ -37,9 +38,13 @@ export class PersistentVolumeDetails extends React.Component - + {capacity?.storage} @@ -57,10 +62,16 @@ export class PersistentVolumeDetails extends React.Component - {storageClassName} + + {storageClassName} + - + {nfs && ( diff --git a/src/renderer/components/+workloads-pods/pod-details.tsx b/src/renderer/components/+workloads-pods/pod-details.tsx index da87515355..726c5326b6 100644 --- a/src/renderer/components/+workloads-pods/pod-details.tsx +++ b/src/renderer/components/+workloads-pods/pod-details.tsx @@ -10,11 +10,11 @@ import kebabCase from "lodash/kebabCase"; import { disposeOnUnmount, observer } from "mobx-react"; import { Link } from "react-router-dom"; import { observable, reaction, makeObservable } from "mobx"; -import type { NodeApi } from "../../../common/k8s-api/endpoints"; import { Pod } from "../../../common/k8s-api/endpoints"; +import type { NodeApi, PriorityClassApi, RuntimeClassApi, ServiceAccountApi } from "../../../common/k8s-api/endpoints"; import { DrawerItem, DrawerTitle } from "../drawer"; import { Badge } from "../badge"; -import { cssNames, toJS } from "../../utils"; +import { cssNames, stopPropagation, toJS } from "../../utils"; import { PodDetailsContainer } from "./pod-details-container"; import { PodDetailsAffinities } from "./pod-details-affinities"; import { PodDetailsTolerations } from "./pod-details-tolerations"; @@ -35,6 +35,9 @@ import type { GetDetailsUrl } from "../kube-detail-params/get-details-url.inject import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable"; import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable"; import nodeApiInjectable from "../../../common/k8s-api/endpoints/node.api.injectable"; +import runtimeClassApiInjectable from "../../../common/k8s-api/endpoints/runtime-class.api.injectable"; +import serviceAccountApiInjectable from "../../../common/k8s-api/endpoints/service-account.api.injectable"; +import priorityClassApiInjectable from "../../../common/k8s-api/endpoints/priority-class.api.injectable"; export interface PodDetailsProps extends KubeObjectDetailsProps { } @@ -44,6 +47,9 @@ interface Dependencies { getActiveClusterEntity: GetActiveClusterEntity; getDetailsUrl: GetDetailsUrl; nodeApi: NodeApi; + priorityClassApi: PriorityClassApi; + runtimeClassApi: RuntimeClassApi; + serviceAccountApi: ServiceAccountApi; } @observer @@ -94,6 +100,22 @@ class NonInjectedPodDetails extends React.Component {!isMetricHidden && ( @@ -130,16 +152,34 @@ class NonInjectedPodDetails extends React.Component )} - {pod.getServiceAccountName()} + + {serviceAccountName} + -