From abaacddaa76d5257029652b7d3d301068725c5ef Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 5 Nov 2020 14:54:16 +0200 Subject: [PATCH] fix: create extension instance only when enabled Signed-off-by: Roman --- src/extensions/extension-loader.ts | 65 ++++++++++++++----- src/extensions/extension-manager.ts | 13 ++-- src/extensions/extensions-store.ts | 49 ++++++++++++++ src/extensions/lens-extension.ts | 41 ++---------- src/main/index.ts | 5 +- src/renderer/bootstrap.tsx | 2 + .../components/+extensions/extensions.scss | 16 +++-- .../components/+extensions/extensions.tsx | 19 +++--- 8 files changed, 139 insertions(+), 71 deletions(-) create mode 100644 src/extensions/extensions-store.ts diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 64c4270439..80fe68f449 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -4,10 +4,11 @@ import type { LensRendererExtension } from "./lens-renderer-extension" import type { InstalledExtension } from "./extension-manager"; import path from "path" import { broadcastIpc } from "../common/ipc" -import { computed, observable, reaction, when } from "mobx" +import { action, computed, observable, reaction, toJS, when } from "mobx" import logger from "../main/logger" import { app, ipcRenderer, remote } from "electron" import * as registries from "./registries"; +import { extensionsStore } from "./extensions-store"; // lazy load so that we get correct userData export function extensionPackagesRoot() { @@ -16,8 +17,8 @@ export function extensionPackagesRoot() { export class ExtensionLoader { @observable isLoaded = false; - protected extensions = observable.map([], { deep: false }); - protected instances = observable.map([], { deep: false }) + protected extensions = observable.map(); + protected instances = observable.map() constructor() { if (ipcRenderer) { @@ -30,18 +31,39 @@ export class ExtensionLoader { }) }); } + this.manageExtensionsState(); } - @computed get userExtensions(): LensExtension[] { - return [...this.instances.values()].filter(ext => !ext.isBundled) + @computed get userExtensions(): InstalledExtension[] { + return Array.from(this.toJSON().values()).filter(ext => !ext.isBundled) } - async init() { - const { extensionManager } = await import("./extension-manager"); - const installedExtensions = await extensionManager.load(); - this.extensions.replace(installedExtensions); + protected async manageExtensionsState() { + await extensionsStore.whenLoaded; + await when(() => this.isLoaded); + + // apply initial state + this.extensions.forEach((ext, extId) => { + ext.enabled = ext.isBundled || extensionsStore.isEnabled(extId); + }) + + // handle updated state from store + reaction(() => extensionsStore.extensions.toJS(), extensionsState => { + extensionsState.forEach((state, extId) => { + const ext = this.extensions.get(extId); + if (ext && !ext.isBundled && ext.enabled !== state.enabled) { + ext.enabled = state.enabled; + } + }) + }); + } + + @action + async init(extensions: Map) { + this.extensions.replace(extensions); this.isLoaded = true; this.loadOnMain(); + this.broadcastExtensions(); } loadOnMain() { @@ -71,22 +93,26 @@ export class ExtensionLoader { } protected autoInitExtensions(register: (ext: LensExtension) => Function[]) { - return reaction(() => this.extensions.toJS(), (installedExtensions) => { - for (const [id, ext] of installedExtensions) { - let instance = this.instances.get(ext.manifestPath) - if (!instance) { + return reaction(() => this.toJSON(), (installedExtensions) => { + for (const [extId, ext] of installedExtensions) { + let instance = this.instances.get(extId); + if (ext.enabled && !instance) { const extensionModule = this.requireExtension(ext) if (!extensionModule) { - continue + continue; } try { const LensExtensionClass: LensExtensionConstructor = extensionModule.default; instance = new LensExtensionClass(ext); instance.whenEnabled(() => register(instance)); - this.instances.set(ext.manifestPath, instance); + instance.enable(); + this.instances.set(extId, instance); } catch (err) { logger.error(`[EXTENSIONS-LOADER]: init extension instance error`, { ext, err }) } + } else if (!ext.enabled && instance) { + instance.disable(); + this.instances.delete(extId); } } }, { @@ -111,6 +137,13 @@ export class ExtensionLoader { } } + toJSON() { + return toJS(this.extensions, { + exportMapsAsObjects: false, + recurseEverything: true, + }) + } + async broadcastExtensions(frameId?: number) { await when(() => this.isLoaded); broadcastIpc({ @@ -118,7 +151,7 @@ export class ExtensionLoader { frameId: frameId, frameOnly: !!frameId, args: [ - Array.from(this.extensions.toJS().values()) + Array.from(this.toJSON().values()), ], }) } diff --git a/src/extensions/extension-manager.ts b/src/extensions/extension-manager.ts index 1d37707596..f538028adf 100644 --- a/src/extensions/extension-manager.ts +++ b/src/extensions/extension-manager.ts @@ -8,9 +8,10 @@ import { extensionPackagesRoot } from "./extension-loader" import { getBundledExtensions } from "../common/utils/app-version" export interface InstalledExtension { - manifest: LensExtensionManifest; - manifestPath: string; - isBundled?: boolean; // defined in package.json + readonly manifest: LensExtensionManifest; + readonly manifestPath: string; + readonly isBundled?: boolean; // defined in package.json + enabled?: boolean; } type Dependencies = { @@ -77,7 +78,7 @@ export class ExtensionManager { return await this.loadExtensions(); } - protected async getByManifest(manifestPath: string): Promise { + protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise { let manifestJson: LensExtensionManifest; try { fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence @@ -88,6 +89,7 @@ export class ExtensionManager { return { manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"), manifest: manifestJson, + isBundled: isBundled, } } catch (err) { logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson }); @@ -129,9 +131,8 @@ export class ExtensionManager { } const absPath = path.resolve(folderPath, fileName); const manifestPath = path.resolve(absPath, "package.json"); - const ext = await this.getByManifest(manifestPath).catch(() => null) + const ext = await this.getByManifest(manifestPath, { isBundled: true }).catch(() => null) if (ext) { - ext.isBundled = true; extensions.push(ext) } } diff --git a/src/extensions/extensions-store.ts b/src/extensions/extensions-store.ts new file mode 100644 index 0000000000..86eb605c52 --- /dev/null +++ b/src/extensions/extensions-store.ts @@ -0,0 +1,49 @@ +import type { LensExtensionId } from "./lens-extension"; +import { BaseStore } from "../common/base-store" +import { action, observable, toJS } from "mobx"; + +export interface LensExtensionsStoreModel { + extensions: Record; +} + +export interface LensExtensionState { + enabled?: boolean; +} + +export class ExtensionsStore extends BaseStore { + constructor() { + super({ + configName: "lens-extensions" + }); + } + + @observable extensions = observable.map(); + + @action + setEnabled(extId: LensExtensionId, enabled: boolean) { + const state = this.extensions.get(extId); + this.extensions.set(extId, { + ...(state || {}), + enabled: enabled, + }) + } + + isEnabled(extensionId: LensExtensionId) { + const state = this.extensions.get(extensionId); + return !state /* enabled by default */ || state.enabled; + } + + protected fromStore({ extensions }: LensExtensionsStoreModel) { + this.extensions.merge(extensions); + } + + toJSON(): LensExtensionsStoreModel { + return toJS({ + extensions: this.extensions.toJSON(), + }, { + recurseEverything: true + }) + } +} + +export const extensionsStore = new ExtensionsStore(); diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index 246edfe110..d947554557 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -1,7 +1,6 @@ import type { InstalledExtension } from "./extension-manager"; -import { action, reaction } from "mobx"; +import { action, observable, reaction } from "mobx"; import logger from "../main/logger"; -import { ExtensionStore } from "./extension-store"; export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionConstructor = new (...args: ConstructorParameters) => LensExtension; @@ -14,35 +13,17 @@ export interface LensExtensionManifest { renderer?: string; // path to %ext/dist/renderer.js } -export interface LensExtensionStoreModel { - isEnabled: boolean; -} - -export class LensExtension = any> { - protected store: S; +export class LensExtension { readonly manifest: LensExtensionManifest; readonly manifestPath: string; readonly isBundled: boolean; + @observable isEnabled = false; + constructor({ manifest, manifestPath, isBundled }: InstalledExtension) { this.manifest = manifest this.manifestPath = manifestPath this.isBundled = !!isBundled - this.init(); - } - - protected async init(store: S = createBaseStore().getInstance()) { - this.store = store; - await this.store.loadExtension(this); - reaction(() => this.store.data.isEnabled, (isEnabled = true) => { - this.toggle(isEnabled); // handle activation & deactivation - }, { - fireImmediately: true - }); - } - - get isEnabled() { - return !!this.store.data.isEnabled; } get id(): LensExtensionId { @@ -64,7 +45,7 @@ export class LensExtension = a @action async enable() { if (this.isEnabled) return; - this.store.data.isEnabled = true; + this.isEnabled = true; this.onActivate(); logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); } @@ -72,7 +53,7 @@ export class LensExtension = a @action async disable() { if (!this.isEnabled) return; - this.store.data.isEnabled = false; + this.isEnabled = false; this.onDeactivate(); logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`); } @@ -114,13 +95,3 @@ export class LensExtension = a // mock } } - -function createBaseStore() { - return class extends ExtensionStore { - constructor() { - super({ - configName: "state" - }); - } - } -} diff --git a/src/main/index.ts b/src/main/index.ts index 762c166bc4..01b98d0f41 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -21,6 +21,8 @@ import { userStore } from "../common/user-store"; import { workspaceStore } from "../common/workspace-store"; import { appEventBus } from "../common/event-bus" import { extensionLoader } from "../extensions/extension-loader"; +import { extensionManager } from "../extensions/extension-manager"; +import { extensionsStore } from "../extensions/extensions-store"; const workingDir = path.join(app.getPath("appData"), appName); let proxyPort: number; @@ -52,6 +54,7 @@ app.on("ready", async () => { userStore.load(), clusterStore.load(), workspaceStore.load(), + extensionsStore.load(), ]); // find free port @@ -76,7 +79,7 @@ app.on("ready", async () => { } LensExtensionsApi.windowManager = windowManager = new WindowManager(proxyPort); - extensionLoader.init(); // call after windowManager to see splash earlier + extensionLoader.init(await extensionManager.load()); // call after windowManager to see splash earlier setTimeout(() => { appEventBus.emit({ name: "app", action: "start" }) diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 7311e0e0cf..ca27086f4a 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 { extensionsStore } from "../extensions/extensions-store"; type AppComponent = React.ComponentType & { init?(): Promise; @@ -34,6 +35,7 @@ export async function bootstrap(App: AppComponent) { userStore.load(), workspaceStore.load(), clusterStore.load(), + extensionsStore.load(), i18nStore.init(), themeStore.init(), ]); diff --git a/src/renderer/components/+extensions/extensions.scss b/src/renderer/components/+extensions/extensions.scss index 8e9256c201..63778d37e9 100644 --- a/src/renderer/components/+extensions/extensions.scss +++ b/src/renderer/components/+extensions/extensions.scss @@ -2,11 +2,17 @@ --width: 100%; --max-width: auto; - .extension { - --flex-gap: $padding / 3; - padding: $padding $padding * 2; - background: $colorVague; - border-radius: $radius; + .extension-list { + .extension { + --flex-gap: $padding / 3; + padding: $padding $padding * 2; + background: $colorVague; + border-radius: $radius; + + &:not(:first-of-type) { + margin-top: $padding * 2; + } + } } .extensions-path { diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 7dc5f55726..f6b1c4399e 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -12,6 +12,7 @@ import { Icon } from "../icon"; import { PageLayout } from "../layout/page-layout"; import { extensionLoader } from "../../../extensions/extension-loader"; import { extensionManager } from "../../../extensions/extension-manager"; +import { extensionsStore } from "../../../extensions/extensions-store"; @observer export class Extensions extends React.Component { @@ -19,7 +20,8 @@ export class Extensions extends React.Component { @computed get extensions() { const searchText = this.search.toLowerCase(); - return extensionLoader.userExtensions.filter(({ name, description }) => { + return extensionLoader.userExtensions.filter(ext => { + const { name, description } = ext.manifest; return [ name.toLowerCase().includes(searchText), description.toLowerCase().includes(searchText), @@ -68,9 +70,10 @@ export class Extensions extends React.Component { ) } return extensions.map(ext => { - const { id, name, description, isEnabled } = ext; + const { manifestPath: extId, enabled, manifest } = ext; + const { name, description } = manifest; return ( -
+
Name: {name} @@ -79,11 +82,11 @@ export class Extensions extends React.Component { Description: {description}
- {!isEnabled && ( - + {!enabled && ( + )} - {isEnabled && ( - + {enabled && ( + )}
) @@ -102,7 +105,7 @@ export class Extensions extends React.Component { value={this.search} onChange={(value) => this.search = value} /> -
+
{this.renderExtensions()}