diff --git a/docs/extensions/capabilities/common-capabilities.md b/docs/extensions/capabilities/common-capabilities.md index 8ba03253c5..e7b585af66 100644 --- a/docs/extensions/capabilities/common-capabilities.md +++ b/docs/extensions/capabilities/common-capabilities.md @@ -37,9 +37,9 @@ export default class ExampleMainExtension extends Main.LensExtension { } ``` -### App Menus +### Menus -This extension can register custom app menus that will be displayed on OS native menus. +This extension can register custom app and tray menus that will be displayed on OS native menus. Example: @@ -56,6 +56,29 @@ export default class ExampleMainExtension extends Main.LensExtension { } } ] + + trayMenus = [ + { + label: "My links", + submenu: [ + { + label: "Lens", + click() { + Main.Navigation.navigate("https://k8slens.dev"); + } + }, + { + type: "separator" + }, + { + label: "Lens Github", + click() { + Main.Navigation.navigate("https://github.com/lensapp/lens"); + } + } + ] + } + ] } ``` diff --git a/docs/extensions/guides/main-extension.md b/docs/extensions/guides/main-extension.md index fec937db17..a0e20880bf 100644 --- a/docs/extensions/guides/main-extension.md +++ b/docs/extensions/guides/main-extension.md @@ -3,7 +3,7 @@ The Main Extension API is the interface to Lens's main process. Lens runs in both main and renderer processes. The Main Extension API allows you to access, configure, and customize Lens data, add custom application menu items and [protocol handlers](protocol-handlers.md), and run custom code in Lens's main process. -It also provides convenient methods for navigating to built-in Lens pages and extension pages, as well as adding and removing sources of catalog entities. +It also provides convenient methods for navigating to built-in Lens pages and extension pages, as well as adding and removing sources of catalog entities. ## `Main.LensExtension` Class @@ -45,7 +45,6 @@ For more details on accessing Lens state data, please see the [Stores](../stores ### `appMenus` The Main Extension API allows you to customize the UI application menu. -Note that this is the only UI feature that the Main Extension API allows you to customize. The following example demonstrates adding an item to the **Help** menu. ``` typescript @@ -65,7 +64,7 @@ export default class SamplePageMainExtension extends Main.LensExtension { ``` `appMenus` is an array of objects that satisfy the `MenuRegistration` interface. -`MenuRegistration` extends React's `MenuItemConstructorOptions` interface. +`MenuRegistration` extends Electron's `MenuItemConstructorOptions` interface. The properties of the appMenus array objects are defined as follows: * `parentId` is the name of the menu where your new menu item will be listed. @@ -96,6 +95,35 @@ export default class SamplePageMainExtension extends Main.LensExtension { When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`. This page would be defined in your extension's `Renderer.LensExtension` implementation (See [`Renderer.LensExtension`](renderer-extension.md)). +### `trayMenus` + +`trayMenus` is an array of `TrayMenuRegistration` objects. Most importantly you can define a `label` and a `click` handler. Other properties are `submenu`, `enabled`, `toolTip`, `id` and `type`. + +``` typescript +interface TrayMenuRegistration { + label?: string; + click?: (menuItem: TrayMenuRegistration) => void; + id?: string; + type?: "normal" | "separator" | "submenu" + toolTip?: string; + enabled?: boolean; + submenu?: TrayMenuRegistration[] +} +``` + +The following example demonstrates how tray menus can be added from extension: + +``` typescript +import { Main } from "@k8slens/extensions"; + +export default class SampleTrayMenuMainExtension extends Main.LensExtension { + trayMenus = [{ + label: "menu from the extension", + click: () => { console.log("tray menu clicked!") } + }] +} +``` + ### `addCatalogSource()` and `removeCatalogSource()` Methods The `Main.LensExtension` class also provides the `addCatalogSource()` and `removeCatalogSource()` methods, for managing custom catalog items (or entities). diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index a076a895c6..ae49ee03dd 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -272,10 +272,7 @@ export class ExtensionLoader { registries.EntitySettingRegistry.getInstance().add(extension.entitySettings), registries.StatusBarRegistry.getInstance().add(extension.statusBarItems), registries.CommandRegistry.getInstance().add(extension.commands), - registries.WelcomeMenuRegistry.getInstance().add(extension.welcomeMenus), - registries.WelcomeBannerRegistry.getInstance().add(extension.welcomeBanners), registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems), - registries.TopBarRegistry.getInstance().add(extension.topBarItems), ]; this.events.on("remove", (removedExtension: LensRendererExtension) => { diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index c0c0a5674a..00f08ca5e5 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -25,9 +25,10 @@ import { catalogEntityRegistry } from "../main/catalog"; import type { CatalogEntity } from "../common/catalog"; import type { IObservableArray } from "mobx"; import type { MenuRegistration } from "../main/menu/menu-registration"; - +import type { TrayMenuRegistration } from "../main/tray/tray-menu-registration"; export class LensMainExtension extends LensExtension { appMenus: MenuRegistration[] = []; + trayMenus: TrayMenuRegistration[] = []; async navigate(pageId?: string, params?: Record, frameId?: number) { return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId); diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 67567969ee..9f7b947d82 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -26,7 +26,10 @@ import type { CatalogEntity } from "../common/catalog"; import type { Disposer } from "../common/utils"; import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry"; import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-registry"; +import type { TopBarRegistration } from "../renderer/components/layout/top-bar/top-bar-registration"; import type { KubernetesCluster } from "../common/catalog-entities"; +import type { WelcomeMenuRegistration } from "../renderer/components/+welcome/welcome-menu-items/welcome-menu-registration"; +import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration"; export class LensRendererExtension extends LensExtension { globalPages: registries.PageRegistration[] = []; @@ -40,10 +43,10 @@ export class LensRendererExtension extends LensExtension { kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = []; kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = []; commands: registries.CommandRegistration[] = []; - welcomeMenus: registries.WelcomeMenuRegistration[] = []; - welcomeBanners: registries.WelcomeBannerRegistration[] = []; + welcomeMenus: WelcomeMenuRegistration[] = []; + welcomeBanners: WelcomeBannerRegistration[] = []; catalogEntityDetailItems: registries.CatalogEntityDetailRegistration[] = []; - topBarItems: registries.TopBarRegistration[] = []; + topBarItems: TopBarRegistration[] = []; async navigate

(pageId?: string, params?: P) { const { navigate } = await import("../renderer/navigation"); diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 4dd64a9c82..76f6c05d11 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -30,9 +30,6 @@ export * from "./kube-object-menu-registry"; export * from "./kube-object-status-registry"; export * from "./command-registry"; export * from "./entity-setting-registry"; -export * from "./welcome-menu-registry"; -export * from "./welcome-banner-registry"; export * from "./catalog-entity-detail-registry"; export * from "./workloads-overview-detail-registry"; -export * from "./topbar-registry"; export * from "./protocol-handler"; diff --git a/src/extensions/renderer-extensions.injectable.ts b/src/extensions/renderer-extensions.injectable.ts new file mode 100644 index 0000000000..dddcf11e80 --- /dev/null +++ b/src/extensions/renderer-extensions.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { IComputedValue } from "mobx"; +import extensionsInjectable from "./extensions.injectable"; +import type { LensRendererExtension } from "./lens-renderer-extension"; + +const rendererExtensionsInjectable = getInjectable({ + lifecycle: lifecycleEnum.singleton, + + instantiate: (di) => + di.inject(extensionsInjectable) as IComputedValue, +}); + +export default rendererExtensionsInjectable; diff --git a/src/main/index.ts b/src/main/index.ts index a80e4f190a..6c0069fb32 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -55,28 +55,23 @@ import { WeblinkStore } from "../common/weblink-store"; import { SentryInit } from "../common/sentry"; import { ensureDir } from "fs-extra"; import { initMenu } from "./menu/menu"; -import { initTray } from "./tray"; import { kubeApiRequest } from "./proxy-functions"; +import { initTray } from "./tray/tray"; import { ShellSession } from "./shell-session/shell-session"; import { getDi } from "./getDi"; import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable"; import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable"; -import extensionDiscoveryInjectable - from "../extensions/extension-discovery/extension-discovery.injectable"; -import directoryForExesInjectable - from "../common/app-paths/directory-for-exes/directory-for-exes.injectable"; -import initIpcMainHandlersInjectable - from "./initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable"; +import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable"; +import directoryForExesInjectable from "../common/app-paths/directory-for-exes/directory-for-exes.injectable"; +import initIpcMainHandlersInjectable from "./initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable"; import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable"; -import directoryForKubeConfigsInjectable - from "../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; -import kubeconfigSyncManagerInjectable - from "./catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable"; +import directoryForKubeConfigsInjectable from "../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; +import kubeconfigSyncManagerInjectable from "./catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable"; import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable"; import routerInjectable from "./router/router.injectable"; -import shellApiRequestInjectable - from "./proxy-functions/shell-api-request/shell-api-request.injectable"; +import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable"; import userStoreInjectable from "../common/user-store/user-store.injectable"; +import trayMenuItemsInjectable from "./tray/tray-menu-items.injectable"; const di = getDi(); @@ -255,10 +250,11 @@ di.runSetups().then(() => { const windowManager = WindowManager.createInstance(); const menuItems = di.inject(electronMenuItemsInjectable); + const trayMenuItems = di.inject(trayMenuItemsInjectable); onQuitCleanup.push( initMenu(windowManager, menuItems), - initTray(windowManager), + initTray(windowManager, trayMenuItems), () => ShellSession.cleanup(), ); diff --git a/src/main/menu/electron-menu-items.injectable.ts b/src/main/menu/electron-menu-items.injectable.ts index dd70812d89..d0e5a650cf 100644 --- a/src/main/menu/electron-menu-items.injectable.ts +++ b/src/main/menu/electron-menu-items.injectable.ts @@ -29,8 +29,7 @@ const electronMenuItemsInjectable = getInjectable({ const extensions = di.inject(mainExtensionsInjectable); return computed(() => - extensions.get().flatMap((extension) => extension.appMenus), - ); + extensions.get().flatMap((extension) => extension.appMenus)); }, }); diff --git a/src/main/prometheus/operator.ts b/src/main/prometheus/operator.ts index 83638e4ec1..140b432baf 100644 --- a/src/main/prometheus/operator.ts +++ b/src/main/prometheus/operator.ts @@ -38,7 +38,7 @@ export class PrometheusOperator extends PrometheusProvider { case "cluster": switch (queryName) { case "memoryUsage": - return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`); + return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`); case "workloadMemoryUsage": return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`; case "memoryRequests": @@ -50,7 +50,7 @@ export class PrometheusOperator extends PrometheusProvider { case "memoryAllocatableCapacity": return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`; case "cpuUsage": - return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`; + return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`; case "cpuRequests": return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`; case "cpuLimits": @@ -66,31 +66,31 @@ export class PrometheusOperator extends PrometheusProvider { case "podAllocatableCapacity": return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`; case "fsSize": - return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`; + return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`; case "fsUsage": - return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`; + return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`; } break; case "nodes": switch (queryName) { case "memoryUsage": - return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`; + return `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`; case "workloadMemoryUsage": - return `sum(container_memory_working_set_bytes{container!=""}) by (node)`; + return `sum(container_memory_working_set_bytes{container!="POD", container!=""}) by (node)`; case "memoryCapacity": return `sum(kube_node_status_capacity{resource="memory"}) by (node)`; case "memoryAllocatableCapacity": return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`; case "cpuUsage": - return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`; + return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`; case "cpuCapacity": return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`; case "cpuAllocatableCapacity": return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`; case "fsSize": - return `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`; + return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`; case "fsUsage": - return `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`; + return `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`; } break; case "pods": diff --git a/src/main/tray/tray-menu-items.injectable.ts b/src/main/tray/tray-menu-items.injectable.ts new file mode 100644 index 0000000000..8a31cc6af5 --- /dev/null +++ b/src/main/tray/tray-menu-items.injectable.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import mainExtensionsInjectable from "../../extensions/main-extensions.injectable"; + +const trayItemsInjectable = getInjectable({ + lifecycle: lifecycleEnum.singleton, + + instantiate: (di) => { + const extensions = di.inject(mainExtensionsInjectable); + + return computed(() => + extensions.get().flatMap(extension => extension.trayMenus)); + }, +}); + +export default trayItemsInjectable; diff --git a/src/main/tray/tray-menu-items.test.ts b/src/main/tray/tray-menu-items.test.ts new file mode 100644 index 0000000000..b46bce3671 --- /dev/null +++ b/src/main/tray/tray-menu-items.test.ts @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; +import { LensMainExtension } from "../../extensions/lens-main-extension"; +import trayItemsInjectable from "./tray-menu-items.injectable"; +import type { IComputedValue } from "mobx"; +import { computed, ObservableMap, runInAction } from "mobx"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import mainExtensionsInjectable from "../../extensions/main-extensions.injectable"; +import type { TrayMenuRegistration } from "./tray-menu-registration"; + +describe("tray-menu-items", () => { + let di: ConfigurableDependencyInjectionContainer; + let trayMenuItems: IComputedValue; + let extensionsStub: ObservableMap; + + beforeEach(() => { + di = getDiForUnitTesting(); + + extensionsStub = new ObservableMap(); + + di.override( + mainExtensionsInjectable, + () => computed(() => [...extensionsStub.values()]), + ); + + trayMenuItems = di.inject(trayItemsInjectable); + }); + + it("does not have any items yet", () => { + expect(trayMenuItems.get()).toHaveLength(0); + }); + + describe("when extension is enabled", () => { + beforeEach(() => { + const someExtension = new SomeTestExtension({ + id: "some-extension-id", + trayMenus: [{ label: "tray-menu-from-some-extension" }], + }); + + runInAction(() => { + extensionsStub.set("some-extension-id", someExtension); + }); + }); + + it("has tray menu items", () => { + expect(trayMenuItems.get()).toEqual([ + { + label: "tray-menu-from-some-extension", + }, + ]); + }); + + it("when disabling extension, does not have tray menu items", () => { + runInAction(() => { + extensionsStub.delete("some-extension-id"); + }); + + expect(trayMenuItems.get()).toHaveLength(0); + }); + + describe("when other extension is enabled", () => { + beforeEach(() => { + const someOtherExtension = new SomeTestExtension({ + id: "some-extension-id", + trayMenus: [{ label: "some-label-from-second-extension" }], + }); + + runInAction(() => { + extensionsStub.set("some-other-extension-id", someOtherExtension); + }); + }); + + it("has tray menu items for both extensions", () => { + expect(trayMenuItems.get()).toEqual([ + { + label: "tray-menu-from-some-extension", + }, + + { + label: "some-label-from-second-extension", + }, + ]); + }); + + it("when extension is disabled, still returns tray menu items for extensions that are enabled", () => { + runInAction(() => { + extensionsStub.delete("some-other-extension-id"); + }); + + expect(trayMenuItems.get()).toEqual([ + { + label: "tray-menu-from-some-extension", + }, + ]); + }); + }); + }); +}); + +class SomeTestExtension extends LensMainExtension { + constructor({ id, trayMenus }: { + id: string; + trayMenus: TrayMenuRegistration[]; + }) { + super({ + id, + absolutePath: "irrelevant", + isBundled: false, + isCompatible: false, + isEnabled: false, + manifest: { name: id, version: "some-version" }, + manifestPath: "irrelevant", + }); + + this.trayMenus = trayMenus; + } +} diff --git a/src/renderer/initializers/welcome-menu-registry.ts b/src/main/tray/tray-menu-registration.d.ts similarity index 70% rename from src/renderer/initializers/welcome-menu-registry.ts rename to src/main/tray/tray-menu-registration.d.ts index f658fe5715..0cf7fe611b 100644 --- a/src/renderer/initializers/welcome-menu-registry.ts +++ b/src/main/tray/tray-menu-registration.d.ts @@ -19,17 +19,12 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { catalogURL } from "../../common/routes"; -import { WelcomeMenuRegistry } from "../../extensions/registries"; -import { navigate } from "../navigation"; - -export function initWelcomeMenuRegistry() { - WelcomeMenuRegistry.getInstance() - .add([ - { - title: "Browse Clusters in Catalog", - icon: "view_list", - click: () => navigate(catalogURL({ params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" }} )), - }, - ]); +export interface TrayMenuRegistration { + label?: string; + click?: (menuItem: TrayMenuRegistration) => void; + id?: string; + type?: "normal" | "separator" | "submenu" + toolTip?: string; + enabled?: boolean; + submenu?: TrayMenuRegistration[] } diff --git a/src/main/tray.ts b/src/main/tray/tray.ts similarity index 70% rename from src/main/tray.ts rename to src/main/tray/tray.ts index 850d19fe21..ac3e140e3b 100644 --- a/src/main/tray.ts +++ b/src/main/tray/tray.ts @@ -20,16 +20,18 @@ */ import path from "path"; -import packageInfo from "../../package.json"; +import packageInfo from "../../../package.json"; import { Menu, Tray } from "electron"; -import { autorun } from "mobx"; -import { showAbout } from "./menu/menu"; -import { checkForUpdates, isAutoUpdateEnabled } from "./app-updater"; -import type { WindowManager } from "./window-manager"; -import logger from "./logger"; -import { isDevelopment, isWindows, productName } from "../common/vars"; -import { exitApp } from "./exit-app"; -import { preferencesURL } from "../common/routes"; +import { autorun, IComputedValue } from "mobx"; +import { showAbout } from "../menu/menu"; +import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater"; +import type { WindowManager } from "../window-manager"; +import logger from "../logger"; +import { isDevelopment, isWindows, productName } from "../../common/vars"; +import { exitApp } from "../exit-app"; +import { preferencesURL } from "../../common/routes"; +import { toJS } from "../../common/utils"; +import type { TrayMenuRegistration } from "./tray-menu-registration"; const TRAY_LOG_PREFIX = "[TRAY]"; @@ -44,7 +46,10 @@ export function getTrayIcon(): string { ); } -export function initTray(windowManager: WindowManager) { +export function initTray( + windowManager: WindowManager, + trayMenuItems: IComputedValue, +) { const icon = getTrayIcon(); tray = new Tray(icon); @@ -62,7 +67,7 @@ export function initTray(windowManager: WindowManager) { const disposers = [ autorun(() => { try { - const menu = createTrayMenu(windowManager); + const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get())); tray.setContextMenu(menu); } catch (error) { @@ -78,8 +83,21 @@ export function initTray(windowManager: WindowManager) { }; } -function createTrayMenu(windowManager: WindowManager): Menu { - const template: Electron.MenuItemConstructorOptions[] = [ +function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions { + return { + ...trayItem, + submenu: trayItem.submenu ? trayItem.submenu.map(getMenuItemConstructorOptions) : undefined, + click: trayItem.click ? () => { + trayItem.click(trayItem); + } : undefined, + }; +} + +function createTrayMenu( + windowManager: WindowManager, + extensionTrayItems: TrayMenuRegistration[], +): Menu { + let template: Electron.MenuItemConstructorOptions[] = [ { label: `Open ${productName}`, click() { @@ -108,6 +126,8 @@ function createTrayMenu(windowManager: WindowManager): Menu { }); } + template = template.concat(extensionTrayItems.map(getMenuItemConstructorOptions)); + return Menu.buildFromTemplate(template.concat([ { label: `About ${productName}`, diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 3eb4283897..9079f4de6e 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -107,9 +107,6 @@ export async function bootstrap(di: DependencyInjectionContainer) { logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`); initializers.initKubeObjectDetailRegistry(); - logger.info(`${logPrefix} initializing WelcomeMenuRegistry`); - initializers.initWelcomeMenuRegistry(); - logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`); initializers.initWorkloadsOverviewDetailRegistry(); diff --git a/src/renderer/components/+preferences/application.tsx b/src/renderer/components/+preferences/application.tsx index 40859cd5e0..1e3c75b68f 100644 --- a/src/renderer/components/+preferences/application.tsx +++ b/src/renderer/components/+preferences/application.tsx @@ -27,7 +27,7 @@ import { ThemeStore } from "../../theme.store"; import { UserStore } from "../../../common/user-store"; import { Input } from "../input"; import { isWindows } from "../../../common/vars"; -import { FormSwitch, Switcher } from "../switch"; +import { Switch } from "../switch"; import moment from "moment-timezone"; import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers"; import { action } from "mobx"; @@ -86,16 +86,12 @@ export const Application = observer(() => {

- userStore.terminalCopyOnSelect = v.target.checked} - name="terminalCopyOnSelect" - /> - } - /> + userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect} + > + Copy on select and paste on right-click +

@@ -135,16 +131,9 @@ export const Application = observer(() => {
- userStore.openAtLogin = v.target.checked} - name="startup" - /> - } - label="Automatically start Lens on login" - /> + userStore.openAtLogin = !userStore.openAtLogin}> + Automatically start Lens on login +

diff --git a/src/renderer/components/+preferences/editor.tsx b/src/renderer/components/+preferences/editor.tsx index 6f346eb63d..523f9bd278 100644 --- a/src/renderer/components/+preferences/editor.tsx +++ b/src/renderer/components/+preferences/editor.tsx @@ -21,7 +21,7 @@ import { observer } from "mobx-react"; import React from "react"; import { UserStore } from "../../../common/user-store"; -import { FormSwitch, Switcher } from "../switch"; +import { Switch } from "../switch"; import { Select } from "../select"; import { SubTitle } from "../layout/sub-title"; import { SubHeader } from "../layout/sub-header"; @@ -45,15 +45,12 @@ export const Editor = observer(() => {
- Show minimap} - control={ - editorConfiguration.minimap.enabled = checked} - /> - } - /> + editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled} + > + Show minimap +
Position diff --git a/src/renderer/components/+preferences/kubectl-binaries.tsx b/src/renderer/components/+preferences/kubectl-binaries.tsx index d4480b599b..71c835f82a 100644 --- a/src/renderer/components/+preferences/kubectl-binaries.tsx +++ b/src/renderer/components/+preferences/kubectl-binaries.tsx @@ -25,7 +25,7 @@ import { SubTitle } from "../layout/sub-title"; import { UserStore } from "../../../common/user-store"; import { bundledKubectlPath } from "../../../main/kubectl/kubectl"; import { SelectOption, Select } from "../select"; -import { FormSwitch, Switcher } from "../switch"; +import { Switch } from "../switch"; import { packageMirrors } from "../../../common/user-store/preferences-helpers"; import directoryForBinariesInjectable from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable"; @@ -54,16 +54,12 @@ const NonInjectedKubectlBinaries: React.FC = (({ defaultPathForKub <>
- userStore.downloadKubectlBinaries = v.target.checked} - name="kubectl-download" - /> - } - label="Download kubectl binaries matching the Kubernetes cluster version" - /> + userStore.downloadKubectlBinaries = !userStore.downloadKubectlBinaries} + > + Download kubectl binaries matching the Kubernetes cluster version +
diff --git a/src/renderer/components/+preferences/proxy.tsx b/src/renderer/components/+preferences/proxy.tsx index b85bd5e0e1..f25c1fbc9b 100644 --- a/src/renderer/components/+preferences/proxy.tsx +++ b/src/renderer/components/+preferences/proxy.tsx @@ -24,10 +24,11 @@ import React from "react"; import { UserStore } from "../../../common/user-store"; import { Input } from "../input"; import { SubTitle } from "../layout/sub-title"; -import { FormSwitch, Switcher } from "../switch"; +import { Switch } from "../switch"; export const LensProxy = observer(() => { const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || ""); + const store = UserStore.getInstance(); return (
@@ -50,16 +51,9 @@ export const LensProxy = observer(() => {
- UserStore.getInstance().allowUntrustedCAs = v.target.checked} - name="startup" - /> - } - label="Allow untrusted Certificate Authorities" - /> + store.allowUntrustedCAs = !store.allowUntrustedCAs}> + Allow untrusted Certificate Authorities + This will make Lens to trust ANY certificate authority without any validations.{" "} Needed with some corporate proxies that do certificate re-writing.{" "} diff --git a/src/renderer/components/+welcome/__test__/welcome.test.tsx b/src/renderer/components/+welcome/__test__/welcome.test.tsx index 2024d69c7e..74cf65b20d 100644 --- a/src/renderer/components/+welcome/__test__/welcome.test.tsx +++ b/src/renderer/components/+welcome/__test__/welcome.test.tsx @@ -20,45 +20,55 @@ */ import React from "react"; -import { render, screen } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; -import { Welcome } from "../welcome"; -import { TopBarRegistry, WelcomeMenuRegistry, WelcomeBannerRegistry } from "../../../../extensions/registries"; -import { defaultWidth } from "../welcome"; +import { defaultWidth, Welcome } from "../welcome"; +import { computed } from "mobx"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import type { DiRender } from "../../test-utils/renderFor"; +import { renderFor } from "../../test-utils/renderFor"; +import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; +import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable"; +import { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; +import type { WelcomeBannerRegistration } from "../welcome-banner-items/welcome-banner-registration"; -jest.mock( - "electron", - () => ({ - ipcRenderer: { - on: jest.fn(), - }, - app: { - getPath: () => "tmp", - }, - }), -); +jest.mock("electron", () => ({ + ipcRenderer: { + on: jest.fn(), + }, + app: { + getPath: () => "tmp", + }, +})); describe("", () => { - beforeEach(() => { - TopBarRegistry.createInstance(); - WelcomeMenuRegistry.createInstance(); - WelcomeBannerRegistry.createInstance(); - }); + let render: DiRender; + let di: ConfigurableDependencyInjectionContainer; + let welcomeBannersStub: WelcomeBannerRegistration[]; - afterEach(() => { - TopBarRegistry.resetInstance(); - WelcomeMenuRegistry.resetInstance(); - WelcomeBannerRegistry.resetInstance(); + beforeEach(() => { + di = getDiForUnitTesting(); + + render = renderFor(di); + + welcomeBannersStub = []; + + di.override(rendererExtensionsInjectable, () => + computed(() => [ + new TestExtension({ + id: "some-id", + welcomeBanners: welcomeBannersStub, + }), + ]), + ); }); it("renders registered in WelcomeBannerRegistry and hide logo", async () => { const testId = "testId"; - WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ - { - Banner: () =>
, - }, - ]); + welcomeBannersStub.push({ + Banner: () =>
, + }); const { container } = render(); @@ -67,16 +77,15 @@ describe("", () => { }); it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => { - WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ - { - width: 100, - Banner: () =>
, - }, - { - width: 800, - Banner: () =>
, - }, - ]); + welcomeBannersStub.push({ + width: 100, + Banner: () =>
, + }); + + welcomeBannersStub.push({ + width: 800, + Banner: () =>
, + }); render(); @@ -92,3 +101,25 @@ describe("", () => { }); }); }); + +class TestExtension extends LensRendererExtension { + constructor({ + id, + welcomeBanners, + }: { + id: string; + welcomeBanners: WelcomeBannerRegistration[]; + }) { + super({ + id, + absolutePath: "irrelevant", + isBundled: false, + isCompatible: false, + isEnabled: false, + manifest: { name: id, version: "some-version" }, + manifestPath: "irrelevant", + }); + + this.welcomeBanners = welcomeBanners; + } +} diff --git a/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-items.injectable.ts b/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-items.injectable.ts new file mode 100644 index 0000000000..288bb540ce --- /dev/null +++ b/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-items.injectable.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable"; +import { computed } from "mobx"; + +const welcomeBannerItemsInjectable = getInjectable({ + instantiate: (di) => { + const extensions = di.inject(rendererExtensionsInjectable); + + return computed(() => [ + ...extensions.get().flatMap((extension) => extension.welcomeBanners), + ]); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default welcomeBannerItemsInjectable; diff --git a/src/extensions/registries/welcome-banner-registry.ts b/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.d.ts similarity index 91% rename from src/extensions/registries/welcome-banner-registry.ts rename to src/renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.d.ts index 1102dc8a3f..ec7cf3cbdb 100644 --- a/src/extensions/registries/welcome-banner-registry.ts +++ b/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.d.ts @@ -19,8 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { BaseRegistry } from "./base-registry"; - /** * WelcomeBannerRegistration is for an extension to register * Provide a Banner component to be renderered in the welcome screen. @@ -35,5 +33,3 @@ export interface WelcomeBannerRegistration { */ width?: number } - -export class WelcomeBannerRegistry extends BaseRegistry { } diff --git a/src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts b/src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts new file mode 100644 index 0000000000..c766704cef --- /dev/null +++ b/src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { computed, IComputedValue } from "mobx"; +import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; +import { navigate } from "../../../navigation"; +import { catalogURL } from "../../../../common/routes"; + +interface Dependencies { + extensions: IComputedValue; +} + +export const getWelcomeMenuItems = ({ extensions }: Dependencies) => { + const browseClusters = { + title: "Browse Clusters in Catalog", + icon: "view_list", + click: () => + navigate( + catalogURL({ + params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" }, + }), + ), + }; + + return computed(() => [ + browseClusters, + ...extensions.get().flatMap((extension) => extension.welcomeMenus), + ]); +}; diff --git a/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts new file mode 100644 index 0000000000..384b0b07bc --- /dev/null +++ b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable"; +import { getWelcomeMenuItems } from "./get-welcome-menu-items"; + +const welcomeMenuItemsInjectable = getInjectable({ + instantiate: (di) => + getWelcomeMenuItems({ + extensions: di.inject(rendererExtensionsInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default welcomeMenuItemsInjectable; diff --git a/src/extensions/registries/welcome-menu-registry.ts b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.d.ts similarity index 90% rename from src/extensions/registries/welcome-menu-registry.ts rename to src/renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.d.ts index 7092028459..8f4d9833b3 100644 --- a/src/extensions/registries/welcome-menu-registry.ts +++ b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.d.ts @@ -19,12 +19,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { BaseRegistry } from "./base-registry"; - export interface WelcomeMenuRegistration { title: string | (() => string); icon: string; click: () => void | Promise; } - -export class WelcomeMenuRegistry extends BaseRegistry {} diff --git a/src/renderer/components/+welcome/welcome.tsx b/src/renderer/components/+welcome/welcome.tsx index 669e768782..56aefbfc3c 100644 --- a/src/renderer/components/+welcome/welcome.tsx +++ b/src/renderer/components/+welcome/welcome.tsx @@ -22,78 +22,129 @@ import "./welcome.scss"; import React from "react"; import { observer } from "mobx-react"; +import type { IComputedValue } from "mobx"; import Carousel from "react-material-ui-carousel"; import { Icon } from "../icon"; import { productName, slackUrl } from "../../../common/vars"; -import { WelcomeMenuRegistry } from "../../../extensions/registries"; -import { WelcomeBannerRegistry } from "../../../extensions/registries"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import welcomeMenuItemsInjectable from "./welcome-menu-items/welcome-menu-items.injectable"; +import type { WelcomeMenuRegistration } from "./welcome-menu-items/welcome-menu-registration"; +import welcomeBannerItemsInjectable from "./welcome-banner-items/welcome-banner-items.injectable"; +import type { WelcomeBannerRegistration } from "./welcome-banner-items/welcome-banner-registration"; export const defaultWidth = 320; -@observer -export class Welcome extends React.Component { - render() { - const welcomeBanner = WelcomeBannerRegistry.getInstance().getItems(); +interface Dependencies { + welcomeMenuItems: IComputedValue + welcomeBannerItems: IComputedValue +} - // if there is banner with specified width, use it to calculate the width of the container - const maxWidth = welcomeBanner.reduce((acc, curr) => { - const currWidth = curr.width ?? 0; +const NonInjectedWelcome: React.FC = ({ welcomeMenuItems, welcomeBannerItems }) => { + const welcomeBanners = welcomeBannerItems.get(); - if (acc > currWidth) { - return acc; - } + // if there is banner with specified width, use it to calculate the width of the container + const maxWidth = welcomeBanners.reduce((acc, curr) => { + const currWidth = curr.width ?? 0; - return currWidth; - }, defaultWidth); + if (acc > currWidth) { + return acc; + } - return ( -
-
- {welcomeBanner.length > 0 ? ( - 1} - autoPlay={true} - navButtonsAlwaysInvisible={true} - indicatorIconButtonProps={{ - style: { - color: "var(--iconActiveBackground)", - }, - }} - activeIndicatorIconButtonProps={{ - style: { - color: "var(--iconActiveColor)", - }, - }} - interval={8000} + return currWidth; + }, defaultWidth); + + return ( +
+
+ {welcomeBanners.length > 0 ? ( + 1} + autoPlay={true} + navButtonsAlwaysInvisible={true} + indicatorIconButtonProps={{ + style: { + color: "var(--iconActiveBackground)", + }, + }} + activeIndicatorIconButtonProps={{ + style: { + color: "var(--iconActiveColor)", + }, + }} + interval={8000} + > + {welcomeBanners.map((item, index) => ( + + ))} + + ) : ( + + )} + +
+
+

Welcome to {productName} 5!

+ +

+ To get you started we have auto-detected your clusters in your + kubeconfig file and added them to the catalog, your centralized + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our{" "} + + Lens Community slack channel + + . +

+ +
- ); - } -} +
+ ); +}; + +export const Welcome = withInjectables( + observer(NonInjectedWelcome), + + { + getProps: (di) => ({ + welcomeMenuItems: di.inject(welcomeMenuItemsInjectable), + welcomeBannerItems: di.inject(welcomeBannerItemsInjectable), + }), + }, +); diff --git a/src/renderer/components/app.scss b/src/renderer/components/app.scss index 0482dd0dd8..40ab38ffdb 100755 --- a/src/renderer/components/app.scss +++ b/src/renderer/components/app.scss @@ -91,6 +91,15 @@ html, body { overflow: hidden; } +#terminal-init { + position: absolute; + top: 0; + left: 0; + height: 0; + visibility: hidden; + overflow: hidden; +} + #app { height: 100%; min-height: 100%; diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index c062289118..cc8a0ef304 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -39,11 +39,10 @@ import { DeleteClusterDialog } from "../delete-cluster-dialog"; import { reaction } from "mobx"; import { navigation } from "../../navigation"; import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync"; -import { TopBar } from "../layout/topbar"; import { catalogURL, getPreviousTabUrl } from "../../../common/routes"; import { withInjectables } from "@ogre-tools/injectable-react"; -import catalogPreviousActiveTabStorageInjectable - from "../+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable"; +import { TopBar } from "../layout/top-bar/top-bar"; +import catalogPreviousActiveTabStorageInjectable from "../+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable"; interface Dependencies { catalogPreviousActiveTabStorage: { get: () => string } diff --git a/src/renderer/components/dock/terminal/terminal.ts b/src/renderer/components/dock/terminal/terminal.ts index 5eaaad1764..06d96d0700 100644 --- a/src/renderer/components/dock/terminal/terminal.ts +++ b/src/renderer/components/dock/terminal/terminal.ts @@ -38,17 +38,9 @@ interface Dependencies { } export class Terminal { - public static readonly spawningPool = (() => { - // terminal element must be in DOM before attaching via xterm.open(elem) - // https://xtermjs.org/docs/api/terminal/classes/terminal/#open - const pool = document.createElement("div"); - - pool.className = "terminal-init"; - pool.style.cssText = "position: absolute; top: 0; left: 0; height: 0; visibility: hidden; overflow: hidden"; - document.body.appendChild(pool); - - return pool; - })(); + public static get spawningPool() { + return document.getElementById("terminal-init"); + } static async preloadFonts() { const fontPath = require("../../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires diff --git a/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap b/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap index 35f29db302..0d1d591668 100644 --- a/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap +++ b/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap @@ -2,10 +2,6 @@ exports[`kube-object-menu given kube object renders 1`] = ` -