From 058494bc73aeef9a4d527fc0d93ca24c48a7ee50 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 5 Apr 2023 10:21:38 -0400 Subject: [PATCH] Introduce clearer boundry between extensions (#7164) - Bundled extensions are always enabled, and are always compatible - Have bundled extensions be loaded asyncronously to support typescript dynamic import (which is typed) as opposed to require Signed-off-by: Sebastian Malton --- .../src/common/protocol-handler/router.ts | 2 +- .../__tests__/extension-loader.test.ts | 4 +- .../extension-discovery.ts | 36 +-- .../create-extension-instance.token.ts | 7 +- .../extension-loader/extension-loader.ts | 109 ++++----- .../core/src/extensions/lens-extension.ts | 7 +- ...gation-using-application-menu.test.ts.snap | 2 +- .../enabled/common/is-enabled.injectable.ts | 9 +- .../create-extension-instance.injectable.ts | 2 +- .../protocol-handler/__test__/router.test.ts | 34 +-- .../lens-protocol-router-main.ts | 28 +-- .../flag-renderer-as-loaded.injectable.ts | 2 +- .../flag-renderer-as-not-loaded.injectable.ts | 2 +- .../attempt-install.injectable.tsx | 2 +- .../unpack-extension.injectable.tsx | 2 +- .../disable-extension.injectable.ts | 27 +++ .../disable-extension.injectable.ts | 18 -- .../disable-extension/disable-extension.ts | 20 -- .../enable-extension.injectable.ts | 27 +++ .../enable-extension.injectable.ts | 18 -- .../enable-extension/enable-extension.ts | 20 -- .../components/+extensions/extensions.tsx | 156 ++++-------- .../components/+extensions/install.tsx | 153 ++++++------ .../+extensions/installed-extensions.tsx | 223 +++++++++--------- .../uninstall-extension.injectable.tsx | 4 +- .../user-extensions.injectable.ts | 2 +- .../create-extension-instance.injectable.ts | 2 +- .../src/bundled-extension.ts | 10 +- .../legacy-extensions/src/lens-extension.ts | 62 +++-- 29 files changed, 440 insertions(+), 550 deletions(-) create mode 100644 packages/core/src/renderer/components/+extensions/disable-extension.injectable.ts delete mode 100644 packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.injectable.ts delete mode 100644 packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.ts create mode 100644 packages/core/src/renderer/components/+extensions/enable-extension.injectable.ts delete mode 100644 packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.injectable.ts delete mode 100644 packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.ts diff --git a/packages/core/src/common/protocol-handler/router.ts b/packages/core/src/common/protocol-handler/router.ts index 7a48dbb343..2147e932e2 100644 --- a/packages/core/src/common/protocol-handler/router.ts +++ b/packages/core/src/common/protocol-handler/router.ts @@ -209,7 +209,7 @@ export abstract class LensProtocolRouter { return name; } - if (!this.dependencies.isExtensionEnabled(extension)) { + if (!extension.isBundled && !this.dependencies.isExtensionEnabled(extension.id)) { this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`); return name; diff --git a/packages/core/src/extensions/__tests__/extension-loader.test.ts b/packages/core/src/extensions/__tests__/extension-loader.test.ts index 9be0507f6d..51a43dfeff 100644 --- a/packages/core/src/extensions/__tests__/extension-loader.test.ts +++ b/packages/core/src/extensions/__tests__/extension-loader.test.ts @@ -113,13 +113,13 @@ describe("ExtensionLoader", () => { }); it("renderer updates extension after ipc broadcast", async () => { - expect(extensionLoader.userExtensions).toEqual(new Map()); + expect(extensionLoader.userExtensions.get()).toEqual(new Map()); await extensionLoader.init(); await delay(10); // Assert the extensions after the extension broadcast event - expect(extensionLoader.userExtensions).toEqual( + expect(extensionLoader.userExtensions.get()).toEqual( new Map([ ["manifest/path", { absolutePath: "/test/1", diff --git a/packages/core/src/extensions/extension-discovery/extension-discovery.ts b/packages/core/src/extensions/extension-discovery/extension-discovery.ts index 7a39fa7467..f097af9731 100644 --- a/packages/core/src/extensions/extension-discovery/extension-discovery.ts +++ b/packages/core/src/extensions/extension-discovery/extension-discovery.ts @@ -10,7 +10,7 @@ import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc import { toJS } from "../../common/utils"; import { isErrnoException } from "@k8slens/utilities"; import type { ExtensionLoader } from "../extension-loader"; -import type { InstalledExtension, LensExtensionId, LensExtensionManifest } from "@k8slens/legacy-extensions"; +import type { InstalledExtension, LensExtensionId, LensExtensionManifest, ExternalInstalledExtension } from "@k8slens/legacy-extensions"; import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store"; import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling"; import { requestInitialExtensionDiscovery } from "../../renderer/ipc"; @@ -73,10 +73,6 @@ interface ExtensionDiscoveryChannelMessage { */ const isDirectoryLike = (lstat: Stats) => lstat.isDirectory() || lstat.isSymbolicLink(); -interface LoadFromFolderOptions { - isBundled?: boolean; -} - interface ExtensionDiscoveryEvents { add: (ext: InstalledExtension) => void; remove: (extId: LensExtensionId) => void; @@ -271,7 +267,7 @@ export class ExtensionDiscovery { * @param extensionId The ID of the extension to uninstall. */ async uninstallExtension(extensionId: LensExtensionId): Promise { - const extension = this.extensions.get(extensionId) ?? this.dependencies.extensionLoader.getExtension(extensionId); + const extension = this.extensions.get(extensionId) ?? this.dependencies.extensionLoader.getExtensionById(extensionId); if (!extension) { return void this.dependencies.logger.warn(`${logModule} could not uninstall extension, not found`, { id: extensionId }); @@ -330,24 +326,26 @@ export class ExtensionDiscovery { * Returns InstalledExtension from path to package.json file. * Also updates this.packagesJson. */ - protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise { + protected async loadExtensionFromFolder(folderPath: string): Promise { + const manifestPath = this.dependencies.joinPaths(folderPath, manifestFilename); + try { const manifest = await this.dependencies.readJsonFile(manifestPath) as unknown as LensExtensionManifest; - const id = isBundled ? manifestPath : this.getInstalledManifestPath(manifest.name); - const isEnabled = this.dependencies.isExtensionEnabled({ id, isBundled }); + const id = this.getInstalledManifestPath(manifest.name); + const isEnabled = this.dependencies.isExtensionEnabled(id); const extensionDir = this.dependencies.getDirnameOfPath(manifestPath); const npmPackage = this.dependencies.joinPaths(extensionDir, `${manifest.name}-${manifest.version}.tgz`); const absolutePath = this.dependencies.isProduction && await this.dependencies.pathExists(npmPackage) ? npmPackage : extensionDir; - const isCompatible = isBundled || this.dependencies.isCompatibleExtension(manifest); + const isCompatible = this.dependencies.isCompatibleExtension(manifest); return { id, absolutePath, manifestPath: id, manifest, - isBundled, + isBundled: false, isEnabled, isCompatible, }; @@ -363,14 +361,14 @@ export class ExtensionDiscovery { } } - async ensureExtensions(): Promise> { + async ensureExtensions(): Promise> { const userExtensions = await this.loadFromFolder(this.localFolderPath); return this.extensions = new Map(userExtensions.map(extension => [extension.id, extension])); } - async loadFromFolder(folderPath: string): Promise { - const extensions: InstalledExtension[] = []; + async loadFromFolder(folderPath: string): Promise { + const extensions: ExternalInstalledExtension[] = []; const paths = await this.dependencies.readDirectory(folderPath); for (const fileName of paths) { @@ -403,16 +401,6 @@ export class ExtensionDiscovery { return extensions; } - /** - * Loads extension from absolute path, updates this.packagesJson to include it and returns the extension. - * @param folderPath Folder path to extension - */ - async loadExtensionFromFolder(folderPath: string, { isBundled = false }: LoadFromFolderOptions = {}): Promise { - const manifestPath = this.dependencies.joinPaths(folderPath, manifestFilename); - - return this.getByManifest(manifestPath, { isBundled }); - } - toJSON(): ExtensionDiscoveryChannelMessage { return toJS({ isLoaded: this.isLoaded, diff --git a/packages/core/src/extensions/extension-loader/create-extension-instance.token.ts b/packages/core/src/extensions/extension-loader/create-extension-instance.token.ts index a064cf55ec..97ded49d84 100644 --- a/packages/core/src/extensions/extension-loader/create-extension-instance.token.ts +++ b/packages/core/src/extensions/extension-loader/create-extension-instance.token.ts @@ -3,11 +3,14 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { LensExtensionConstructor, InstalledExtension } from "@k8slens/legacy-extensions"; +import type { LensExtensionConstructor, BundledInstalledExtension, ExternalInstalledExtension, BundledLensExtensionConstructor } from "@k8slens/legacy-extensions"; import { getInjectionToken } from "@ogre-tools/injectable"; import type { LensExtension } from "../lens-extension"; -export type CreateExtensionInstance = (ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => LensExtension; +export interface CreateExtensionInstance { + (ExtensionClass: LensExtensionConstructor, extension: ExternalInstalledExtension): LensExtension; + (ExtensionClass: BundledLensExtensionConstructor, extension: BundledInstalledExtension): LensExtension; +} export const createExtensionInstanceInjectionToken = getInjectionToken({ id: "create-extension-instance-token", diff --git a/packages/core/src/extensions/extension-loader/extension-loader.ts b/packages/core/src/extensions/extension-loader/extension-loader.ts index 4a24ad23a1..3ba5a16728 100644 --- a/packages/core/src/extensions/extension-loader/extension-loader.ts +++ b/packages/core/src/extensions/extension-loader/extension-loader.ts @@ -6,9 +6,10 @@ import { ipcMain, ipcRenderer } from "electron"; import { isEqual } from "lodash"; import type { ObservableMap } from "mobx"; -import { action, computed, makeObservable, toJS, observable, observe, reaction, when } from "mobx"; +import { runInAction, action, computed, toJS, observable, reaction, when } from "mobx"; import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc"; -import { isDefined } from "@k8slens/utilities"; +import { isDefined, iter } from "@k8slens/utilities"; +import type { ExternalInstalledExtension, InstalledExtension, LensExtensionConstructor, LensExtensionId, BundledExtension } from "@k8slens/legacy-extensions"; import type { LensExtension } from "../lens-extension"; import { extensionLoaderFromMainChannel, extensionLoaderFromRendererChannel } from "../../common/ipc/extension-handling"; import { requestExtensionLoaderInitialState } from "../../renderer/ipc"; @@ -19,7 +20,6 @@ import type { Extension } from "./extension/extension.injectable"; import type { Logger } from "../../common/logger"; import type { JoinPaths } from "../../common/path/join-paths.injectable"; import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; -import type { LensExtensionId, BundledExtension, InstalledExtension, LensExtensionConstructor } from "@k8slens/legacy-extensions"; import type { UpdateExtensionsState } from "../../features/extensions/enabled/common/update-state.injectable"; const logModule = "[EXTENSIONS-LOADER]"; @@ -60,51 +60,21 @@ export class ExtensionLoader { */ protected readonly nonInstancesByName = observable.set(); - /** - * This is updated by the `observe` in the constructor. DO NOT write directly to it - */ - protected readonly instancesByName = observable.map(); + protected readonly instancesByName = computed(() => new Map(( + iter.chain(this.dependencies.extensionInstances.entries()) + .map(([, instance]) => [instance.name, instance]) + ))); private readonly onRemoveExtensionId = new EventEmitter<[string]>(); - @observable isLoaded = false; + readonly isLoaded = observable.box(false); - get whenLoaded() { - return when(() => this.isLoaded); - } + constructor(protected readonly dependencies: Dependencies) {} - constructor(protected readonly dependencies: Dependencies) { - makeObservable(this); - - observe(this.dependencies.extensionInstances, change => { - switch (change.type) { - case "add": - if (this.instancesByName.has(change.newValue.name)) { - throw new TypeError("Extension names must be unique"); - } - - this.instancesByName.set(change.newValue.name, change.newValue); - break; - case "delete": - this.instancesByName.delete(change.oldValue.name); - break; - case "update": - throw new Error("Extension instances shouldn't be updated"); - } - }); - } - - @computed get userExtensions(): Map { - const extensions = this.toJSON(); - - extensions.forEach((ext, extId) => { - if (ext.isBundled) { - extensions.delete(extId); - } - }); - - return extensions; - } + readonly userExtensions = computed(() => new Map(( + this.extensions.toJSON() + .filter(([, extension]) => !extension.isBundled) + ))); /** * Get the extension instance by its manifest name @@ -120,19 +90,18 @@ export class ExtensionLoader { return null; } - return this.instancesByName.get(name); + return this.instancesByName.get().get(name); } // Transform userExtensions to a state object for storing into ExtensionsStore - @computed get storeState() { - return Array.from(this.userExtensions) - .map(([extId, extension]) => [extId, { - enabled: extension.isEnabled, - name: extension.manifest.name, - }] as const); - } + readonly storeState = computed(() => Array.from( + this.userExtensions.get(), + ([extId, extension]) => [extId, { + enabled: extension.isEnabled, + name: extension.manifest.name, + }] as const, + )); - @action async init() { if (ipcMain) { await this.initMain(); @@ -140,7 +109,7 @@ export class ExtensionLoader { await this.initRenderer(); } - await Promise.all([this.whenLoaded]); + await when(() => this.isLoaded.get()); // broadcasting extensions between main/renderer processes reaction(() => this.toJSON(), () => this.broadcastExtensions(), { @@ -148,8 +117,7 @@ export class ExtensionLoader { }); reaction( - () => this.storeState, - + () => this.storeState.get(), (state) => { this.dependencies.updateExtensionsState(state); }, @@ -199,18 +167,20 @@ export class ExtensionLoader { setIsEnabled(lensExtensionId: LensExtensionId, isEnabled: boolean) { const extension = this.extensions.get(lensExtensionId); - assert(extension, `Must register extension ${lensExtensionId} with before enabling it`); + assert(extension, `Extension "${lensExtensionId}" must be registered before it can be enabled.`); + assert(!extension.isBundled, `Cannot change the enabled state of a bundled extension`); extension.isEnabled = isEnabled; } protected async initMain() { - this.isLoaded = true; + runInAction(() => { + this.isLoaded.set(true); + }); + await this.autoInitExtensions(); - ipcMainHandle(extensionLoaderFromMainChannel, () => { - return Array.from(this.toJSON()); - }); + ipcMainHandle(extensionLoaderFromMainChannel, () => [...this.toJSON()]); ipcMainOn(extensionLoaderFromRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => { this.syncExtensions(extensions); @@ -219,7 +189,9 @@ export class ExtensionLoader { protected async initRenderer() { const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => { - this.isLoaded = true; + runInAction(() => { + this.isLoaded.set(true); + }); this.syncExtensions(extensions); const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId); @@ -255,10 +227,10 @@ export class ExtensionLoader { } protected async loadBundledExtensions() { - return this.dependencies.bundledExtensions - .map(extension => { + const bundledExtensions = await Promise.all((this.dependencies.bundledExtensions + .map(async extension => { try { - const LensExtensionClass = extension[this.dependencies.extensionEntryPointName](); + const LensExtensionClass = await extension[this.dependencies.extensionEntryPointName](); if (!LensExtensionClass) { return null; @@ -291,7 +263,9 @@ export class ExtensionLoader { return null; } }) - .filter(isDefined); + )); + + return bundledExtensions.filter(isDefined); } protected async loadExtensions(extensions: ExtensionBeingActivated[]): Promise { @@ -332,6 +306,7 @@ export class ExtensionLoader { // 4. Return ExtensionLoading[] return [...installedExtensions.entries()] + .filter((entry): entry is [string, ExternalInstalledExtension] => !entry[1].isBundled) .map(([extId, extension]) => { const alreadyInit = this.dependencies.extensionInstances.has(extId) || this.nonInstancesByName.has(extension.manifest.name); @@ -391,7 +366,7 @@ export class ExtensionLoader { return loadedExtensions; } - protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null { + protected requireExtension(extension: ExternalInstalledExtension): LensExtensionConstructor | null { const extRelativePath = extension.manifest[this.dependencies.extensionEntryPointName]; if (!extRelativePath) { @@ -411,7 +386,7 @@ export class ExtensionLoader { return null; } - getExtension(extId: LensExtensionId) { + getExtensionById(extId: LensExtensionId) { return this.extensions.get(extId); } diff --git a/packages/core/src/extensions/lens-extension.ts b/packages/core/src/extensions/lens-extension.ts index ec5eca0cd9..ea451ca56e 100644 --- a/packages/core/src/extensions/lens-extension.ts +++ b/packages/core/src/extensions/lens-extension.ts @@ -9,7 +9,6 @@ import type { LensExtensionDependencies } from "./lens-extension-set-dependencie import type { ProtocolHandlerRegistration } from "../common/protocol-handler/registration"; import type { InstalledExtension, LegacyLensExtension, LensExtensionId, LensExtensionManifest } from "@k8slens/legacy-extensions"; - export const lensExtensionDependencies = Symbol("lens-extension-dependencies"); export const Disposers = Symbol("disposers"); @@ -42,14 +41,12 @@ export class LensExtension< [Disposers] = disposer(); constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) { - makeObservable(this); - // id is the name of the manifest this.id = id; - - this.manifest = manifest; + this.manifest = manifest as LensExtensionManifest; this.manifestPath = manifestPath; this.isBundled = !!isBundled; + makeObservable(this); } get name() { diff --git a/packages/core/src/features/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap b/packages/core/src/features/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap index dbaf12b978..4fb639ff2b 100644 --- a/packages/core/src/features/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap +++ b/packages/core/src/features/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap @@ -376,7 +376,7 @@ exports[`extensions - navigation using application menu when navigating to exten

Add new features via Lens Extensions. Check out the diff --git a/packages/core/src/features/extensions/enabled/common/is-enabled.injectable.ts b/packages/core/src/features/extensions/enabled/common/is-enabled.injectable.ts index bb7f531cb3..f2f6114a1f 100644 --- a/packages/core/src/features/extensions/enabled/common/is-enabled.injectable.ts +++ b/packages/core/src/features/extensions/enabled/common/is-enabled.injectable.ts @@ -5,19 +5,14 @@ import { getInjectable } from "@ogre-tools/injectable"; import enabledExtensionsStateInjectable from "./state.injectable"; -export interface IsEnabledExtensionDescriptor { - readonly id: string; - readonly isBundled: boolean; -} - -export type IsExtensionEnabled = (desc: IsEnabledExtensionDescriptor) => boolean; +export type IsExtensionEnabled = (id: string) => boolean; const isExtensionEnabledInjectable = getInjectable({ id: "is-extension-enabled", instantiate: (di): IsExtensionEnabled => { const state = di.inject(enabledExtensionsStateInjectable); - return ({ id, isBundled }) => isBundled || (state.get(id)?.enabled ?? false); + return (id) => (state.get(id)?.enabled ?? false); }, }); diff --git a/packages/core/src/main/extension-loader/create-extension-instance.injectable.ts b/packages/core/src/main/extension-loader/create-extension-instance.injectable.ts index cd20ab7ede..d0fe1e25c3 100644 --- a/packages/core/src/main/extension-loader/create-extension-instance.injectable.ts +++ b/packages/core/src/main/extension-loader/create-extension-instance.injectable.ts @@ -24,7 +24,7 @@ const createExtensionInstanceInjectable = getInjectable({ }; return (ExtensionClass, extension) => { - const instance = new ExtensionClass(extension) as LensMainExtension; + const instance = new ExtensionClass(extension as any) as LensMainExtension; (instance as Writable)[lensExtensionDependencies] = deps; diff --git a/packages/core/src/main/protocol-handler/__test__/router.test.ts b/packages/core/src/main/protocol-handler/__test__/router.test.ts index e3eddb6b34..11e49dc113 100644 --- a/packages/core/src/main/protocol-handler/__test__/router.test.ts +++ b/packages/core/src/main/protocol-handler/__test__/router.test.ts @@ -10,16 +10,12 @@ import { noop } from "@k8slens/utilities"; import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable"; -import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; import { LensExtension } from "../../../extensions/lens-extension"; import type { ObservableMap } from "mobx"; +import { runInAction } from "mobx"; import extensionInstancesInjectable from "../../../extensions/extension-loader/extension-instances.injectable"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable"; -import pathExistsSyncInjectable from "../../../common/fs/path-exists-sync.injectable"; -import pathExistsInjectable from "../../../common/fs/path-exists.injectable"; -import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable"; -import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable"; import type { LensExtensionId } from "@k8slens/legacy-extensions"; import type { LensExtensionState } from "../../../features/extensions/enabled/common/state.injectable"; import enabledExtensionsStateInjectable from "../../../features/extensions/enabled/common/state.injectable"; @@ -39,16 +35,8 @@ describe("protocol router tests", () => { beforeEach(async () => { const di = getDiForUnitTesting(); - di.override(pathExistsInjectable, () => () => { throw new Error("tried call pathExists without override"); }); - di.override(pathExistsSyncInjectable, () => () => { throw new Error("tried call pathExistsSync without override"); }); - di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); }); - di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); }); - enabledExtensions = di.inject(enabledExtensionsStateInjectable); - - di.permitSideEffects(getConfigurationFileModelInjectable); - - di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); broadcastMessageMock = jest.fn(); di.override(broadcastMessageInjectable, () => broadcastMessageMock); @@ -56,7 +44,9 @@ describe("protocol router tests", () => { extensionInstances = di.inject(extensionInstancesInjectable); lpr = di.inject(lensProtocolRouterMainInjectable); - lpr.rendererLoaded = true; + runInAction(() => { + lpr.rendererLoaded.set(true); + }); }); it("should broadcast invalid protocol on non-lens URLs", async () => { @@ -69,7 +59,19 @@ describe("protocol router tests", () => { expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInvalid, "invalid host", "lens://foobar"); }); - it("should not throw when has valid host", async () => { + it("should broadcast internal route when called with valid host", async () => { + lpr.addInternalHandler("/", noop); + + try { + expect(await lpr.route("lens://app")).toBeUndefined(); + } catch (error) { + expect(throwIfDefined(error)).not.toThrow(); + } + + expect(broadcastMessageMock).toHaveBeenCalledWith(ProtocolHandlerInternal, "lens://app", "matched"); + }); + + it("should broadcast external route when called with valid host", async () => { const extId = uuid.v4(); const ext = new LensExtension({ id: extId, diff --git a/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts b/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts index 8185ca4e68..1018420b10 100644 --- a/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts +++ b/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts @@ -6,7 +6,7 @@ import * as proto from "../../../common/protocol-handler"; import URLParse from "url-parse"; import type { LensExtension } from "../../../extensions/lens-extension"; -import { observable, when, makeObservable } from "mobx"; +import { observable, when } from "mobx"; import type { LensProtocolRouterDependencies, RouteAttempt } from "../../../common/protocol-handler"; import { ProtocolHandlerInvalid } from "../../../common/protocol-handler"; import { disposer, noop } from "@k8slens/utilities"; @@ -39,17 +39,15 @@ export interface LensProtocolRouterMainDependencies extends LensProtocolRouterDe } export class LensProtocolRouterMain extends proto.LensProtocolRouter { - private missingExtensionHandlers: FallbackHandler[] = []; + private readonly missingExtensionHandlers: FallbackHandler[] = []; // TODO: This is used to solve out-of-place temporal dependency. Remove, and solve otherwise. - @observable rendererLoaded = false; + readonly rendererLoaded = observable.box(false); - protected disposers = disposer(); + protected readonly disposers = disposer(); constructor(protected readonly dependencies: LensProtocolRouterMainDependencies) { super(dependencies); - - makeObservable(this); } public cleanup() { @@ -118,13 +116,12 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { protected _routeToInternal(url: URLParse>): RouteAttempt { const rawUrl = url.toString(); // for sending to renderer const attempt = super._routeToInternal(url); + const broadcastToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerInternal, rawUrl, attempt); - const sendRoutingToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerInternal, rawUrl, attempt); - - if (this.rendererLoaded) { - sendRoutingToRenderer(); + if (this.rendererLoaded.get()) { + broadcastToRenderer(); } else { - this.disposers.push(when(() => this.rendererLoaded, sendRoutingToRenderer)); + this.disposers.push(when(() => this.rendererLoaded.get(), broadcastToRenderer)); } return attempt; @@ -141,13 +138,12 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { * argument. */ const attempt = await super._routeToExtension(new URLParse(url.toString(), true)); + const broadcastToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerExtension, rawUrl, attempt); - const sendRoutingToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerExtension, rawUrl, attempt); - - if (this.rendererLoaded) { - sendRoutingToRenderer(); + if (this.rendererLoaded.get()) { + broadcastToRenderer(); } else { - this.disposers.push(when(() => this.rendererLoaded, sendRoutingToRenderer)); + this.disposers.push(when(() => this.rendererLoaded.get(), broadcastToRenderer)); } return attempt; diff --git a/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts b/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts index 2fe71ed7d5..3096c5587f 100644 --- a/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts +++ b/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts @@ -16,7 +16,7 @@ const flagRendererAsLoadedInjectable = getInjectable({ runInAction(() => { // Todo: remove this kludge which enables out-of-place temporal dependency. - lensProtocolRouterMain.rendererLoaded = true; + lensProtocolRouterMain.rendererLoaded.set(true); }); }, }), diff --git a/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts b/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts index 899b234f39..078cc7def0 100644 --- a/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts +++ b/packages/core/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts @@ -16,7 +16,7 @@ const flagRendererAsNotLoadedInjectable = getInjectable({ runInAction(() => { // Todo: remove this kludge which enables out-of-place temporal dependency. - lensProtocolRouterMain.rendererLoaded = false; + lensProtocolRouterMain.rendererLoaded.set(false); }); return undefined; diff --git a/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx b/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx index 2f4f36bc34..053892d7ab 100644 --- a/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx @@ -86,7 +86,7 @@ const attemptInstall = ({ } const extensionFolder = getExtensionDestFolder(name); - const installedExtension = extensionLoader.getExtension(validatedRequest.id); + const installedExtension = extensionLoader.getExtensionById(validatedRequest.id); if (installedExtension) { const { version: oldVersion } = installedExtension.manifest; diff --git a/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx b/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx index 5317fbfc48..47dd5e02cf 100644 --- a/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx @@ -73,7 +73,7 @@ const unpackExtensionInjectable = getInjectable({ await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true }); // wait for the loader has actually install it - await when(() => extensionLoader.userExtensions.has(id)); + await when(() => extensionLoader.userExtensions.get().has(id)); // Enable installed extensions by default. extensionLoader.setIsEnabled(id, true); diff --git a/packages/core/src/renderer/components/+extensions/disable-extension.injectable.ts b/packages/core/src/renderer/components/+extensions/disable-extension.injectable.ts new file mode 100644 index 0000000000..b2bf76df3e --- /dev/null +++ b/packages/core/src/renderer/components/+extensions/disable-extension.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { LensExtensionId } from "@k8slens/legacy-extensions"; +import { getInjectable } from "@ogre-tools/injectable"; +import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; + +export type DisableExtension = (extId: LensExtensionId) => void; + +const disableExtensionInjectable = getInjectable({ + id: "disable-extension", + + instantiate: (di): DisableExtension => { + const extensionLoader = di.inject(extensionLoaderInjectable); + + return (extId) => { + const ext = extensionLoader.getExtensionById(extId); + + if (ext && !ext.isBundled) { + ext.isEnabled = false; + } + }; + }, +}); + +export default disableExtensionInjectable; diff --git a/packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.injectable.ts b/packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.injectable.ts deleted file mode 100644 index 274679c331..0000000000 --- a/packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.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 extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; -import { disableExtension } from "./disable-extension"; - -const disableExtensionInjectable = getInjectable({ - id: "disable-extension", - - instantiate: (di) => - disableExtension({ - extensionLoader: di.inject(extensionLoaderInjectable), - }), -}); - -export default disableExtensionInjectable; diff --git a/packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.ts b/packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.ts deleted file mode 100644 index 834577a31c..0000000000 --- a/packages/core/src/renderer/components/+extensions/disable-extension/disable-extension.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { LensExtensionId } from "@k8slens/legacy-extensions"; -import type { ExtensionLoader } from "../../../../extensions/extension-loader"; - -interface Dependencies { - extensionLoader: ExtensionLoader; -} - -export const disableExtension = - ({ extensionLoader }: Dependencies) => - (id: LensExtensionId) => { - const extension = extensionLoader.getExtension(id); - - if (extension) { - extension.isEnabled = false; - } - }; diff --git a/packages/core/src/renderer/components/+extensions/enable-extension.injectable.ts b/packages/core/src/renderer/components/+extensions/enable-extension.injectable.ts new file mode 100644 index 0000000000..b9372f4a33 --- /dev/null +++ b/packages/core/src/renderer/components/+extensions/enable-extension.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { LensExtensionId } from "@k8slens/legacy-extensions"; +import { getInjectable } from "@ogre-tools/injectable"; +import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; + +export type EnableExtension = (extId: LensExtensionId) => void; + +const enableExtensionInjectable = getInjectable({ + id: "enable-extension", + + instantiate: (di): EnableExtension => { + const extensionLoader = di.inject(extensionLoaderInjectable); + + return (extId) => { + const ext = extensionLoader.getExtensionById(extId); + + if (ext && !ext.isBundled) { + ext.isEnabled = true; + } + }; + }, +}); + +export default enableExtensionInjectable; diff --git a/packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.injectable.ts b/packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.injectable.ts deleted file mode 100644 index 19e5e83233..0000000000 --- a/packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.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 extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; -import { enableExtension } from "./enable-extension"; - -const enableExtensionInjectable = getInjectable({ - id: "enable-extension", - - instantiate: (di) => - enableExtension({ - extensionLoader: di.inject(extensionLoaderInjectable), - }), -}); - -export default enableExtensionInjectable; diff --git a/packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.ts b/packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.ts deleted file mode 100644 index 5692e9efdc..0000000000 --- a/packages/core/src/renderer/components/+extensions/enable-extension/enable-extension.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { LensExtensionId } from "@k8slens/legacy-extensions"; -import type { ExtensionLoader } from "../../../../extensions/extension-loader"; - -interface Dependencies { - extensionLoader: ExtensionLoader; -} - -export const enableExtension = - ({ extensionLoader }: Dependencies) => - (id: LensExtensionId) => { - const extension = extensionLoader.getExtension(id); - - if (extension) { - extension.isEnabled = true; - } - }; diff --git a/packages/core/src/renderer/components/+extensions/extensions.tsx b/packages/core/src/renderer/components/+extensions/extensions.tsx index ab8049fe92..dcb6a4b70f 100644 --- a/packages/core/src/renderer/components/+extensions/extensions.tsx +++ b/packages/core/src/renderer/components/+extensions/extensions.tsx @@ -4,138 +4,66 @@ */ import styles from "./extensions.module.scss"; -import type { IComputedValue } from "mobx"; -import { - makeObservable, - observable, - reaction, - when, -} from "mobx"; -import { disposeOnUnmount, observer } from "mobx-react"; import React from "react"; import { DropFileInput } from "../input"; -import { Install } from "./install"; +import { ExtensionInstall } from "./install"; import { InstalledExtensions } from "./installed-extensions"; import { Notice } from "./notice"; import { SettingLayout } from "../layout/setting-layout"; import { docsUrl } from "../../../common/vars"; import { withInjectables } from "@ogre-tools/injectable-react"; - -import userExtensionsInjectable from "./user-extensions/user-extensions.injectable"; -import enableExtensionInjectable from "./enable-extension/enable-extension.injectable"; -import disableExtensionInjectable from "./disable-extension/disable-extension.injectable"; -import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable"; -import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable"; -import type { InstallExtensionFromInput } from "./install-extension-from-input.injectable"; -import installExtensionFromInputInjectable from "./install-extension-from-input.injectable"; -import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.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"; import Gutter from "../gutter/gutter"; -import type { InstalledExtension, LensExtensionId } from "@k8slens/legacy-extensions"; + +const ExtensionsNotice = () => ( + +

+ {"Add new features via Lens Extensions. Check out the "} + + docs + + {" and list of "} + + available extensions + + . +

+ +); interface Dependencies { - userExtensions: IComputedValue; - enableExtension: (id: LensExtensionId) => void; - disableExtension: (id: LensExtensionId) => void; - confirmUninstallExtension: ConfirmUninstallExtension; - installExtensionFromInput: InstallExtensionFromInput; - installFromSelectFileDialog: () => Promise; installOnDrop: InstallOnDrop; - extensionInstallationStateStore: ExtensionInstallationStateStore; } -@observer -class NonInjectedExtensions extends React.Component { - @observable installPath = ""; - - constructor(props: Dependencies) { - super(props); - makeObservable(this); - } - - componentDidMount() { - disposeOnUnmount(this, [ - reaction(() => this.props.userExtensions.get().length, (curSize, prevSize) => { - if (curSize > prevSize) { - disposeOnUnmount(this, [ - when(() => !this.props.extensionInstallationStateStore.anyInstalling, () => this.installPath = ""), - ]); - } - }), - ]); - } - - render() { - const userExtensions = this.props.userExtensions.get(); - - return ( - - -
-

Extensions

- - -

- {"Add new features via Lens Extensions. Check out the "} - - docs - - {" and list of "} - - available extensions - - . -

-
- - (this.installPath = value)} - installFromInput={() => this.props.installExtensionFromInput(this.installPath)} - installFromSelectFileDialog={this.props.installFromSelectFileDialog} - installPath={this.installPath} - /> - - - - -
-
-
- ); - } -} +const NonInjectedExtensions = ({ installOnDrop }: Dependencies) => ( + + +
+

Extensions

+ + + + +
+
+
+); export const Extensions = withInjectables(NonInjectedExtensions, { getProps: (di) => ({ - userExtensions: di.inject(userExtensionsInjectable), - enableExtension: di.inject(enableExtensionInjectable), - disableExtension: di.inject(disableExtensionInjectable), - confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable), - installExtensionFromInput: di.inject(installExtensionFromInputInjectable), installOnDrop: di.inject(installOnDropInjectable), - installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable), - extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), }), }); diff --git a/packages/core/src/renderer/components/+extensions/install.tsx b/packages/core/src/renderer/components/+extensions/install.tsx index 63c09f8b18..ffbdc3ea2b 100644 --- a/packages/core/src/renderer/components/+extensions/install.tsx +++ b/packages/core/src/renderer/components/+extensions/install.tsx @@ -4,7 +4,7 @@ */ import styles from "./install.module.scss"; -import React from "react"; +import React, { useEffect, useRef, useState } from "react"; import { prevDefault } from "@k8slens/utilities"; import { Button } from "../button"; import { Icon } from "../icon"; @@ -16,17 +16,16 @@ import type { ExtensionInstallationStateStore } from "../../../extensions/extens import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; import { unionInputValidatorsAsync } from "../input/input_validators"; - -export interface InstallProps { - installPath: string; - supportedFormats: string[]; - onChange: (path: string) => void; - installFromInput: () => void; - installFromSelectFileDialog: () => void; -} +import { supportedExtensionFormats } from "./supported-extension-formats"; +import type { InstallExtensionFromInput } from "./install-extension-from-input.injectable"; +import type { InstallFromSelectFileDialog } from "./install-from-select-file-dialog.injectable"; +import installExtensionFromInputInjectable from "./install-extension-from-input.injectable"; +import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable"; interface Dependencies { - extensionInstallationStateStore: ExtensionInstallationStateStore; + installState: ExtensionInstallationStateStore; + installExtensionFromInput: InstallExtensionFromInput; + installFromSelectFileDialog: InstallFromSelectFileDialog; } const installInputValidator = unionInputValidatorsAsync( @@ -38,71 +37,75 @@ const installInputValidator = unionInputValidatorsAsync( InputValidators.isPath, ); -const NonInjectedInstall: React.FC = ({ - installPath, - supportedFormats, - onChange, - installFromInput, +const installTitle = `Name or file path or URL to an extension package (${supportedExtensionFormats.join(", ")})`; + +const NonInjectedInstall = observer(({ + installExtensionFromInput, installFromSelectFileDialog, - extensionInstallationStateStore, -}) => ( -
- -
-
- - )} - /> -
-
-
-
- - Pro-Tip - : you can drag and drop a tarball file to this area - -
-); + installState, +}: Dependencies) => { + const [installPath, setInstallPath] = useState(""); + const prevAnyInstalling = useRef(installState.anyInstalling); -export const Install = withInjectables( - observer(NonInjectedInstall), - { - getProps: (di, props) => ({ - extensionInstallationStateStore: di.inject( - extensionInstallationStateStoreInjectable, - ), + useEffect(() => { + const currentlyInstalling = installState.anyInstalling; + const previouslyInstalling = prevAnyInstalling.current; - ...props, - }), - }, -); + if (!currentlyInstalling && previouslyInstalling) { + prevAnyInstalling.current = false; + setInstallPath(""); + } + }, [installState.anyInstalling]); + + return ( +
+ +
+
+ installExtensionFromInput(installPath)} + iconRight={( + + )} + /> +
+
+
+
+ + Pro-Tip + : you can drag and drop a tarball file to this area + +
+ ); +}); + +export const ExtensionInstall = withInjectables(NonInjectedInstall, { + getProps: (di, props) => ({ + ...props, + installState: di.inject(extensionInstallationStateStoreInjectable), + installExtensionFromInput: di.inject(installExtensionFromInputInjectable), + installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable), + }), +}); diff --git a/packages/core/src/renderer/components/+extensions/installed-extensions.tsx b/packages/core/src/renderer/components/+extensions/installed-extensions.tsx index f420b63f59..62c40b253a 100644 --- a/packages/core/src/renderer/components/+extensions/installed-extensions.tsx +++ b/packages/core/src/renderer/components/+extensions/installed-extensions.tsx @@ -4,8 +4,7 @@ */ import styles from "./installed-extensions.module.scss"; -import React, { useMemo } from "react"; -import type { ExtensionDiscovery } from "../../../extensions/extension-discovery/extension-discovery"; +import React from "react"; import { Icon } from "../icon"; import { List } from "../list/list"; import { MenuActions, MenuItem } from "../menu"; @@ -17,18 +16,27 @@ import extensionDiscoveryInjectable from "../../../extensions/extension-discover import { withInjectables } from "@ogre-tools/injectable-react"; 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"; -import type { InstalledExtension, LensExtensionId } from "@k8slens/legacy-extensions"; +import type { InstalledExtension } from "@k8slens/legacy-extensions"; +import type { IComputedValue } from "mobx"; +import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable"; +import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable"; +import type { DisableExtension } from "./disable-extension.injectable"; +import disableExtensionInjectable from "./disable-extension.injectable"; +import type { EnableExtension } from "./enable-extension.injectable"; +import enableExtensionInjectable from "./enable-extension.injectable"; +import userExtensionsInjectable from "./user-extensions/user-extensions.injectable"; +import type { ExtensionDiscovery } from "../../../extensions/extension-discovery/extension-discovery"; export interface InstalledExtensionsProps { - extensions: InstalledExtension[]; - enable: (id: LensExtensionId) => void; - disable: (id: LensExtensionId) => void; - uninstall: (extension: InstalledExtension) => void; } interface Dependencies { extensionDiscovery: ExtensionDiscovery; extensionInstallationStateStore: ExtensionInstallationStateStore; + userExtensions: IComputedValue; + enableExtension: EnableExtension; + disableExtension: DisableExtension; + confirmUninstallExtension: ConfirmUninstallExtension; } function getStatus(extension: InstalledExtension) { @@ -39,106 +47,20 @@ function getStatus(extension: InstalledExtension) { return extension.isEnabled ? "Enabled" : "Disabled"; } -const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extensionInstallationStateStore, extensions, uninstall, enable, disable }: Dependencies & InstalledExtensionsProps) => { - const columns = useMemo( - () => [ - { - Header: "Name", - accessor: "extension", - width: 200, - sortType: (rowA: Row, rowB: Row) => { // Custom sorting for extension name - const nameA = extensions[rowA.index].manifest.name; - const nameB = extensions[rowB.index].manifest.name; - - if (nameA > nameB) return -1; - if (nameB > nameA) return 1; - - return 0; - }, - }, - { - Header: "Version", - accessor: "version", - }, - { - Header: "Status", - accessor: "status", - }, - { - Header: "", - accessor: "actions", - disableSortBy: true, - width: 20, - className: "actions", - }, - ], [], - ); - - const data = useMemo( - () => extensions.map(extension => { - const { id, isEnabled, isCompatible, manifest } = extension; - const { name, description, version } = manifest; - const isUninstalling = extensionInstallationStateStore.isExtensionUninstalling(id); - - return { - extension: ( -
-
-
{name}
-
{description}
-
-
- ), - version, - status: ( -
- {getStatus(extension)} -
- ), - actions: ( - - {isCompatible && ( - <> - {isEnabled ? ( - disable(id)} - > - - Disable - - ) : ( - enable(id)} - > - - Enable - - )} - - )} - - uninstall(extension)} - > - - Uninstall - - - ), - }; - }), [extensions, extensionInstallationStateStore.anyUninstalling], - ); - +const NonInjectedInstalledExtensions = observer(({ + extensionDiscovery, + extensionInstallationStateStore, + userExtensions, + confirmUninstallExtension, + enableExtension, + disableExtension, +}: Dependencies & InstalledExtensionsProps) => { if (!extensionDiscovery.isLoaded) { return
; } + const extensions = userExtensions.get(); + if (extensions.length == 0) { return (
@@ -151,13 +73,96 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension ); } + const toggleExtensionWith = (enabled: boolean) => ( + enabled + ? disableExtension + : enableExtension + ); + return (
Installed extensions} - columns={columns} - data={data} - items={extensions} + columns={[ + { + Header: "Name", + accessor: "extension", + width: 200, + sortType: (rowA: Row, rowB: Row) => { // Custom sorting for extension name + const nameA = extensions[rowA.index].manifest.name; + const nameB = extensions[rowB.index].manifest.name; + + if (nameA > nameB) return -1; + if (nameB > nameA) return 1; + + return 0; + }, + }, + { + Header: "Version", + accessor: "version", + }, + { + Header: "Status", + accessor: "status", + }, + { + Header: "", + accessor: "actions", + disableSortBy: true, + width: 20, + }, + ]} + data={extensions.map(extension => { + const { id, isEnabled, isCompatible, manifest } = extension; + const { name, description, version } = manifest; + const isUninstalling = extensionInstallationStateStore.isExtensionUninstalling(id); + const toggleExtension = toggleExtensionWith(isEnabled); + + return { + extension: ( +
+
+
{name}
+
{description}
+
+
+ ), + version, + status: ( +
+ {getStatus(extension)} +
+ ), + actions: ( + + {isCompatible && ( + toggleExtension(id)} + > + + + {isEnabled ? "Disable" : "Enabled"} + + + )} + + confirmUninstallExtension(extension)} + > + + Uninstall + + + ), + }; + })} + items={userExtensions.get()} filters={[ (extension) => extension.manifest.name, (extension) => getStatus(extension), @@ -170,8 +175,12 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension export const InstalledExtensions = withInjectables(NonInjectedInstalledExtensions, { getProps: (di, props) => ({ + ...props, extensionDiscovery: di.inject(extensionDiscoveryInjectable), extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), - ...props, + userExtensions: di.inject(userExtensionsInjectable), + enableExtension: di.inject(enableExtensionInjectable), + disableExtension: di.inject(disableExtensionInjectable), + confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable), }), }); diff --git a/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx b/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx index 6a68107ad5..d70d56b5f9 100644 --- a/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx @@ -27,7 +27,7 @@ const uninstallExtensionInjectable = getInjectable({ const showErrorNotification = di.inject(showErrorNotificationInjectable); return async (extensionId: LensExtensionId): Promise => { - const ext = extensionLoader.getExtension(extensionId); + const ext = extensionLoader.getExtensionById(extensionId); if (!ext) { logger.debug(`[EXTENSIONS]: cannot uninstall ${extensionId}, was not installed`); @@ -45,7 +45,7 @@ const uninstallExtensionInjectable = getInjectable({ await extensionDiscovery.uninstallExtension(extensionId); // wait for the ExtensionLoader to actually uninstall the extension - await when(() => !extensionLoader.userExtensions.has(extensionId)); + await when(() => !extensionLoader.userExtensions.get().has(extensionId)); showSuccessNotification(

diff --git a/packages/core/src/renderer/components/+extensions/user-extensions/user-extensions.injectable.ts b/packages/core/src/renderer/components/+extensions/user-extensions/user-extensions.injectable.ts index 63d5c7a5fc..9c3d8fc3eb 100644 --- a/packages/core/src/renderer/components/+extensions/user-extensions/user-extensions.injectable.ts +++ b/packages/core/src/renderer/components/+extensions/user-extensions/user-extensions.injectable.ts @@ -12,7 +12,7 @@ const userExtensionsInjectable = getInjectable({ instantiate: (di) => { const extensionLoader = di.inject(extensionLoaderInjectable); - return computed(() => [...extensionLoader.userExtensions.values()]); + return computed(() => [...extensionLoader.userExtensions.get().values()]); }, }); diff --git a/packages/core/src/renderer/extension-loader/create-extension-instance.injectable.ts b/packages/core/src/renderer/extension-loader/create-extension-instance.injectable.ts index ebc424641a..fda96f9b7a 100644 --- a/packages/core/src/renderer/extension-loader/create-extension-instance.injectable.ts +++ b/packages/core/src/renderer/extension-loader/create-extension-instance.injectable.ts @@ -30,7 +30,7 @@ const createExtensionInstanceInjectable = getInjectable({ }; return (ExtensionClass, extension) => { - const instance = new ExtensionClass(extension) as LensRendererExtension; + const instance = new ExtensionClass(extension as any) as LensRendererExtension; (instance as Writable)[lensExtensionDependencies] = deps; diff --git a/packages/technical-features/application/legacy-extensions/src/bundled-extension.ts b/packages/technical-features/application/legacy-extensions/src/bundled-extension.ts index f800ee0d37..e743d9dbed 100644 --- a/packages/technical-features/application/legacy-extensions/src/bundled-extension.ts +++ b/packages/technical-features/application/legacy-extensions/src/bundled-extension.ts @@ -1,13 +1,13 @@ import { getInjectionToken } from "@ogre-tools/injectable"; import type { - LensExtensionConstructor, - LensExtensionManifest, + BundledLensExtensionConstructor, + BundledLensExtensionManifest, } from "./lens-extension"; export interface BundledExtension { - readonly manifest: LensExtensionManifest; - main: () => LensExtensionConstructor | null; - renderer: () => LensExtensionConstructor | null; + readonly manifest: BundledLensExtensionManifest; + main: () => Promise; + renderer: () => Promise; } export const bundledExtensionInjectionToken = diff --git a/packages/technical-features/application/legacy-extensions/src/lens-extension.ts b/packages/technical-features/application/legacy-extensions/src/lens-extension.ts index 5c8222113d..dfde441d85 100644 --- a/packages/technical-features/application/legacy-extensions/src/lens-extension.ts +++ b/packages/technical-features/application/legacy-extensions/src/lens-extension.ts @@ -1,26 +1,39 @@ export type LensExtensionId = string; + export type LensExtensionConstructor = new ( ext: InstalledExtension ) => LegacyLensExtension; +export type BundledLensExtensionConstructor = new ( + ext: BundledInstalledExtension +) => LegacyLensExtension; -export interface InstalledExtension { - id: LensExtensionId; - - readonly manifest: LensExtensionManifest; - +export interface BaseInstalledExtension { + readonly id: LensExtensionId; // Absolute path to the non-symlinked source folder, // e.g. "/Users/user/.k8slens/extensions/helloworld" readonly absolutePath: string; - - /** - * Absolute to the symlinked package.json file - */ + // Absolute to the symlinked package.json file readonly manifestPath: string; - readonly isBundled: boolean; +} + +export interface BundledInstalledExtension extends BaseInstalledExtension { + readonly manifest: BundledLensExtensionManifest; + readonly isBundled: true; + readonly isCompatible: true; + readonly isEnabled: true; +} + +export interface ExternalInstalledExtension extends BaseInstalledExtension { + readonly manifest: LensExtensionManifest; + readonly isBundled: false; readonly isCompatible: boolean; isEnabled: boolean; } +export type InstalledExtension = + | BundledInstalledExtension + | ExternalInstalledExtension; + export interface LegacyLensExtension { readonly id: LensExtensionId; readonly manifest: LensExtensionManifest; @@ -38,22 +51,11 @@ export interface LegacyLensExtension { activate(): Promise; } -export interface LensExtensionManifest { +export interface BundledLensExtensionManifest { name: string; version: string; description?: string; - - main?: string; // path to %ext/dist/main.js - renderer?: string; // path to %ext/dist/renderer.js - /** - * Supported Lens version engine by extension could be defined in `manifest.engines.lens` - * Only MAJOR.MINOR version is taken in consideration. - */ - engines: { - lens: string; // "semver"-package format - npm?: string; - node?: string; - }; + publishConfig?: Partial>; /** * Specify extension name used for persisting data. @@ -61,3 +63,17 @@ export interface LensExtensionManifest { */ storeName?: string; } + +export interface LensExtensionManifest extends BundledLensExtensionManifest { + main?: string; // path to %ext/dist/main.js + renderer?: string; // path to %ext/dist/renderer.js + + /** + * Supported Lens version engine by extension could be defined in `manifest.engines.lens` + * Only MAJOR.MINOR version is taken in consideration. + */ + engines: { + lens: string; // "semver"-package format + [x: string]: string | undefined; + }; +}