diff --git a/src/extensions/__tests__/extension-loader.test.ts b/src/extensions/__tests__/extension-loader.test.ts index d2eec85a79..06d4feb23d 100644 --- a/src/extensions/__tests__/extension-loader.test.ts +++ b/src/extensions/__tests__/extension-loader.test.ts @@ -9,7 +9,7 @@ jest.mock( () => ({ ipcRenderer: { invoke: jest.fn(async (channel: string) => { - if (channel === "extensions:loaded") { + if (channel === "extensions:main") { return [ [ manifestPath, @@ -44,7 +44,7 @@ jest.mock( }), on: jest.fn( (channel: string, listener: (event: any, ...args: any[]) => void) => { - if (channel === "extensions:loaded") { + if (channel === "extensions:main") { // First initialize with extensions 1 and 2 // and then broadcast event to remove extensioin 2 and add extension number 3 setTimeout(() => { diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 71eaa19524..30ef1157ec 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -1,5 +1,6 @@ import { app, ipcRenderer, remote } from "electron"; import { EventEmitter } from "events"; +import { isEqual } from "lodash"; import { action, computed, observable, reaction, toJS, when } from "mobx"; import path from "path"; import { getHostedCluster } from "../common/cluster-store"; @@ -25,7 +26,12 @@ const logModule = "[EXTENSIONS-LOADER]"; export class ExtensionLoader { protected extensions = observable.map(); protected instances = observable.map(); - protected readonly requestExtensionsChannel = "extensions:loaded"; + + // IPC channel to broadcast changes to extensions from main + protected static readonly extensionsMainChannel = "extensions:main"; + + // IPC channel to broadcast changes to extensions from renderer + protected static readonly extensionsRendererChannel = "extensions:renderer"; // emits event "remove" of type LensExtension when the extension is removed private events = new EventEmitter(); @@ -95,28 +101,27 @@ export class ExtensionLoader { this.loadOnMain(); this.broadcastExtensions(); - reaction(() => this.extensions.toJS(), () => { + reaction(() => this.toJSON(), () => { this.broadcastExtensions(); }); - handleRequest(this.requestExtensionsChannel, () => { + handleRequest(ExtensionLoader.extensionsMainChannel, () => { return Array.from(this.toJSON()); }); + + subscribeToBroadcast(ExtensionLoader.extensionsRendererChannel, (_event, extensions: [LensExtensionId, InstalledExtension][]) => { + this.syncExtensions(extensions); + }); } protected async initRenderer() { const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => { this.isLoaded = true; + this.syncExtensions(extensions); + const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId); - - // Add new extensions - extensions.forEach(([extId, ext]) => { - if (!this.extensions.has(extId)) { - this.extensions.set(extId, ext); - } - }); - - // Remove deleted extensions + + // Remove deleted extensions in renderer side only this.extensions.forEach((_, lensExtensionId) => { if (!receivedExtensionIds.includes(lensExtensionId)) { this.removeExtension(lensExtensionId); @@ -124,14 +129,26 @@ export class ExtensionLoader { }); }; - requestMain(this.requestExtensionsChannel).then(extensionListHandler); - subscribeToBroadcast(this.requestExtensionsChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => { + reaction(() => this.toJSON(), () => { + this.broadcastExtensions(false); + }); + + requestMain(ExtensionLoader.extensionsMainChannel).then(extensionListHandler); + subscribeToBroadcast(ExtensionLoader.extensionsMainChannel, (_event, extensions: [LensExtensionId, InstalledExtension][]) => { extensionListHandler(extensions); }); } + syncExtensions(extensions: [LensExtensionId, InstalledExtension][]) { + extensions.forEach(([lensExtensionId, extension]) => { + if (!isEqual(this.extensions.get(lensExtensionId), extension)) { + this.extensions.set(lensExtensionId, extension); + } + }); + } + loadOnMain() { - logger.info(`${logModule}: load on main`); + logger.debug(`${logModule}: load on main`); this.autoInitExtensions(async (extension: LensMainExtension) => { // Each .add returns a function to remove the item const removeItems = [ @@ -151,7 +168,7 @@ export class ExtensionLoader { } loadOnClusterManagerRenderer() { - logger.info(`${logModule}: load on main renderer (cluster manager)`); + logger.debug(`${logModule}: load on main renderer (cluster manager)`); this.autoInitExtensions(async (extension: LensRendererExtension) => { const removeItems = [ registries.globalPageRegistry.add(extension.globalPages, extension), @@ -174,7 +191,7 @@ export class ExtensionLoader { } loadOnClusterRenderer() { - logger.info(`${logModule}: load on cluster renderer (dashboard)`); + logger.debug(`${logModule}: load on cluster renderer (dashboard)`); const cluster = getHostedCluster(); this.autoInitExtensions(async (extension: LensRendererExtension) => { @@ -204,26 +221,26 @@ export class ExtensionLoader { protected autoInitExtensions(register: (ext: LensExtension) => Promise) { return reaction(() => this.toJSON(), installedExtensions => { - for (const [extId, ext] of installedExtensions) { + for (const [extId, extension] of installedExtensions) { const alreadyInit = this.instances.has(extId); - if (ext.isEnabled && !alreadyInit) { + if (extension.isEnabled && !alreadyInit) { try { - const LensExtensionClass = this.requireExtension(ext); + const LensExtensionClass = this.requireExtension(extension); if (!LensExtensionClass) { continue; } - const instance = new LensExtensionClass(ext); + const instance = new LensExtensionClass(extension); instance.whenEnabled(() => register(instance)); instance.enable(); this.instances.set(extId, instance); } catch (err) { - logger.error(`${logModule}: activation extension error`, { ext, err }); + logger.error(`${logModule}: activation extension error`, { ext: extension, err }); } - } else if (!ext.isEnabled && alreadyInit) { + } else if (!extension.isEnabled && alreadyInit) { this.removeInstance(extId); } } @@ -262,8 +279,8 @@ export class ExtensionLoader { }); } - broadcastExtensions() { - broadcastMessage(this.requestExtensionsChannel, Array.from(this.toJSON())); + broadcastExtensions(main = true) { + broadcastMessage(main ? ExtensionLoader.extensionsMainChannel : ExtensionLoader.extensionsRendererChannel, Array.from(this.toJSON())); } } diff --git a/src/extensions/extensions-store.ts b/src/extensions/extensions-store.ts index 1533c9ad89..07edb20453 100644 --- a/src/extensions/extensions-store.ts +++ b/src/extensions/extensions-store.ts @@ -45,17 +45,6 @@ export class ExtensionsStore extends BaseStore { await extensionLoader.whenLoaded; await this.whenLoaded; - // apply state on changes from store - reaction(() => this.state.toJS(), extensionsState => { - extensionsState.forEach((state, extId) => { - const ext = extensionLoader.getExtension(extId); - - if (ext && !ext.isBundled) { - ext.isEnabled = state.enabled; - } - }); - }); - // save state on change `extension.isEnabled` reaction(() => this.getState(extensionLoader), extensionsState => { this.state.merge(extensionsState); @@ -65,7 +54,9 @@ export class ExtensionsStore extends BaseStore { isEnabled(extId: LensExtensionId) { const state = this.state.get(extId); - return state && state.enabled; // by default false + // By default false, so that copied extensions are disabled by default. + // If user installs the extension from the UI, the Extensions component will specifically enable it. + return Boolean(state?.enabled); } @action diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 83ec7e27cd..cbadfc7d28 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -91,11 +91,20 @@ export class Extensions extends React.Component { }); this.addedInstalling.forEach(({ id, displayName }) => { + const extension = this.extensions.find(extension => extension.id === id); + + if (!extension) { + throw new Error("Extension not found"); + } + Notifications.ok(

Extension {displayName} successfully installed!

); this.extensionState.delete(id); this.installPath = ""; + + // Enable installed extensions by default. + extension.isEnabled = true; }); }) );