From addc55e577ce93dd7dad0f25bcc962b2bd473955 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 3 Nov 2020 08:08:52 +0200 Subject: [PATCH] auto enable/disable extensions -- part 1 Signed-off-by: Roman --- src/extensions/extension-loader.ts | 81 +++++++++++++------ src/extensions/lens-extension.ts | 31 +++++-- src/main/index.ts | 1 + src/renderer/bootstrap.tsx | 2 + .../components/+extensions/extensions.tsx | 4 +- 5 files changed, 86 insertions(+), 33 deletions(-) diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 435b2fb00e..eafe7e13d1 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -1,18 +1,21 @@ -import type { LensExtension, LensExtensionConstructor, LensExtensionManifest } from "./lens-extension" +import type { LensExtension, LensExtensionConstructor, LensExtensionId, LensExtensionManifest, LensExtensionStoreModel } from "./lens-extension" import type { LensMainExtension } from "./lens-main-extension" import type { LensRendererExtension } from "./lens-renderer-extension" import path from "path" import { broadcastIpc } from "../common/ipc" -import { computed, observable, reaction, toJS, } from "mobx" +import { action, computed, observable, reaction, toJS } from "mobx" import logger from "../main/logger" import { app, ipcRenderer, remote } from "electron" +import { BaseStore } from "../common/base-store"; import * as registries from "./registries"; -type ExtensionManifestPath = string; // path to package.json +export interface ExtensionLoaderStoreModel { + extensions: LensExtensionStoreModel[] +} export interface InstalledExtension { - manifestPath: string; manifest: LensExtensionManifest; + manifestPath: string; isBundled?: boolean; // defined in package.json } @@ -21,19 +24,23 @@ export function extensionPackagesRoot() { return path.join((app || remote.app).getPath("userData")) } -export class ExtensionLoader { - @observable extensions = observable.map([], { deep: false }); - @observable instances = observable.map([], { deep: false }) +export class ExtensionLoader extends BaseStore { + @observable extensions = observable.map([], { deep: false }); + @observable instances = observable.map([], { deep: false }) + @observable state = observable.map(); constructor() { + super({ + configName: "lens-extensions", + }); if (ipcRenderer) { ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => { extensions.forEach((ext) => { - if (!this.getByManifest(ext.manifestPath)) { + if (!this.extensions.has(ext.manifestPath)) { this.extensions.set(ext.manifestPath, ext) } }) - }) + }); } } @@ -43,14 +50,16 @@ export class ExtensionLoader { loadOnMain() { logger.info('[EXTENSIONS-LOADER]: load on main') - this.autoloadExtensions((extension: LensMainExtension) => { + this.autoInitExtensions(); + this.autoEnableExtensions((extension: LensMainExtension) => { extension.registerTo(registries.menuRegistry, extension.appMenus) }) } loadOnClusterManagerRenderer() { logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)') - this.autoloadExtensions((extension: LensRendererExtension) => { + this.autoInitExtensions(); + this.autoEnableExtensions((extension: LensRendererExtension) => { extension.registerTo(registries.globalPageRegistry, extension.globalPages) extension.registerTo(registries.appPreferenceRegistry, extension.appPreferences) extension.registerTo(registries.clusterFeatureRegistry, extension.clusterFeatures) @@ -60,14 +69,32 @@ export class ExtensionLoader { loadOnClusterRenderer() { logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)') - this.autoloadExtensions((extension: LensRendererExtension) => { + this.autoInitExtensions(); + this.autoEnableExtensions((extension: LensRendererExtension) => { extension.registerTo(registries.clusterPageRegistry, extension.clusterPages) extension.registerTo(registries.kubeObjectMenuRegistry, extension.kubeObjectMenuItems) extension.registerTo(registries.kubeObjectDetailRegistry, extension.kubeObjectDetailItems) }) } - protected autoloadExtensions(callback: (instance: LensExtension) => void) { + protected autoEnableExtensions(callback: (ext: LensExtension) => void) { + return reaction(() => this.instances.toJS(), instances => { + instances.forEach(ext => { + const extensionState = this.state.get(ext.id); + const enabledInStore = !extensionState /*enabled by default*/ || extensionState.isEnabled; + if (!ext.isEnabled && enabledInStore) { + ext.enable(); + callback(ext); + } else if (ext.isEnabled && !enabledInStore) { + ext.disable(); + } + }) + }, { + fireImmediately: true, + }) + } + + protected autoInitExtensions() { return reaction(() => this.extensions.toJS(), (installedExtensions) => { for (const [id, ext] of installedExtensions) { let instance = this.instances.get(ext.manifestPath) @@ -79,17 +106,14 @@ export class ExtensionLoader { try { const LensExtensionClass: LensExtensionConstructor = extensionModule.default; instance = new LensExtensionClass(ext); - instance.enable() - callback(instance) - this.instances.set(ext.manifestPath, instance) + this.instances.set(ext.manifestPath, instance); } catch (err) { - logger.error(`[EXTENSIONS-LOADER]: activating extension error`, { ext, err }) + logger.error(`[EXTENSIONS-LOADER]: init extension instance error`, { ext, err }) } } } }, { fireImmediately: true, - delay: 0, }) } @@ -110,26 +134,31 @@ export class ExtensionLoader { } } - getByManifest(manifestPath: ExtensionManifestPath): InstalledExtension { - return this.extensions.get(manifestPath); - } - broadcastExtensions(frameId?: number) { broadcastIpc({ channel: "extensions:loaded", frameId: frameId, frameOnly: !!frameId, - args: [this.toJSON().extensions], + args: [ + Array.from(this.extensions.toJS().values()) + ], }) } - toJSON() { + @action + protected fromStore({ extensions = [] }: ExtensionLoaderStoreModel) { + extensions.forEach(ext => { + this.state.set(ext.id, ext); + }) + } + + toJSON(): ExtensionLoaderStoreModel { return toJS({ - extensions: Array.from(this.extensions).map(([id, instance]) => instance), + extensions: this.userExtensions.map(ext => ext.toJSON()) }, { recurseEverything: true, }) } } -export const extensionLoader = new ExtensionLoader() +export const extensionLoader: ExtensionLoader = ExtensionLoader.getInstance(); diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index 0a8766a019..89cb77e1f0 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -1,10 +1,17 @@ -import { observable, toJS } from "mobx"; +import { action, observable, toJS } from "mobx"; import logger from "../main/logger"; import { BaseRegistry } from "./registries/base-registry"; import type { InstalledExtension } from "./extension-loader"; +export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionConstructor = new (init: InstalledExtension) => LensExtension; +export interface LensExtensionStoreModel { + id: LensExtensionId; + name: string; + isEnabled?: boolean; +} + export interface LensExtensionManifest { name: string; version: string; @@ -27,6 +34,10 @@ export class LensExtension { this.isBundled = !!isBundled } + get id(): LensExtensionId { + return this.manifestPath; + } + get name() { return this.manifest.name } @@ -39,13 +50,17 @@ export class LensExtension { return this.manifest.description } + @action async enable() { + if (this.isEnabled) return; this.isEnabled = true; logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); this.onActivate(); } + @action async disable() { + if (!this.isEnabled) return; this.onDeactivate(); this.isEnabled = false; this.disposers.forEach(cleanUp => cleanUp()); @@ -53,6 +68,14 @@ export class LensExtension { logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`); } + toggle(enable?: boolean) { + if (typeof enable === "boolean") { + enable ? this.enable() : this.disable() + } else { + this.isEnabled ? this.disable() : this.enable() + } + } + protected onActivate() { // mock } @@ -69,12 +92,10 @@ export class LensExtension { }; } - toJSON() { + toJSON(): LensExtensionStoreModel { return toJS({ + id: this.id, name: this.name, - version: this.version, - description: this.description, - manifestPath: this.manifestPath, isEnabled: this.isEnabled, }, { recurseEverything: true, diff --git a/src/main/index.ts b/src/main/index.ts index ff7e050dd2..dc54a5f20b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -53,6 +53,7 @@ app.on("ready", async () => { userStore.load(), clusterStore.load(), workspaceStore.load(), + extensionLoader.load(), ]); // find free port diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 7311e0e0cf..be5cfc4848 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -13,6 +13,7 @@ import { i18nStore } from "./i18n"; import { themeStore } from "./theme.store"; import { App } from "./components/app"; import { LensApp } from "./lens-app"; +import { extensionLoader } from "../extensions/extension-loader"; type AppComponent = React.ComponentType & { init?(): Promise; @@ -34,6 +35,7 @@ export async function bootstrap(App: AppComponent) { userStore.load(), workspaceStore.load(), clusterStore.load(), + extensionLoader.load(), i18nStore.init(), themeStore.init(), ]); diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index f1374a833b..58dbe56ac3 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -67,9 +67,9 @@ export class Extensions extends React.Component { ) } return extensions.map(ext => { - const { manifestPath, name, description, isEnabled } = ext; + const { id, name, description, isEnabled } = ext; return ( -
+
Name: {name}